diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2068080fa..01a2a432e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,7 +20,7 @@ on: - 'NOTICE' - 'README*' # Daily, so a post dated later than the merge publishes on its date - # (future-dated posts are left out until then), and the docs pick up new releases. + # (future-dated posts are left out until then). schedule: - cron: "0 6 * * *" workflow_dispatch: @@ -34,16 +34,6 @@ jobs: build: runs-on: ubuntu-latest - services: - kroki: - image: yuzutech/kroki - env: - KROKI_MERMAID_HOST: kroki-mermaid - ports: - - 8000:8000 - kroki-mermaid: - image: yuzutech/kroki-mermaid - steps: - name: checkout uses: actions/checkout@v4 @@ -51,16 +41,6 @@ jobs: # The sitemap plugin reads each page's last change from git history. fetch-depth: 0 - # Before the site: the Roq build leaves a node_modules/ in the repository root, and - # npm would install Antora there instead of in docs/. - - name: generate docs - run: | - cd docs - # asciidoctor-kroki must stay on 0.18.1: since 1.0.0, its extensions are - # async and require Asciidoctor.js 4, while Antora 2.3 uses Asciidoctor.js 1.5.9 - npm i @antora/cli@2.3 @antora/site-generator-default@2.3 asciidoctor-kroki@0.18.1 - ./node_modules/@antora/cli/bin/antora --stacktrace generate antora-playbook.yml - - name: generate website uses: quarkiverse/quarkus-roq@v1.1 with: @@ -72,8 +52,14 @@ jobs: - name: put the stylesheet in the pages run: java .github/InlineStyles.java target/roq - - name: add docs to the website - run: cp -r docs/build/site target/roq/docs + # GitHub Pages cannot redirect: the documentation addresses this site published before + # (Antora's /docs///.html) get small redirect pages. + - uses: jbangdev/setup-jbang@2b1b465a7b75f4222b81426f23a01e013aa7b95c # v0.1.1 + with: + version: 0.138.0 + + - name: add redirects for moved documentation + run: jbang tools/docs/Docs.java redirects target/roq - name: attach website uses: actions/upload-artifact@v4 diff --git a/.github/workflows/docs-check.yml b/.github/workflows/docs-check.yml new file mode 100644 index 000000000..cd36b2cbd --- /dev/null +++ b/.github/workflows/docs-check.yml @@ -0,0 +1,88 @@ +name: Check project documentation + +# Reusable: a SmallRye project calls this on pull requests that touch its docs. It renders the +# pull request's AsciiDoc with the smallrye.io site exactly as it will be published, fails on a +# missing include or a page that does not render, and attaches the rendered docs as an artifact. +# +# jobs: +# docs: +# uses: smallrye/smallrye.github.io/.github/workflows/docs-check.yml@develop +# with: +# project: smallrye-graphql # the key in smallrye.github.io data/docs.yml + +on: + workflow_call: + inputs: + project: + description: 'Project key in data/docs.yml' + type: string + required: true + +# It renders pull-request content: read-only, no secrets. +permissions: + contents: read + +jobs: + check: + runs-on: ubuntu-latest + # Diagram blocks ([mermaid], [plantuml], ...) are rendered to SVG by the sync (tools/docs/Docs.java). + services: + kroki: + image: yuzutech/kroki + env: + KROKI_MERMAID_HOST: kroki-mermaid + ports: + - 8000:8000 + kroki-mermaid: + image: yuzutech/kroki-mermaid + env: + KROKI_URL: http://localhost:8000 + steps: + - name: checkout the project + uses: actions/checkout@v4 + with: + path: project + + - name: checkout smallrye.io + uses: actions/checkout@v4 + with: + repository: smallrye/smallrye.github.io + ref: develop + path: site + + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 21 + cache: maven + cache-dependency-path: site/pom.xml + + - uses: jbangdev/setup-jbang@2b1b465a7b75f4222b81426f23a01e013aa7b95c # v0.1.1 + with: + version: 0.138.0 + + - name: copy the docs into the site + env: + PROJECT: ${{ inputs.project }} + run: jbang site/tools/docs/Docs.java sync --strict --project "$PROJECT" --version main --source project + + - name: render the site + working-directory: site + env: + QUARKUS_ROQ_GENERATOR_BATCH: 'true' + run: | + ./mvnw -B -q package quarkus:run -DskipTests 2>&1 | tee ../render.log + # Asciidoctor warnings (such as a section level out of sequence) are reported; a page that + # fails to render, or an include that cannot be resolved, fails the check. + grep -E 'WARNING \[asciidoctor\]' ../render.log | sed 's/^.*WARNING \[asciidoctor\] ([^)]*) /::warning::/' || true + if grep -E 'Roq request failed|Error occurred while rendering|SEVERE \[asciidoctor\]' ../render.log; then + echo "::error::The documentation does not render cleanly, see the lines above" + exit 1 + fi + + - name: attach the rendered docs + uses: actions/upload-artifact@v4 + with: + name: docs-${{ inputs.project }} + path: site/target/roq + retention-days: 7 diff --git a/.github/workflows/docs-sync.yml b/.github/workflows/docs-sync.yml new file mode 100644 index 000000000..c535e74a3 --- /dev/null +++ b/.github/workflows/docs-sync.yml @@ -0,0 +1,101 @@ +name: Sync project documentation + +# Pulls each project's AsciiDoc documentation (data/docs.yml) into content/docs/ and commits it to +# develop, which then builds and deploys the site. Nightly it picks up new commits on the projects' +# main branches and new release tags; it can also be run by hand for one project or one version. + +on: + schedule: + - cron: "30 4 * * *" + workflow_dispatch: + inputs: + project: + description: 'Project key from data/docs.yml (empty: all projects)' + required: false + version: + description: 'Version id (main, latest, a stream id); with ref, syncs exactly that' + required: false + ref: + description: 'Branch or tag to sync the version from' + required: false + force: + description: 'Sync even when nothing changed upstream' + type: boolean + default: false + +concurrency: + group: docs-sync + cancel-in-progress: false + +jobs: + sync: + if: github.repository_owner == 'smallrye' + runs-on: ubuntu-latest + # Diagram blocks ([mermaid], [plantuml], ...) are rendered to SVG by the sync (tools/docs/Docs.java). + services: + kroki: + image: yuzutech/kroki + env: + KROKI_MERMAID_HOST: kroki-mermaid + ports: + - 8000:8000 + kroki-mermaid: + image: yuzutech/kroki-mermaid + env: + KROKI_URL: http://localhost:8000 + steps: + - uses: actions/create-github-app-token@v1 + id: app-token + with: + app-id: ${{ vars.CI_APP_ID }} + private-key: ${{ secrets.CI_APP_PRIVATE_KEY }} + + - uses: actions/checkout@v4 + with: + ref: develop + # A push made with the app token (not GITHUB_TOKEN) starts the website build. + token: ${{ steps.app-token.outputs.token }} + + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 21 + + - uses: jbangdev/setup-jbang@2b1b465a7b75f4222b81426f23a01e013aa7b95c # v0.1.1 + with: + version: 0.138.0 + + # Releases still documented with MkDocs are converted with pandoc. + - name: install pandoc + run: sudo apt-get update -qq && sudo apt-get install -y -qq pandoc + + - name: sync + env: + PROJECT: ${{ inputs.project }} + VERSION: ${{ inputs.version }} + REF: ${{ inputs.ref }} + FORCE: ${{ inputs.force }} + run: | + if [ -n "$VERSION" ] && [ -n "$REF" ]; then + args=(--project "$PROJECT" --version "$VERSION" --ref "$REF") + else + args=(--auto) + [ -n "$PROJECT" ] && args+=(--project "$PROJECT") + [ "$FORCE" = true ] && args+=(--force) + fi + jbang tools/docs/Docs.java sync "${args[@]}" --summary "$GITHUB_STEP_SUMMARY" + + - name: commit + run: | + git add -A content/docs data/docversions.yml + if git diff --cached --quiet; then + echo "Documentation unchanged" + exit 0 + fi + slug='${{ steps.app-token.outputs.app-slug }}' + git config user.name "$slug[bot]" + git config user.email "$(gh api "/users/$slug%5Bbot%5D" --jq .id)+$slug[bot]@users.noreply.github.com" + git commit -q -m "Sync project documentation" + git push origin develop + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} diff --git a/.gitignore b/.gitignore index d90d69d7f..0695d4521 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ package-lock.json .idea .vscode .quarkus +__pycache__/ diff --git a/README.md b/README.md index 09d8d209d..04788871d 100644 --- a/README.md +++ b/README.md @@ -51,22 +51,76 @@ Add `content/posts/YYYY-MM-DD-name.adoc`. The header carries the post's details: `:page-author:` is a key in `data/authors.yml`; add yourself there first. A post dated in the future is published by the first build on or after that date (the workflow runs daily). -## Building the documentation +## Project documentation -The documentation at `/docs/` is built separately with Antora, from the repositories listed in -`docs/antora-playbook.yml`, and copied into the site by CI. +Each project keeps its documentation in its own repository, as AsciiDoc laid out like the +Quarkus guides (`docs/src/main/asciidoc/`: one page per `.adoc` file, `_nav.yml` for the sidebar, +`_attributes.adoc` for shared attributes, examples included from compiled sources). This site +pulls it in and publishes it at `/docs///`. -1. Install Antora. See [Antora installation](https://docs.antora.org/antora/2.3/install/install-antora/). -2. Go to the `docs` directory and run +| What | Where | +| --- | --- | +| The projects, their repositories and the versions followed | `data/docs.yml` | +| The synced pages, one directory per version | `content/docs///` | +| Synced versions, their refs and navigation (generated) | `data/docversions.yml` | +| The documentation layout and its sidebar | `templates/layouts/doc.html`, `templates/partials/docs-nav.html` | +| The `/docs/` hub | `content/docs/index.html` | +| Sync and conversion tools | `tools/docs/` | + +The tools are one [JBang](https://www.jbang.dev) program, `tools/docs/Docs.java`: Java and JBang are +all they need (pandoc, or podman to run it in a container, only to convert releases still +documented with MkDocs). + +`sync` copies a project's docs from a branch or tag (or a local checkout) into `content/docs/`. +The `Sync project documentation` workflow runs it nightly: each project's `main` branch becomes +`main`, and its newest release tag becomes `latest`; the commit to `develop` then deploys. A release +whose docs are still MkDocs or Antora is converted on the fly by `mkdocs2adoc` or `antora2adoc`, +which are also the tools to convert a project's sources once. + +```bash +jbang tools/docs/Docs.java sync --auto # what the nightly job does +jbang tools/docs/Docs.java sync --project smallrye-graphql --version main --source ../smallrye-graphql +jbang tools/docs/Docs.java --help # every tool +``` + +### Previewing a project's documentation + +In a project's checkout, `jbang docs-preview@smallrye/smallrye.github.io` (or `./mvnw -Pdocs-preview` +where the project has that profile) shows its documentation on a local smallrye.io at +`http://localhost:8080/docs//main/`, and publishes it again whenever a page or an included +example changes. It downloads this site's sources once a day; `--site ../smallrye.github.io` uses a +checkout instead, to try changes to the site itself. + +Projects call `.github/workflows/docs-check.yml` from their pull requests: it renders their docs +with this site and fails on a broken include or a page that does not render. + +Converting a project once (then commit the result in the project's repository): + +```bash +jbang tools/docs/Docs.java mkdocs2adoc --mkdocs ../project/mkdocs.yml --out ../project/docs/src/main/asciidoc +jbang tools/docs/Docs.java antora2adoc --antora ../project/doc --out ../project/docs/src/main/asciidoc +``` + +Diagram blocks (`[mermaid]`, `[plantuml]`, ...) are rendered to SVG during the sync, by a Kroki +server (`KROKI_URL`, https://kroki.io by default; the workflows run their own). + +### Old addresses - antora generate antora-playbook.yml --fetch +The documentation addresses this site published before (Antora's +`/docs///.html`) are listed under `legacy` in `data/docs.yml`. The build +writes a redirect page for each of them (`Docs.java redirects`), and +`Docs.java rewrite-links --check target/roq content/posts/*.adoc` points blog links at the new +addresses, checking every page and anchor against the generated site. -3. The HTML for the SmallRye documentation is now in `docs/build/site`. +The projects' old MkDocs sites (their `gh-pages` branches, at `smallrye.io///`) are +replaced by redirect pages: `Docs.java gh-pages-redirects --project --checkout +--site target/roq` sends every page of every version to the same page of the version this site +publishes for that major line (older majors are streams in `data/docs.yml`). Javadoc stays as it is. ## Deployment Pushing to `develop` runs `.github/workflows/build.yml`: it generates the site with the Roq -GitHub Action, builds the documentation with Antora, and pushes the result to the `main` +GitHub Action (project docs included), adds the redirects for old documentation addresses, and pushes the result to the `main` branch, which GitHub Pages serves. Pull requests build the site without deploying it. The workflow also runs daily and can be started by hand from the Actions tab. diff --git a/config/application.properties b/config/application.properties index 57bfbdc0a..b9ceb61cd 100644 --- a/config/application.properties +++ b/config/application.properties @@ -18,4 +18,21 @@ quarkus.roq.generator.timeout=180 # the Jekyll site. The layouts print the title, so Asciidoctor must not repeat it: a key # ending in "!" unsets the attribute (the value is ignored but cannot be empty). quarkus.asciidoc.attributes."showtitle!"=unset -quarkus.asciidoc.attributes.idseparator=_ +# Asciidoctor renders in SAFE mode, which keeps includes inside base_dir. The site root (rather +# than each page's directory) lets a docs page in a subdirectory include its version's +# _attributes.adoc and _examples/, while nothing outside the site can be included. +quarkus.asciidoc.base-dir=site +# "@" makes it a default a page can override: blog posts keep Jekyll's ids (_like_this), while +# project docs set Quarkus-style ids (like-this) in their _attributes.adoc. +quarkus.asciidoc.attributes.idseparator=_@ + +# Project documentation, pulled from each project's repository by tools/docs/sync.py into +# content/docs/// and published at /docs////. +site.collections.docs=true +site.collections.docs.layout=doc +site.collections.docs.link=/:path +# Docs are AsciiDoc: their {attribute} references are not Qute expressions. +site.escaped-pages=docs/*/** +# The docs templates walk data/docs.yml and data/docversions.yml, whose list items Qute can +# only type as Object; as on quarkus.io, check those expressions at render time instead. +quarkus.qute.type-check-excludes=java.lang.Object.* diff --git a/content/404.html b/content/404.html index 286928a47..84d9392f9 100644 --- a/content/404.html +++ b/content/404.html @@ -10,4 +10,4 @@ # own grey screen instead of the site's. link: /404.html --- -

The requested page could not be found. Try the home page, the project list or the documentation.

+

The requested page could not be found. Try the home page, the project list or the documentation.

diff --git a/content/docs/index.html b/content/docs/index.html new file mode 100644 index 000000000..0a41428dd --- /dev/null +++ b/content/docs/index.html @@ -0,0 +1,31 @@ +--- +layout: page +title: Documentation +description: Documentation for every SmallRye project, from the latest release back to the main branch. +eyebrow: SmallRye +link: /docs/ +--- +{@io.quarkiverse.roq.frontmatter.runtime.model.Site site} +{! The projects and their documentation versions come from data/docs.yml and data/docversions.yml. !} +{#for g in cdi:docs.getJsonArray('groups')} +

{=g.getString('name')}

+ +{/for} diff --git a/content/docs/jandex/latest/_attributes.adoc b/content/docs/jandex/latest/_attributes.adoc new file mode 100644 index 000000000..356e69a55 --- /dev/null +++ b/content/docs/jandex/latest/_attributes.adoc @@ -0,0 +1,18 @@ +:project-version: 3.6.0 +// Attributes shared by every page. smallrye.io overrides the ones it needs when it publishes. +// Pages in subdirectories set :version-root: (../ per level) before including this file. +ifndef::version-root[:version-root:] +:idprefix: +:idseparator: - +:sectanchors: +:partialsdir: {version-root}_partials +:examples-dir: {version-root}_examples +:generated-dir: {version-root}_generated + +// Added by smallrye.io (tools/docs/Docs.java sync) +:javadoc-version: 3.6.0 +:relfileprefix: /docs/jandex/latest/ +:imagesdir: /docs/jandex/latest +:docs-project: jandex +:docs-version: latest +:docs-ref: 3.6.0 diff --git a/content/docs/jandex/latest/_nav.yml b/content/docs/jandex/latest/_nav.yml new file mode 100644 index 000000000..7594b1a6d --- /dev/null +++ b/content/docs/jandex/latest/_nav.yml @@ -0,0 +1,20 @@ +title: Jandex +nav: +- title: Introduction + page: index +- title: Jandex + items: + - title: Getting Jandex + page: core/getting + - title: Browsing Declarations and Types + page: core/browsing + - title: Indexing Classes + page: core/indexing +- title: Jandex Maven Plugin + items: + - title: Basic Usage + page: maven/basic + - title: Advanced Usage + page: maven/advanced + - title: Usage with Shading + page: maven/shading diff --git a/content/docs/jandex/latest/core/browsing.adoc b/content/docs/jandex/latest/core/browsing.adoc new file mode 100644 index 000000000..631289c77 --- /dev/null +++ b/content/docs/jandex/latest/core/browsing.adoc @@ -0,0 +1,156 @@ += Browsing Declarations and Types +:version-root: ../ +include::{version-root}_attributes.adoc[] + +== Browsing a Class + +The following example demonstrates indexing a class and browsing its methods: + +[source,java] +---- +Index index = Index.of(Map.class); + +ClassInfo clazz = index.getClassByName(DotName.createSimple("java.util.Map")); + +for (MethodInfo method : clazz.methods()) { + System.out.println(method); +} +---- + +== Searching for Annotations + +The following example demonstrates indexing the `Thread` and `String` classes, and searching for methods that have been marked with `@Deprecated`: + +[source,java] +---- +Indexer indexer = new Indexer(); +indexer.indexClass(Thread.class); +indexer.indexClass(String.class); +Index index = indexer.complete(); + +DotName deprecated = DotName.createSimple("java.lang.Deprecated"); +List annotations = index.getAnnotations(deprecated); + +for (AnnotationInstance annotation : annotations) { + switch (annotation.target().kind()) { + case METHOD: + System.out.println(annotation.target()); + break; + } +} +---- + +== Analyzing Generics + +The following example demonstrates indexing the `Collections` class and printing the resolved bound on the `List` method parameter, which resolves to `Comparable` from the method type parameter. + +The `sort()` method analyzed by the example is defined in source as: + +[source,java] +---- +public static > void sort(List list) +---- + +The example code, which prints `Comparable` followed by `T`, is: + +[source,java] +---- +Index index = Index.of(Collections.class); + +ClassInfo clazz = index.getClassByName(DotName.createSimple("java.util.Collections")); +Type listType = Type.create(DotName.createSimple("java.util.List"), Type.Kind.CLASS); +MethodInfo sort = clazz.method("sort", listType); + +Type t = sort.parameterTypes().get(0).asParameterizedType() // List> + .arguments().get(0).asTypeVariable() // T extends Comparable + .bounds().get(0); // Comparable + +System.out.println(t); + +Type b = t.asParameterizedType() // Comparable + .arguments().get(0).asWildcardType() // ? super T + .superBound(); // T + +System.out.println(b); +---- + +== Browsing Type Annotations + +Consider a declaration with complex nested generic type which contains a `@Label` annotation: + +[source,java] +---- +Map> names; +---- + +The following code will print `Name`, the annotation value associated with the type: + +[source,java] +---- +Indexer indexer = new Indexer(); +indexer.indexClass(Test.class); +indexer.indexClass(Test.Label.class); +Index index = indexer.complete(); + +FieldInfo field = index.getClassByName(Test.class).field("names"); +System.out.println( + field.type().asParameterizedType() // Map> + .arguments().get(1).asParameterizedType() // List<@Label("Name") String> + .arguments().get(0) // @Label("Name") String + .annotations().get(0) // @Label("Name") + .value().asString() // "Name" +); +---- + +== Searching for Type Annotations + +A type annotation can also be located by searching for the annotation. +The target for a found type annotation is represented as a `TypeTarget`. +The `TypeTarget` provides a reference to the annotated type, as well as the enclosing target that contains the type. +The target itself can be a method, a class, or a field. +The usage of that target can be a number of places, including parameters, return types, type parameters, type arguments, class extends values, type bounds and receiver types. +Subclasses of `TypeTarget` provide the necessary information to locate the starting point of the usage. + +Since the particular type use can occur at any depth, the relevant branch of the type tree constrained by the above starting point must be traversed to understand the context of the use. + +Consider a declaration with complex nested generic type which contains a `@Label` annotation: + +[source,java] +---- +Map> names; +---- + +The following code locates a type annotation and then inspects its location: + +[source,java] +---- +Indexer indexer = new Indexer(); +indexer.indexClass(Test.class); +indexer.indexClass(Test.Label.class); +Index index = indexer.complete(); + +DotName label = DotName.createSimple("Test$Label"); +List annotations = index.getAnnotations(label); +for (AnnotationInstance annotation : annotations) { + if (annotation.target().kind() == AnnotationTarget.Kind.TYPE) { + TypeTarget typeTarget = annotation.target().asType(); + System.out.println("Type usage is located within: " + typeTarget.enclosingTarget()); + System.out.println("Usage type: " + typeTarget.usage()); + System.out.println("Target type: " + typeTarget.target()); + System.out.println("Expected target? " + (typeTarget.enclosingTarget().asField().type() + .asParameterizedType().arguments().get(1) + .asParameterizedType().arguments().get(0) + == typeTarget.target())); + } +} +---- + +The output is: + +[source] +---- +Type usage is located within: java.util.Map> Test.names +Usage type: EMPTY +Target type: java.lang.@Label("Name") String +Expected target? true +---- diff --git a/content/docs/jandex/latest/core/getting.adoc b/content/docs/jandex/latest/core/getting.adoc new file mode 100644 index 000000000..33d10a90c --- /dev/null +++ b/content/docs/jandex/latest/core/getting.adoc @@ -0,0 +1,21 @@ += Getting Jandex +:version-root: ../ +include::{version-root}_attributes.adoc[] + +== Using Jandex in a Maven Project + +Just add the following to your POM: + +[source,xml] +---- + + io.smallrye + jandex + ${version.jandex} + +---- + +== Downloading Jandex + +If you don't use Maven, you can download the Jandex JAR link:https://search.maven.org/search?q=g:io.smallrye%20a:jandex[from Maven Central]. +This JAR is completely self-sufficient and doesn't require any dependencies. diff --git a/content/docs/jandex/latest/core/indexing.adoc b/content/docs/jandex/latest/core/indexing.adoc new file mode 100644 index 000000000..33c78394d --- /dev/null +++ b/content/docs/jandex/latest/core/indexing.adoc @@ -0,0 +1,74 @@ += Indexing Classes +:version-root: ../ +include::{version-root}_attributes.adoc[] + +== Creating a Persistent Index Using the API + +The following example demonstrates indexing a class and persisting the index to disk. +The resulting file can later be loaded and used. + +[source,java] +---- +Indexer indexer = new Indexer(); +indexer.indexClass(Map.class); +Index index = indexer.complete(); + +try (FileOutputStream out = new FileOutputStream("/tmp/index.idx")) { + IndexWriter writer = new IndexWriter(out); + writer.write(index); +} +---- + +== Loading a Persistent Index + +The following example demonstrates loading the index from the previous example and using that index to print all methods on `java.util.Map`: + +[source,java] +---- +Index index; +try (FileInputStream input = new FileInputStream("/tmp/index.idx")) { + IndexReader reader = new IndexReader(input); + index = reader.read(); +} + +ClassInfo clazz = index.getClassByName(DotName.createSimple("java.util.Map")); + +for (MethodInfo method : clazz.methods()) { + System.out.println(method); +} +---- + +== Creating a Persistent Index Using the CLI + +The following example demonstrates indexing hibernate core, followed by the entire Java +JDK 8 using Jandex on the CLI: + +[source,bash] +---- +$ java -jar jandex-2.4.2.Final.jar hibernate-core-6.0.0.Final.jar + Wrote hibernate-core-6.0.0.Final-jar.idx in 0.9170 seconds + (5746 classes, 50 annotations, 2995 instances, 61729 class usages, 1737428 bytes) +$ java -jar jandex-2.4.2.Final.jar rt.jar + Wrote rt-jar.idx in 1.7310 seconds + (20226 classes, 57 annotations, 2476 instances, 246298 class usages, 5890787 bytes) +---- + +The above summary output tells us that this version of Hibernate ORM has 5,746 classes, and those classes contained 2,995 annotation declarations, using 50 different annotation types. +The resulting index is 1.7 MB uncompressed, which is only 19% of the 9.0MB compressed jar size, or 4% of the uncompressed class file data. +If the index is stored in the jar (using the -m option) it can be compressed an additional 43%, leading to a jar growth of only 11%. + +== Using the Ant Task to Index Your Project + +The following Ant task can be used with either the `maven-antrun-plugin` or an Ant build to build an index for your project: + +[source,xml] +---- + + + + +---- + +== Using the Maven Plugin to Index Your Project + +See xref:maven/basic.adoc[Basic Maven Plugin Usage]. diff --git a/content/docs/jandex/latest/index.adoc b/content/docs/jandex/latest/index.adoc new file mode 100644 index 000000000..b9ddf2a4e --- /dev/null +++ b/content/docs/jandex/latest/index.adoc @@ -0,0 +1,85 @@ += Jandex +include::_attributes.adoc[] + +Jandex is a space efficient Java class file indexer and offline reflection library. + +It supports: + +* Indexing a set of classes into a memory efficient representation +* Browsing and searching for annotations, including Java 8 type annotations +* Browsing and searching for declarations and types, including generic type information +* Browsing and searching through the class and interface inheritance hierarchy +* Persisting an index in a custom storage efficient format and fast loading of that format +* Compatibility with previous API and storage format versions, as described in the <> +* Execution via API, command line tool, Ant, and Maven plugin + +Remember that Jandex has extensive API documentation in the link:https://javadoc.io/doc/io.smallrye/jandex/latest/index.html[javadoc form]. +This documentation site is currently rather incomplete, so please consult the javadoc frequently. + +== Requirements + +Jandex 3.0+ requires Java 8 to run. + +TIP: The runtime Java version does _not_ affect the Jandex bytecode parsing capabilities. +Jandex running on Java 8 can still parse Java 17 bytecode. + +== Getting Help + +Issues can be reported in link:https://github.com/smallrye/jandex/issues[GitHub Issues]. + +For discussions, use the link:https://groups.google.com/g/smallrye[SmallRye Google Group] or the link:https://quarkusio.zulipchat.com/[Quarkus Zulip chat]. + +[[compatibility_promise]] +== Compatibility Promise + +Jandex uses an `X.Y.Z` versioning scheme. +In the following text, we call `X` a _major_ version, `Y` a _minor_ version, and `Z` a _micro_ version. + +=== API + +Jandex may break backward compatibility for _callers_ of the Jandex API in major versions. +If you only call Jandex API, updating to a newer minor or micro version is safe. + +Jandex may break backward compatibility for users that _extend_ Jandex classes or _implement_ Jandex interfaces in minor or major versions. +If you extend Jandex classes or implement Jandex interfaces, updating to a newer micro version is safe. + +=== Persistent Index Format + +The persistent index format is versioned. +Jandex is backward compatible when it comes to reading the persistent index, but not forward compatible. +In other words, newer Jandex can read older index, but older Jandex can't read newer index. + +Jandex may introduce a new persistent index format version in minor or major versions. +If you distribute a Jandex index as part of your artifacts, updating to a newer micro version is safe. +Updating to a newer minor or major version may require consumers of the Jandex index to also update. + +[[persistent_index_format_versions]] +== Persistent Index Format Versions + +For reference, this table shows which Jandex version produces which persistent format version by default. +It is also a maximum persistent index format version the given Jandex version can read. + +|=== +|Jandex version |Persistent format version + +|Jandex 3.3.x, 3.4.x, 3.5.x, 3.6.x +|13 + +|Jandex 3.2.x +|12 + +|Jandex 3.0.x, 3.1.x +|11 + +|Jandex 2.4.x +|10 + +|Jandex 2.1.1+, 2.2.x, 2.3.x +|9 + +|Jandex 2.1.0 +|8 (version 7 only in Jandex 2.1.0.Beta1 and Beta2) + +|Jandex 2.0.x +|6 +|=== diff --git a/content/docs/jandex/latest/maven/advanced.adoc b/content/docs/jandex/latest/maven/advanced.adoc new file mode 100644 index 000000000..417961d5e --- /dev/null +++ b/content/docs/jandex/latest/maven/advanced.adoc @@ -0,0 +1,123 @@ += Advanced Maven Plugin Usage +:version-root: ../ +include::{version-root}_attributes.adoc[] + +If you need to process more than one directory of classes, you can specify multiple `fileSets` like this: + +[source,xml] +---- + + + + io.smallrye + jandex-maven-plugin + ${version.jandex} + + + make-index + + jandex + + + + + ${project.build.directory}/generated-classes/foo + + + ${project.build.directory}/generated-classes/bar + + + + + + + + +---- + +To turn _off_ processing of `target/classes`, add the following configuration: + +[source,xml] +---- +false +---- + +For any `fileSet`, you can also fine-tune the classes that are processed using the following options: + +[source,xml] +---- + + ${project.build.directory}/somedir + + + **/indexed/*.class + **/idxd/*.class + + + + **/*NotIndexed.class + + + false + +---- + +A `fileSet` may specify a _dependency_ instead of a directory. +That dependency must exist among the set of dependencies of the Maven project being built. +A `groupId` and `artifactId` are mandatory, a `classifier` is optional: + +[source,xml] +---- + + + com.example + my-project + + + com/example/my/project/api/**/*.class + + + com/example/**/_private/*.class + + +---- + +== Index Location and Version + +To change where the persistent index is located, the `indexDir` and `indexName` configuration properties can be used. + +CAUTION: These properties should be left unchanged. +Jandex consumers typically look for a persistent Jandex index in the default location. + +The `indexDir` property defaults to `${project.build.outputDirectory}/META-INF` (so `target/classes/META-INF` by default), while `indexName` defaults to `jandex.idx`. +Therefore, the index is by default stored to `target/classes/META-INF/jandex.idx`. + +This example moves the index to `target/classes/index.dat`: + +[source,xml] +---- + + io.smallrye + jandex-maven-plugin + ${version.jandex} + + + make-index + + jandex + + + ${project.build.outputDirectory} + index.dat + + + + +---- + +The `indexVersion` property can be used to produce an older index version. +By default, the index version produced is the latest version supported by the Jandex version. +Possible values of the `indexVersion` property can be found in link:/docs/jandex/latest/#persistent_index_format_versions[the table of persistent index format versions]. + +CAUTION: It is usually best to leave the index version unchanged. +Exercise care when upgrading Jandex itself. diff --git a/content/docs/jandex/latest/maven/basic.adoc b/content/docs/jandex/latest/maven/basic.adoc new file mode 100644 index 000000000..8d24608cd --- /dev/null +++ b/content/docs/jandex/latest/maven/basic.adoc @@ -0,0 +1,30 @@ += Basic Maven Plugin Usage +:version-root: ../ +include::{version-root}_attributes.adoc[] + +This is how you add a persistent Jandex index to your Maven project: + +[source,xml] +---- + + + + io.smallrye + jandex-maven-plugin + ${version.jandex} + + + make-index + + jandex + + + + + + +---- + +The `jandex` goal is bound to the `process-classes` phase by default. + +This configuration will index all `.class` files in your `target/classes` directory, and write the index to `target/classes/META-INF/jandex.idx`. diff --git a/content/docs/jandex/latest/maven/shading.adoc b/content/docs/jandex/latest/maven/shading.adoc new file mode 100644 index 000000000..ed0d3b27d --- /dev/null +++ b/content/docs/jandex/latest/maven/shading.adoc @@ -0,0 +1,51 @@ += Using Jandex Maven Plugin with Shading +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The Jandex Maven plugin has an additional goal `jandex-jar` that can be used to create an index inside an existing JAR. +This goal is not bound to any phase by default, so you have to configure that manually. + +It is useful together with shading, where the Maven Shade plugin creates a JAR from multiple previously existing JARs. +A shaded JAR may already contain a Jandex index, if at least one of the constituent JARs contains one, but that index is most likely _not_ what you want. +First, it is an unmodified index originating in one of the constituent JARs. +If multiple constituent JARs contain an index, only one of them makes it to the shaded JAR; the others are lost. +Second, during shading, classes may be relocated, so the index data may become stale. + +There is no support for merging existing index files from the original JARs. +Likewise, there is no support for applying class relocations over an existing index. + +In short, if you want a shaded JAR to have a Jandex index, you have to reindex it. + +[source,xml] +---- + + io.smallrye + jandex-maven-plugin + ${version.jandex} + + + uberjar-index + package + + jandex-jar + + + ${project.build.directory}/${project.build.finalName}.jar + + com/example/my/project/**/*.class + + + com/example/**/_private/*.class + + + + + +---- + +If we want to reindex a shaded JAR, we have to make sure that the `jandex-jar` goal of the Jandex Maven plugin executes _later_ than the `shade` goal of the Maven Shade plugin. +In this example, we bind the `jandex-jar` goal to the `package` phase, which is also the phase in which the `shade` goal of the Maven Shade plugin executes by default. +In such case, we have to put the `` element of the Jandex Maven plugin _after_ the `` element of the Maven Shade plugin. + +Remember that if the Jandex Maven plugin operates on a JAR that was produced by the Maven Shade plugin, and if the Maven Shade plugin is configured to perform class relocations, the Jandex Maven plugin operates on already relocated classes. +This is important for configuring includes and excludes correctly. diff --git a/content/docs/jandex/main/_attributes.adoc b/content/docs/jandex/main/_attributes.adoc new file mode 100644 index 000000000..d0832da94 --- /dev/null +++ b/content/docs/jandex/main/_attributes.adoc @@ -0,0 +1,18 @@ +:project-version: 3.6.0 +// Attributes shared by every page. smallrye.io overrides the ones it needs when it publishes. +// Pages in subdirectories set :version-root: (../ per level) before including this file. +ifndef::version-root[:version-root:] +:idprefix: +:idseparator: - +:sectanchors: +:partialsdir: {version-root}_partials +:examples-dir: {version-root}_examples +:generated-dir: {version-root}_generated + +// Added by smallrye.io (tools/docs/Docs.java sync) +:javadoc-version: 3.6.0 +:relfileprefix: /docs/jandex/main/ +:imagesdir: /docs/jandex/main +:docs-project: jandex +:docs-version: main +:docs-ref: main diff --git a/content/docs/jandex/main/_nav.yml b/content/docs/jandex/main/_nav.yml new file mode 100644 index 000000000..7594b1a6d --- /dev/null +++ b/content/docs/jandex/main/_nav.yml @@ -0,0 +1,20 @@ +title: Jandex +nav: +- title: Introduction + page: index +- title: Jandex + items: + - title: Getting Jandex + page: core/getting + - title: Browsing Declarations and Types + page: core/browsing + - title: Indexing Classes + page: core/indexing +- title: Jandex Maven Plugin + items: + - title: Basic Usage + page: maven/basic + - title: Advanced Usage + page: maven/advanced + - title: Usage with Shading + page: maven/shading diff --git a/content/docs/jandex/main/core/browsing.adoc b/content/docs/jandex/main/core/browsing.adoc new file mode 100644 index 000000000..631289c77 --- /dev/null +++ b/content/docs/jandex/main/core/browsing.adoc @@ -0,0 +1,156 @@ += Browsing Declarations and Types +:version-root: ../ +include::{version-root}_attributes.adoc[] + +== Browsing a Class + +The following example demonstrates indexing a class and browsing its methods: + +[source,java] +---- +Index index = Index.of(Map.class); + +ClassInfo clazz = index.getClassByName(DotName.createSimple("java.util.Map")); + +for (MethodInfo method : clazz.methods()) { + System.out.println(method); +} +---- + +== Searching for Annotations + +The following example demonstrates indexing the `Thread` and `String` classes, and searching for methods that have been marked with `@Deprecated`: + +[source,java] +---- +Indexer indexer = new Indexer(); +indexer.indexClass(Thread.class); +indexer.indexClass(String.class); +Index index = indexer.complete(); + +DotName deprecated = DotName.createSimple("java.lang.Deprecated"); +List annotations = index.getAnnotations(deprecated); + +for (AnnotationInstance annotation : annotations) { + switch (annotation.target().kind()) { + case METHOD: + System.out.println(annotation.target()); + break; + } +} +---- + +== Analyzing Generics + +The following example demonstrates indexing the `Collections` class and printing the resolved bound on the `List` method parameter, which resolves to `Comparable` from the method type parameter. + +The `sort()` method analyzed by the example is defined in source as: + +[source,java] +---- +public static > void sort(List list) +---- + +The example code, which prints `Comparable` followed by `T`, is: + +[source,java] +---- +Index index = Index.of(Collections.class); + +ClassInfo clazz = index.getClassByName(DotName.createSimple("java.util.Collections")); +Type listType = Type.create(DotName.createSimple("java.util.List"), Type.Kind.CLASS); +MethodInfo sort = clazz.method("sort", listType); + +Type t = sort.parameterTypes().get(0).asParameterizedType() // List> + .arguments().get(0).asTypeVariable() // T extends Comparable + .bounds().get(0); // Comparable + +System.out.println(t); + +Type b = t.asParameterizedType() // Comparable + .arguments().get(0).asWildcardType() // ? super T + .superBound(); // T + +System.out.println(b); +---- + +== Browsing Type Annotations + +Consider a declaration with complex nested generic type which contains a `@Label` annotation: + +[source,java] +---- +Map> names; +---- + +The following code will print `Name`, the annotation value associated with the type: + +[source,java] +---- +Indexer indexer = new Indexer(); +indexer.indexClass(Test.class); +indexer.indexClass(Test.Label.class); +Index index = indexer.complete(); + +FieldInfo field = index.getClassByName(Test.class).field("names"); +System.out.println( + field.type().asParameterizedType() // Map> + .arguments().get(1).asParameterizedType() // List<@Label("Name") String> + .arguments().get(0) // @Label("Name") String + .annotations().get(0) // @Label("Name") + .value().asString() // "Name" +); +---- + +== Searching for Type Annotations + +A type annotation can also be located by searching for the annotation. +The target for a found type annotation is represented as a `TypeTarget`. +The `TypeTarget` provides a reference to the annotated type, as well as the enclosing target that contains the type. +The target itself can be a method, a class, or a field. +The usage of that target can be a number of places, including parameters, return types, type parameters, type arguments, class extends values, type bounds and receiver types. +Subclasses of `TypeTarget` provide the necessary information to locate the starting point of the usage. + +Since the particular type use can occur at any depth, the relevant branch of the type tree constrained by the above starting point must be traversed to understand the context of the use. + +Consider a declaration with complex nested generic type which contains a `@Label` annotation: + +[source,java] +---- +Map> names; +---- + +The following code locates a type annotation and then inspects its location: + +[source,java] +---- +Indexer indexer = new Indexer(); +indexer.indexClass(Test.class); +indexer.indexClass(Test.Label.class); +Index index = indexer.complete(); + +DotName label = DotName.createSimple("Test$Label"); +List annotations = index.getAnnotations(label); +for (AnnotationInstance annotation : annotations) { + if (annotation.target().kind() == AnnotationTarget.Kind.TYPE) { + TypeTarget typeTarget = annotation.target().asType(); + System.out.println("Type usage is located within: " + typeTarget.enclosingTarget()); + System.out.println("Usage type: " + typeTarget.usage()); + System.out.println("Target type: " + typeTarget.target()); + System.out.println("Expected target? " + (typeTarget.enclosingTarget().asField().type() + .asParameterizedType().arguments().get(1) + .asParameterizedType().arguments().get(0) + == typeTarget.target())); + } +} +---- + +The output is: + +[source] +---- +Type usage is located within: java.util.Map> Test.names +Usage type: EMPTY +Target type: java.lang.@Label("Name") String +Expected target? true +---- diff --git a/content/docs/jandex/main/core/getting.adoc b/content/docs/jandex/main/core/getting.adoc new file mode 100644 index 000000000..33d10a90c --- /dev/null +++ b/content/docs/jandex/main/core/getting.adoc @@ -0,0 +1,21 @@ += Getting Jandex +:version-root: ../ +include::{version-root}_attributes.adoc[] + +== Using Jandex in a Maven Project + +Just add the following to your POM: + +[source,xml] +---- + + io.smallrye + jandex + ${version.jandex} + +---- + +== Downloading Jandex + +If you don't use Maven, you can download the Jandex JAR link:https://search.maven.org/search?q=g:io.smallrye%20a:jandex[from Maven Central]. +This JAR is completely self-sufficient and doesn't require any dependencies. diff --git a/content/docs/jandex/main/core/indexing.adoc b/content/docs/jandex/main/core/indexing.adoc new file mode 100644 index 000000000..33c78394d --- /dev/null +++ b/content/docs/jandex/main/core/indexing.adoc @@ -0,0 +1,74 @@ += Indexing Classes +:version-root: ../ +include::{version-root}_attributes.adoc[] + +== Creating a Persistent Index Using the API + +The following example demonstrates indexing a class and persisting the index to disk. +The resulting file can later be loaded and used. + +[source,java] +---- +Indexer indexer = new Indexer(); +indexer.indexClass(Map.class); +Index index = indexer.complete(); + +try (FileOutputStream out = new FileOutputStream("/tmp/index.idx")) { + IndexWriter writer = new IndexWriter(out); + writer.write(index); +} +---- + +== Loading a Persistent Index + +The following example demonstrates loading the index from the previous example and using that index to print all methods on `java.util.Map`: + +[source,java] +---- +Index index; +try (FileInputStream input = new FileInputStream("/tmp/index.idx")) { + IndexReader reader = new IndexReader(input); + index = reader.read(); +} + +ClassInfo clazz = index.getClassByName(DotName.createSimple("java.util.Map")); + +for (MethodInfo method : clazz.methods()) { + System.out.println(method); +} +---- + +== Creating a Persistent Index Using the CLI + +The following example demonstrates indexing hibernate core, followed by the entire Java +JDK 8 using Jandex on the CLI: + +[source,bash] +---- +$ java -jar jandex-2.4.2.Final.jar hibernate-core-6.0.0.Final.jar + Wrote hibernate-core-6.0.0.Final-jar.idx in 0.9170 seconds + (5746 classes, 50 annotations, 2995 instances, 61729 class usages, 1737428 bytes) +$ java -jar jandex-2.4.2.Final.jar rt.jar + Wrote rt-jar.idx in 1.7310 seconds + (20226 classes, 57 annotations, 2476 instances, 246298 class usages, 5890787 bytes) +---- + +The above summary output tells us that this version of Hibernate ORM has 5,746 classes, and those classes contained 2,995 annotation declarations, using 50 different annotation types. +The resulting index is 1.7 MB uncompressed, which is only 19% of the 9.0MB compressed jar size, or 4% of the uncompressed class file data. +If the index is stored in the jar (using the -m option) it can be compressed an additional 43%, leading to a jar growth of only 11%. + +== Using the Ant Task to Index Your Project + +The following Ant task can be used with either the `maven-antrun-plugin` or an Ant build to build an index for your project: + +[source,xml] +---- + + + + +---- + +== Using the Maven Plugin to Index Your Project + +See xref:maven/basic.adoc[Basic Maven Plugin Usage]. diff --git a/content/docs/jandex/main/index.adoc b/content/docs/jandex/main/index.adoc new file mode 100644 index 000000000..b9ddf2a4e --- /dev/null +++ b/content/docs/jandex/main/index.adoc @@ -0,0 +1,85 @@ += Jandex +include::_attributes.adoc[] + +Jandex is a space efficient Java class file indexer and offline reflection library. + +It supports: + +* Indexing a set of classes into a memory efficient representation +* Browsing and searching for annotations, including Java 8 type annotations +* Browsing and searching for declarations and types, including generic type information +* Browsing and searching through the class and interface inheritance hierarchy +* Persisting an index in a custom storage efficient format and fast loading of that format +* Compatibility with previous API and storage format versions, as described in the <> +* Execution via API, command line tool, Ant, and Maven plugin + +Remember that Jandex has extensive API documentation in the link:https://javadoc.io/doc/io.smallrye/jandex/latest/index.html[javadoc form]. +This documentation site is currently rather incomplete, so please consult the javadoc frequently. + +== Requirements + +Jandex 3.0+ requires Java 8 to run. + +TIP: The runtime Java version does _not_ affect the Jandex bytecode parsing capabilities. +Jandex running on Java 8 can still parse Java 17 bytecode. + +== Getting Help + +Issues can be reported in link:https://github.com/smallrye/jandex/issues[GitHub Issues]. + +For discussions, use the link:https://groups.google.com/g/smallrye[SmallRye Google Group] or the link:https://quarkusio.zulipchat.com/[Quarkus Zulip chat]. + +[[compatibility_promise]] +== Compatibility Promise + +Jandex uses an `X.Y.Z` versioning scheme. +In the following text, we call `X` a _major_ version, `Y` a _minor_ version, and `Z` a _micro_ version. + +=== API + +Jandex may break backward compatibility for _callers_ of the Jandex API in major versions. +If you only call Jandex API, updating to a newer minor or micro version is safe. + +Jandex may break backward compatibility for users that _extend_ Jandex classes or _implement_ Jandex interfaces in minor or major versions. +If you extend Jandex classes or implement Jandex interfaces, updating to a newer micro version is safe. + +=== Persistent Index Format + +The persistent index format is versioned. +Jandex is backward compatible when it comes to reading the persistent index, but not forward compatible. +In other words, newer Jandex can read older index, but older Jandex can't read newer index. + +Jandex may introduce a new persistent index format version in minor or major versions. +If you distribute a Jandex index as part of your artifacts, updating to a newer micro version is safe. +Updating to a newer minor or major version may require consumers of the Jandex index to also update. + +[[persistent_index_format_versions]] +== Persistent Index Format Versions + +For reference, this table shows which Jandex version produces which persistent format version by default. +It is also a maximum persistent index format version the given Jandex version can read. + +|=== +|Jandex version |Persistent format version + +|Jandex 3.3.x, 3.4.x, 3.5.x, 3.6.x +|13 + +|Jandex 3.2.x +|12 + +|Jandex 3.0.x, 3.1.x +|11 + +|Jandex 2.4.x +|10 + +|Jandex 2.1.1+, 2.2.x, 2.3.x +|9 + +|Jandex 2.1.0 +|8 (version 7 only in Jandex 2.1.0.Beta1 and Beta2) + +|Jandex 2.0.x +|6 +|=== diff --git a/content/docs/jandex/main/maven/advanced.adoc b/content/docs/jandex/main/maven/advanced.adoc new file mode 100644 index 000000000..4f993ca32 --- /dev/null +++ b/content/docs/jandex/main/maven/advanced.adoc @@ -0,0 +1,123 @@ += Advanced Maven Plugin Usage +:version-root: ../ +include::{version-root}_attributes.adoc[] + +If you need to process more than one directory of classes, you can specify multiple `fileSets` like this: + +[source,xml] +---- + + + + io.smallrye + jandex-maven-plugin + ${version.jandex} + + + make-index + + jandex + + + + + ${project.build.directory}/generated-classes/foo + + + ${project.build.directory}/generated-classes/bar + + + + + + + + +---- + +To turn _off_ processing of `target/classes`, add the following configuration: + +[source,xml] +---- +false +---- + +For any `fileSet`, you can also fine-tune the classes that are processed using the following options: + +[source,xml] +---- + + ${project.build.directory}/somedir + + + **/indexed/*.class + **/idxd/*.class + + + + **/*NotIndexed.class + + + false + +---- + +A `fileSet` may specify a _dependency_ instead of a directory. +That dependency must exist among the set of dependencies of the Maven project being built. +A `groupId` and `artifactId` are mandatory, a `classifier` is optional: + +[source,xml] +---- + + + com.example + my-project + + + com/example/my/project/api/**/*.class + + + com/example/**/_private/*.class + + +---- + +== Index Location and Version + +To change where the persistent index is located, the `indexDir` and `indexName` configuration properties can be used. + +CAUTION: These properties should be left unchanged. +Jandex consumers typically look for a persistent Jandex index in the default location. + +The `indexDir` property defaults to `${project.build.outputDirectory}/META-INF` (so `target/classes/META-INF` by default), while `indexName` defaults to `jandex.idx`. +Therefore, the index is by default stored to `target/classes/META-INF/jandex.idx`. + +This example moves the index to `target/classes/index.dat`: + +[source,xml] +---- + + io.smallrye + jandex-maven-plugin + ${version.jandex} + + + make-index + + jandex + + + ${project.build.outputDirectory} + index.dat + + + + +---- + +The `indexVersion` property can be used to produce an older index version. +By default, the index version produced is the latest version supported by the Jandex version. +Possible values of the `indexVersion` property can be found in link:/docs/jandex/main/#persistent_index_format_versions[the table of persistent index format versions]. + +CAUTION: It is usually best to leave the index version unchanged. +Exercise care when upgrading Jandex itself. diff --git a/content/docs/jandex/main/maven/basic.adoc b/content/docs/jandex/main/maven/basic.adoc new file mode 100644 index 000000000..8d24608cd --- /dev/null +++ b/content/docs/jandex/main/maven/basic.adoc @@ -0,0 +1,30 @@ += Basic Maven Plugin Usage +:version-root: ../ +include::{version-root}_attributes.adoc[] + +This is how you add a persistent Jandex index to your Maven project: + +[source,xml] +---- + + + + io.smallrye + jandex-maven-plugin + ${version.jandex} + + + make-index + + jandex + + + + + + +---- + +The `jandex` goal is bound to the `process-classes` phase by default. + +This configuration will index all `.class` files in your `target/classes` directory, and write the index to `target/classes/META-INF/jandex.idx`. diff --git a/content/docs/jandex/main/maven/shading.adoc b/content/docs/jandex/main/maven/shading.adoc new file mode 100644 index 000000000..ed0d3b27d --- /dev/null +++ b/content/docs/jandex/main/maven/shading.adoc @@ -0,0 +1,51 @@ += Using Jandex Maven Plugin with Shading +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The Jandex Maven plugin has an additional goal `jandex-jar` that can be used to create an index inside an existing JAR. +This goal is not bound to any phase by default, so you have to configure that manually. + +It is useful together with shading, where the Maven Shade plugin creates a JAR from multiple previously existing JARs. +A shaded JAR may already contain a Jandex index, if at least one of the constituent JARs contains one, but that index is most likely _not_ what you want. +First, it is an unmodified index originating in one of the constituent JARs. +If multiple constituent JARs contain an index, only one of them makes it to the shaded JAR; the others are lost. +Second, during shading, classes may be relocated, so the index data may become stale. + +There is no support for merging existing index files from the original JARs. +Likewise, there is no support for applying class relocations over an existing index. + +In short, if you want a shaded JAR to have a Jandex index, you have to reindex it. + +[source,xml] +---- + + io.smallrye + jandex-maven-plugin + ${version.jandex} + + + uberjar-index + package + + jandex-jar + + + ${project.build.directory}/${project.build.finalName}.jar + + com/example/my/project/**/*.class + + + com/example/**/_private/*.class + + + + + +---- + +If we want to reindex a shaded JAR, we have to make sure that the `jandex-jar` goal of the Jandex Maven plugin executes _later_ than the `shade` goal of the Maven Shade plugin. +In this example, we bind the `jandex-jar` goal to the `package` phase, which is also the phase in which the `shade` goal of the Maven Shade plugin executes by default. +In such case, we have to put the `` element of the Jandex Maven plugin _after_ the `` element of the Maven Shade plugin. + +Remember that if the Jandex Maven plugin operates on a JAR that was produced by the Maven Shade plugin, and if the Maven Shade plugin is configured to perform class relocations, the Jandex Maven plugin operates on already relocated classes. +This is important for configuring includes and excludes correctly. diff --git a/content/docs/smallrye-config/2/_attributes.adoc b/content/docs/smallrye-config/2/_attributes.adoc new file mode 100644 index 000000000..6bb868eca --- /dev/null +++ b/content/docs/smallrye-config/2/_attributes.adoc @@ -0,0 +1,21 @@ +:project-version: 2.13.3 +// Attributes shared by every page. smallrye.io overrides the ones it needs when it publishes. +// Pages in subdirectories set :version-root: (../ per level) before including this file. +ifndef::version-root[:version-root:] +:idprefix: +:idseparator: - +:sectanchors: +:examples-dir: {version-root}_examples +:partialsdir: {version-root}_partials +:project-name: SmallRye Config +:javadoc-version: latest +:version: {project-version} +:generated-dir: {version-root}_generated + +// Added by smallrye.io (tools/docs/Docs.java sync) +:javadoc-version: 2.13.3 +:relfileprefix: /docs/smallrye-config/2/ +:imagesdir: /docs/smallrye-config/2 +:docs-project: smallrye-config +:docs-version: 2 +:docs-ref: 2.13.3 diff --git a/content/docs/smallrye-config/2/_nav.yml b/content/docs/smallrye-config/2/_nav.yml new file mode 100644 index 000000000..e5bcafd99 --- /dev/null +++ b/content/docs/smallrye-config/2/_nav.yml @@ -0,0 +1,58 @@ +title: SmallRye Config +nav: +- title: Home + page: index +- title: Config + items: + - title: Getting Started + page: config/getting-started + - title: Expressions + page: config/expressions + - title: Profiles + page: config/profiles + - title: Environment Variables + page: config/environment-variables + - title: Indexed Properties + page: config/indexed-properties + - title: ConfigValue + page: config/config-value + - title: Mappings + page: config/mappings + - title: Map Support + page: config/map-support + - title: Secret Keys + page: config/secret-keys + - title: Configuration Reference + page: config/configuration +- title: Config Sources + items: + - title: Custom + page: config-sources/custom + - title: Factories + page: config-sources/factories + - title: Locations + page: config-sources/locations + - title: YAML + page: config-sources/yaml + - title: FileSystem + page: config-sources/filesystem + - title: ZooKeeper + page: config-sources/zookeeper + - title: HOCON + page: config-sources/hocon +- title: Converters + items: + - title: Custom + page: converters/custom + - title: JSON + page: converters/json +- title: Extensions + items: + - title: Interceptors + page: extensions/interceptors + - title: Fallback + page: extensions/fallback + - title: Relocate + page: extensions/relocate + - title: Logging + page: extensions/logging diff --git a/content/docs/smallrye-config/2/assets/smallrye-icon.svg b/content/docs/smallrye-config/2/assets/smallrye-icon.svg new file mode 100644 index 000000000..c10b011e0 --- /dev/null +++ b/content/docs/smallrye-config/2/assets/smallrye-icon.svg @@ -0,0 +1,21 @@ + + + + + SmallRye Icon + + + + + + + + + diff --git a/content/docs/smallrye-config/2/config-sources/custom.adoc b/content/docs/smallrye-config/2/config-sources/custom.adoc new file mode 100644 index 000000000..76dd15c11 --- /dev/null +++ b/content/docs/smallrye-config/2/config-sources/custom.adoc @@ -0,0 +1,173 @@ += Config Sources +:version-root: ../ +include::{version-root}_attributes.adoc[] + +== Custom `ConfigSource` + +It’s possible to create a custom ConfigSource as specified in +https://github.com/eclipse/microprofile-config/[MicroProfile Config]. + +With a Custom `ConfigSource` it is possible to read additional configuration values and add them to the `Config` +instance in a defined ordinal. This allows overriding values from other sources or falling back to other values. + +A custom `ConfigSource` requires an implementation of `org.eclipse.microprofile.config.spi.ConfigSource` or +`org.eclipse.microprofile.config.spi.ConfigSourceProvider`. Each implementation requires registration via the +`ServiceLoader` mechanism, either in `META-INF/services/org.eclipse.microprofile.config.spi.ConfigSource` or +`META-INF/services/org.eclipse.microprofile.config.spi.ConfigSourceProvider` files. + +Consider a simple in-memory ConfigSource: + +[source,java] +---- +package org.acme.config; + +import org.eclipse.microprofile.config.spi.ConfigSource; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +public class InMemoryConfigSource implements ConfigSource { + private static final Map configuration = new HashMap<>(); + + static { + configuration.put("my.prop", "1234"); + } + + @Override + public int getOrdinal() { + return 275; + } + + @Override + public Set getPropertyNames() { + return configuration.keySet(); + } + + @Override + public String getValue(final String propertyName) { + return configuration.get(propertyName); + } + + @Override + public String getName() { + return InMemoryConfigSource.class.getSimpleName(); + } +} +---- + +And registration in: + +_META-INF/services/org.eclipse.microprofile.config.spi.ConfigSource_ + +[source,properties] +---- +org.acme.config.InMemoryConfigSource +---- + +The `InMemoryConfigSource` will be ordered between the `System properties` config source, and the +`Environment variables` config source due to the `350` ordinal. + +In this case, `my.prop` from `InMemoryConfigSource` will only be used if the `Config` instance is unable to find a +value in `System Properties`, ignoring all the other lower ordinal config sources. + +== `ConfigSourceFactory` + +Another way to create a `ConfigSource` is via the SmallRye Config `io.smallrye.config.ConfigSourceFactory` API. The +difference between the SmallRye Config factory and the standard way to create a `ConfigSource` as specified in +https://github.com/eclipse/microprofile-config/[MicroProfile Config], is the factory ability to provide a context +with access to the current configuration. + +Each implementation of `io.smallrye.config.ConfigSourceFactory` requires registration via the `ServiceLoader` mechanism +in the `META-INF/services/io.smallrye.config.ConfigSourceFactory` file. + +[source,java] +---- +package org.acme.config; + +import java.util.Collections; +import java.util.OptionalInt; + +import org.eclipse.microprofile.config.spi.ConfigSource; + +import io.smallrye.config.ConfigSourceContext; +import io.smallrye.config.ConfigSourceFactory; +import io.smallrye.config.ConfigValue; +import io.smallrye.config.PropertiesConfigSource; + +public class URLConfigSourceFactory implements ConfigSourceFactory { + @Override + public Iterable getConfigSources(final ConfigSourceContext context) { + final ConfigValue value = context.getValue("config.url"); + if (value == null || value.getValue() == null) { + return Collections.emptyList(); + } + + try { + return Collections.singletonList(new PropertiesConfigSource(new URL(value.getValue()))); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public OptionalInt getPriority() { + return OptionalInt.of(290); + } +} +---- + +And registration in: + +_META-INF/services/io.smallrye.config.ConfigSourceFactory_ + +[source,properties] +---- +org.acme.config.URLConfigSourceFactory +---- + +By implementing `io.smallrye.config.ConfigSourceFactory`, a list of `ConfigSource` may be provided via the +`Iterable++<++ConfigSource++>++ getConfigSources(ConfigSourceContext context)` method. The `ConfigSourceFactory` may also +assign a priority by overriding the method `OptionalInt getPriority()`. The priority value is used to sort multiple +`io.smallrye.config.ConfigSourceFactory` (if found). Higher the value, higher the priority. + +[WARNING] +==== + +The `io.smallrye.config.ConfigSourceFactory` priority does not affect the `ConfigSource` ordinal. These are sorted +independently. + +==== + +When the Factory is initializing, the provided `ConfigSourceContext` may call the method +`ConfigValue getValue(String name)`. This method looks up configuration names in all `ConfigSource`s that were already +initialized by the `Config` instance, including sources with lower ordinals than the ones defined in the +`ConfigSourceFactory`. This means that a `Config` instance is initialized in two steps: first all `ConfigSource` and +`ConfigSourceProvider` and then the `ConfigSourceFactory`. Only configuation values found in the first step are avaible +to the ++`++ConfigSourceFactory. + +The `ConfigSource` list provided by a `ConfigSourceFactory` is not taken into consideration to +configure other sources produced by a lower priority `ConfigSourceFactory`. + +== Override `ConfigSource` ordinal + +The special configuration property name `config++_++ordinal` can be set in any `ConfigSource` to override its default +ordinal. + +For instance, setting the system property `-Dconfig++_++ordinal=200` will override the ordinal for the `System properties` +config source and move it to be looked up after the `Environment Variables` config source. + +== Properties + +The https://github.com/smallrye/smallrye-config/blob/main/implementation/src/main/java/io/smallrye/config/PropertiesConfigSource.java[PropertiesConfigSource] +creates a `ConfigSource` from Java `Properties`, `Map++<++String, String++>++` objects or a `.properties` file (referenced by +its URL). + +== Config Value Properties + +The https://github.com/smallrye/smallrye-config/blob/main/implementation/src/main/java/io/smallrye/config/ConfigValuePropertiesConfigSource.java[ConfigValuePropertiesConfigSource] a `ConfigSource` from Java `Properties`, `Map++<++String, String++>++` objects or +a `.properties` file (referenced by its URL) with support for `io.smallrye.config.ConfigValue`. + +== `.env` + +The https://github.com/smallrye/smallrye-config/blob/main/implementation/src/main/java/io/smallrye/config/DotEnvConfigSourceProvider.java[DotEnvConfigSourceProvider] create a `ConfigSource` from a `.env` file. diff --git a/content/docs/smallrye-config/2/config-sources/factories.adoc b/content/docs/smallrye-config/2/config-sources/factories.adoc new file mode 100644 index 000000000..ff1d09d07 --- /dev/null +++ b/content/docs/smallrye-config/2/config-sources/factories.adoc @@ -0,0 +1,86 @@ += Config Source Factory +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Another way to create a `ConfigSource` is via the `ConfigSourceFactory`. The difference between the SmallRye Config +Factory and the standard way to make a `ConfigSource` as specified in MicroProfile Config is the Factory's ability to +provide a context with access to the available configuration. With the `ConfigSourceFactory` it is possible to +bootstrap a `ConfigSource` that configures itself with previously initialized `ConfigSource's`. + +By implementing +https://github.com/smallrye/smallrye-config/blob/main/implementation/src/main/java/io/smallrye/config/ConfigSourceFactory.java[ConfigSourceFactory], +a list of `ConfigSource's` may be provided via the `Iterable++<++ConfigSource++>++ getConfigSources(ConfigSourceContext context)` method. The `ConfigSourceFactory` may also +assign a priority by overriding the default method `OptionalInt getPriority()`. The priority only sorts the factories +during initialization. After initialization, the provided `ConfigSources` will use their own ordinal and sorted with +all `ConfigSources` available in the `Config` instance. + +When the Factory initializes, the provided `ConfigSourceContext` may call the method +`ConfigValue getValue(String name)`. This method looks up configuration names in all `ConfigSource's` already +initialized by the `Config` instance, including sources with lower ordinals than the ones defined in the +`ConfigSourceFactory`. The `ConfigSourceFactory` does not consider `ConfigSources's` provided by other +`ConfigSourceFactory's` (the priority does not matter). + +Registration of a `ConfigSourceFactory` is done via the `ServiceLoader` mechanism by providing the +implementation classes in a `META-INF/services/io.smallrye.config.ConfigSourceFactory` file. Alternatively, factories +may be registered via the Programmatic API in `SmallRyeConfigBuilder#withSources`. + +A `ConfigSourceFactory` requires an implementation of `io.smallrye.config.ConfigSourceFactory`. Each implementation +requires registration via the `ServiceLoader` mechanism in the +`META-INF/services/io.smallrye.config.ConfigSourceFactory` file. Alternatively, interceptors may be registered via the +Programmatic API in `SmallRyeConfigBuilder#withSources`. + +[source,java] +---- +package org.acme.config + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Collections; +import java.util.List; + +import org.eclipse.microprofile.config.spi.ConfigSource; + +import io.smallrye.config.ConfigMessages; +import io.smallrye.config.PropertiesConfigSource; +import io.smallrye.config.ConfigSourceContext; +import io.smallrye.config.ConfigSourceFactory; +import io.smallrye.config.ConfigValue; + +public class FileSystemConfigSourceFactory implements ConfigSourceFactory { + @Override + public Iterable getConfigSources(final ConfigSourceContext context) { + final ConfigValue value = context.getValue("org.acme.config.file.locations"); + if (value == null || value.getValue() == null) { + return Collections.emptyList(); + } + + try { + return List.of(new PropertiesConfigSource(toURL(value.getValue()), 250)); + } catch (IOException e) { + return Collections.emptyList(); + } + } + + private URL toURL(final String value) { + try { + return new URI(value).toURL(); + } catch (URISyntaxException | MalformedURLException e) { + throw ConfigMessages.msg.uriSyntaxInvalid(e, value); + } + } +} +---- + +And registration in: +_META-INF/services/io.smallrye.config.ConfigSourceFactory_ + +[source,properties] +---- +org.acme.config.FileSystemConfigSourceFactory +---- + +The `FileSystemConfigSourceFactory` look ups the configuration value for `org.acme.config.file.locations`, and uses it +to set up an additional `ConfigSource`. diff --git a/content/docs/smallrye-config/2/config-sources/filesystem.adoc b/content/docs/smallrye-config/2/config-sources/filesystem.adoc new file mode 100644 index 000000000..66e16db05 --- /dev/null +++ b/content/docs/smallrye-config/2/config-sources/filesystem.adoc @@ -0,0 +1,51 @@ += FileSystem Config Source +:version-root: ../ +include::{version-root}_attributes.adoc[] + +This Config Source loads configuration values for each file found in a directory. Each file corresponds to a single +property, where the file name is the configuration property name and the file content the configuration value. + +For instance, if a directory structure looks like: + +.... +foo/ +|__num.max +|__num.size +.... + +The `FileSystem` Config Source will provide 2 properties: + +* num.max +* num.size + +[WARNING] +==== + +Nested directories are not supported. + +==== + +This Config Source can be used to read configuration from +https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap[Kubernetes ConfigMap]. Check the +https://github.com/smallrye/smallrye-config/blob/main/examples/configmap/README.adoc[Kubernetes ConfigMap ConfigSource Example]. + +The same mapping rules as defined for environment variables are applied, so the `FileSystem` Config Source will search +for a given property name `num.max`: + +* Exact match (`num.max`) +* Replace each character that is neither alphanumeric nor ++_++ with ++_++ (`num++_++max`) +* Replace each character that is neither alphanumeric nor ++_++ with ++_++; then convert the name to upper case (`NUM++_++MAX`) + +The following dependency is required in the classpath to use the `FileSystem` Config Source: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.config + smallrye-config-source-file-system + {version} + +---- + +The configuration property `smallrye.config.source.file.locations` sets the directory paths to look up the files. It +accepts multiple locations separated by a comma and each must represent a valid URI to a directory. diff --git a/content/docs/smallrye-config/2/config-sources/hocon.adoc b/content/docs/smallrye-config/2/config-sources/hocon.adoc new file mode 100644 index 000000000..23b29b437 --- /dev/null +++ b/content/docs/smallrye-config/2/config-sources/hocon.adoc @@ -0,0 +1,46 @@ += HOCON Config Source +:version-root: ../ +include::{version-root}_attributes.adoc[] + +This Config Source allows to use the https://github.com/lightbend/config/blob/main/HOCON.md[HOCON] file format to +load configuration values. The HOCON Config Source loads the +configuration from the file `META-INF/microprofile-config.conf`. It has a lower ordinal (`50`) than the +`microprofile-config.properties`. + +The following dependency is required in the classpath to use the HOCON Config Source: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.config + smallrye-config-source-hocon + {version} + +---- + +Expressions defined as `$++{++value}` (unquoted) are resolved internally by the HOCON Config Source as described in the +https://github.com/lightbend/config/blob/main/HOCON.md#substitutions[HOCON Substitutions] documentation. Quoted +Expressions defined as `"$++{++value}"` are resolved by xref:config/expressions.adoc[SmallRye Config Property Expressions]. + +Consider: + +*hocon.conf* + +[source,conf] +---- +{ + foo: "bar", + hocon: ${foo}, + config: "${foo}" +} +---- + +*application.properties* + +[source,properties] +---- +config_ordinal=1000 +foo=baz +---- + +The value of `hocon` is `bar` and the value of `config` is `baz` (if the properties source has a higher ordinal). diff --git a/content/docs/smallrye-config/2/config-sources/locations.adoc b/content/docs/smallrye-config/2/config-sources/locations.adoc new file mode 100644 index 000000000..1457440e3 --- /dev/null +++ b/content/docs/smallrye-config/2/config-sources/locations.adoc @@ -0,0 +1,77 @@ += Locations +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Additionally, to the default config locations specified by the +https://github.com/eclipse/microprofile-config/[MicroProfile Config] specification, SmallRye Config provides a way to +scan additional locations for configuration properties files. + +The `smallrye.config.locations` configuration property accepts multiple locations separated by a comma `,` and each +ust represent a valid `URI`. The supported `URI` schemes are: + +* file or directory (`file:`) +* classpath resource +* jar resource (`jar:`) +* http resource (`http:`) + +Each `URI` scheme loads all discovered resources in a `ConfigSource`. All loaded sources use the same ordinal of the +source that found the `smallrye.config.locations` configuration property. For instance, if `smallrye.config.locations` +is set as a system property, then all loaded sources have their ordinals set to `400` (system properties use `400` as +their ordinal). The ordinal may be overridden directly in the resource by setting the `config++_++ordinal` property. +Sources are sorted first by their ordinal, then by location order, and finally by loading order. + +If a profile is active, and the `URI` represents a single resource (for instance a file), then resources that match +the active profile are also loaded. The profile resource name must follow the pattern: `++{++location}-++{++profile}`. A +profile resource is only loaded if the unprofiled resource is also available in the same location. This is to keep a +consistent loading order and pair all the resources together. + +Profile resources are not taken into account if the location is a directory since there is no reliable way to discover +which file is the main resource. Properties that use the profile prefix syntax `%profile.` will work as expected. + +_All properties files from a directory_ + +[source,properties] +---- +# loads all files from a relative path +smallrye.config.locations=./src/main/resources/ + +# loads all files from an absolute path +smallrye.config.locations=/user/local/config +---- + +For relative paths, the JVM `user.dir` property defines the current directory. + +_A specific file_ + +[source,properties] +---- +smallrye.config.locations=./src/main/resources/additional.properties +---- + +If a profile `dev` is active, and an `additional-dev.properties` file exists, this will also be loaded. + +_All `additional.properties` files from the classpath_ + +[source,properties] +---- +smallrye.config.locations=additional.properties +---- + +If a profile `prod` is active, and an `additional-prod.properties` resources exists next to the `additional.properties` +resource, this will also be loaded. + +_The `resources.properties` file from a specific jar_ + +[source,properties] +---- +smallrye.config.locations=jar:file:///user/local/app/lib/resources-.jar!/resources.properties +---- + +If a profile `test` is active, and an `additional-test.properties` respource exists, this will also be loaded. + +_The `config.properties` file from a web server_ + +[source,properties] +---- +smallrye.config.locations=http://localhost:8080/config/config. +---- diff --git a/content/docs/smallrye-config/2/config-sources/yaml.adoc b/content/docs/smallrye-config/2/config-sources/yaml.adoc new file mode 100644 index 000000000..855f6b063 --- /dev/null +++ b/content/docs/smallrye-config/2/config-sources/yaml.adoc @@ -0,0 +1,18 @@ += YAML Config Source +:version-root: ../ +include::{version-root}_attributes.adoc[] + +This Config Source allows to use a `yaml` file to load configuration values. The YAML Config Source loads the +configuration from the file `META-INF/microprofile-config.yaml`. It has a higher ordinal (`110`) than the +`microprofile-config.properties`. + +The following dependency is required in the classpath to use the YAML Config Source: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.config + smallrye-config-source-yaml + {version} + +---- diff --git a/content/docs/smallrye-config/2/config-sources/zookeeper.adoc b/content/docs/smallrye-config/2/config-sources/zookeeper.adoc new file mode 100644 index 000000000..37f27785d --- /dev/null +++ b/content/docs/smallrye-config/2/config-sources/zookeeper.adoc @@ -0,0 +1,30 @@ += ZooKeeper Config Source +:version-root: ../ +include::{version-root}_attributes.adoc[] + +This Config Source allows using https://zookeeper.apache.org/index.html[Apache ZooKeeper] to load configuration +values. + +The following dependency is required in the classpath to use the ZooKeeper Config Source: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.config + smallrye-config-source-zookeeper + {version} + +---- + +It also requires to set up additional configuration properties to identify the ZooKeeper instance: + +[source,properties] +---- +io.smallrye.configsource.zookeeper.url=localhost:2181 +io.smallrye.configsource.zookeeper.applicationId=applicationId +---- + +The ZooKeeper Config Source will look for configuration values in a ZooKeeper instance running in the url set in +`io.smallrye.configsource.zookeeper.url` and in the znodes available in `/applicationId/`. + +This Config Source has an ordinal of `150`. diff --git a/content/docs/smallrye-config/2/config/config-value.adoc b/content/docs/smallrye-config/2/config/config-value.adoc new file mode 100644 index 000000000..63d58d715 --- /dev/null +++ b/content/docs/smallrye-config/2/config/config-value.adoc @@ -0,0 +1,8 @@ += Config Value +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The `io.smallrye.config.ConfigValue` is a metadata object that holds additional information after the lookup of a +configuration property. It is able to hold information of configuration property name, value, profile, the +`ConfigSource` from where the configuration was loaded, the ordinal of the `ConfigSource` and a line number from where +the configuration was loaded if exists. diff --git a/content/docs/smallrye-config/2/config/configuration.adoc b/content/docs/smallrye-config/2/config/configuration.adoc new file mode 100644 index 000000000..79e5d381b --- /dev/null +++ b/content/docs/smallrye-config/2/config/configuration.adoc @@ -0,0 +1,41 @@ += Configuration Reference +:version-root: ../ +include::{version-root}_attributes.adoc[] + +== `smallrye.config.profile` + +The main xref:config/profiles.adoc[Profile] to activate. + +____ +* type: String++[]++ +* default: +____ + +== `smallrye.config.profile.parent` + +The parent xref:config/profiles.adoc#parent-profile[Profile] to activate. + +____ +* type: String++[]++ +* default: +____ + +== `smallrye.config.locations` + +xref:config-sources/locations.adoc[Additional config locations] to be loaded with the Config. The configuration supports +multiple locations separated by a comma and each must represent a valid `java.net.URI`. + +____ +* type: URI++[]++ +* default: +____ + +== `smallrye.config.mapping.validate-unknown` + +xref:config/mappings.adoc#retrieval[Validates] that a `@ConfigMapping` maps every available configuration name contained in the +mapping prefix. + +____ +* type: boolean +* default: false +____ diff --git a/content/docs/smallrye-config/2/config/environment-variables.adoc b/content/docs/smallrye-config/2/config/environment-variables.adoc new file mode 100644 index 000000000..af2b6b98f --- /dev/null +++ b/content/docs/smallrye-config/2/config/environment-variables.adoc @@ -0,0 +1,76 @@ += Environment Variables +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Environment Variable names follow the conversion rules specified by https://github.com/eclipse/microprofile-config/blob/master/spec/src/main/asciidoc/configsources.asciidoc#default-configsources[MicroProfile Config]. + +SmallRye Config specifies additional conversion rules: + +* A property with double quotes `foo."bar".baz`, replace each character that is neither alphanumeric nor `++_++` +with `++_++`: `FOO++__++BAR++__++BAZ` +* A property with dashes `foo.bar-baz`, replace each character that is neither alphanumeric nor `++_++` +with `++_++`: `FOO++_++BAR++_++BAZ` +* An indexed property `foo.bar++[++0++]++` or `foo.bar++[++0++]++.baz`, replace each character that is neither alphanumeric nor `++_++` +with `++_++`: `FOO++_++BAR++_++0++_++` or `FOO++_++BAR++_++0++__++BAZ`. + +[CAUTION] +==== + +Environment Variables format cannot represent the entire spectrum of common property names. + +==== + +The lookup of configuration values from Environment Variables will always use the dotted format name. For +instance, the lookup of the Environment Variable `FOO++_++BAR` value, requires the property name `foo.bar`: + +[source,java] +---- +ConfigProvider.getConfig().getValue("foo.bar", String.class); +---- + +When SmallRyeConfig performs the lookup on the Environment Variables Config Source, it applies the conversion rules to +find the matching property name and retrieve the value. + +In some situations, looking up the exact property name is impossible. For instance, when SmallRye Config has to look up +a configuration that is part of a `Map`, and the property name contains a dynamic segment (a `Map` key). In this case, +SmallRye Config relies upon each source's list of property names. These must be converted back to their most likely +dotted format for Environment Variables. + +By default, the underscore `++_++` of an Environment Variable name always maps to a dot `.`. If the property name +contains a dash or some other special character, that property name can be specified in another Config +Source, with the expected dotted format. It will provide additional information to SmallRye Config to perform a +two-way conversion and match the property names. + +For instance: + +*Config A* + +[source,console] +---- +FOO_BAR_BAZ=VALUE +---- + +Will map to `foo.bar.baz` and value `value`. + +*Config B* + +[source,console] +---- +FOO_BAR_BAZ=VALUE +---- + +[source,properties] +---- +foo.bar-baz=default +---- + +Will map to `foo.bar-baz` and value `value`. + +[NOTE] +==== + +The property name in dotted format needs to exist somewhere to provide this additional information. It can be set in a +low ordinal source, even without value. The Environment Variables source will override the value and map the correct +configuration name. + +==== diff --git a/content/docs/smallrye-config/2/config/expressions.adoc b/content/docs/smallrye-config/2/config/expressions.adoc new file mode 100644 index 000000000..99187c64a --- /dev/null +++ b/content/docs/smallrye-config/2/config/expressions.adoc @@ -0,0 +1,34 @@ += Property Expressions +:version-root: ../ +include::{version-root}_attributes.adoc[] + +SmallRye Config provides property expressions expansion on configuration values. An expression string is a mix of plain +strings and expression segments, which are wrapped by the sequence $++{++ ... }. + +For instance, the following configuration properties file: + +[source,properties] +---- +remote.host=smallrye.io +callable.url=https://${remote.host}/ +---- + +The resolved value of the `callable.url` property is `https://smallrye.io/`. + +Additionally, the Expression Expansion engine supports the following segments: + +* `$++{++expression:value}` - Provides a default value after the `:` if the expansion doesn’t find a value. +* `$++{++my.prop$++{++compose}}` - Composed expressions. Inner expressions are resolved first. +* `$++{++my.prop}$++{++my.prop}` - Multiple expressions. + +If an expression cannot be expanded and no default is supplied a `NoSuchElementException` is thrown. + +Expression expansion may be selectively disabled with `io.smallrye.config.Expressions`: + +[source,java] +---- +Config config = ConfigProvider.getConfig(); + +String url = Expressions.withoutExpansion(() -> + config.getValue("callable.url", String.class)); +---- diff --git a/content/docs/smallrye-config/2/config/getting-started.adoc b/content/docs/smallrye-config/2/config/getting-started.adoc new file mode 100644 index 000000000..3e3bbd124 --- /dev/null +++ b/content/docs/smallrye-config/2/config/getting-started.adoc @@ -0,0 +1,172 @@ += Getting Started +:version-root: ../ +include::{version-root}_attributes.adoc[] + +== Config Sources + +By default, SmallRye Config reads configuration properties from multiple configuration sources (by descending ordinal): + +[arabic] +. (`400`) System properties +. (`300`) Environment variables +. (`100`) MicroProfile Config configuration file `META-INF/microprofile-config.properties` in the classpath + +A configuration source is handled by a `ConfigSource`. A `ConfigSource` provides configuration values from a specific +place. + +The final configuration is the aggregation of the properties defined by all these sources. A configuration property +lookup starts by the highest ordinal configuration source available and works it way down to other sources until a +match is found. This means that any configuration property may override a value just by setting a different value in a +higher ordinal config source. For example, a property configured using an Environment Variable overrides the value +provided using the `microprofile-config.properties` file. + +=== System Properties + +System properties can be handed to the application through the `-D` flag during startup. For instance, +`java -Dmy.prop -jar my.jar`. + +=== Environment Variables + +Environment variables are set directly in the host operating system. Environment variables names follow the conversion +rules detailed by xref:config/environment-variables.adoc[Environment Variables]. + +=== MicroProfile Config configuration file + +The MicroProfile Config configuration file `META-INF/microprofile-config.properties` in the classpath. It follows the +standard convention for `properties` files. + +[source,properties] +---- +greeting.message=hello +goodbye.message=bye +---- + +=== Additional Config Sources + +SmallRye Config provides additional extensions which cover other configuration formats and stores: + +* xref:config-sources/yaml.adoc[YAML] +* xref:config-sources/filesystem.adoc[File System] +* xref:config-sources/zookeeper.adoc[ZooKeeper] +* xref:config-sources/hocon.adoc[HOCON] + +It is also possible to create a xref:config-sources/custom.adoc[Custom ConfigSource]. + +== Retrieving the Configuration + +=== Programmatically + +The `org.eclipse.microprofile.config.ConfigProvider.getConfig()` API allows to access the +`org.eclipse.microprofile.config.Config` API programmatically. + +[source,java] +---- +Config config = ConfigProvider.getConfig(); + +String message = config.getValue("greeting.message", String.class); +---- + +The `Config` instance will be created and registered to the current context class loader if no such configuration is +already created and registered. This means that subsequent calls to `ConfigProvider.getConfig()` will return the same +`Config` instance if the context class loader is the same. + +To obtain a detached instanced, use the `io.smallrye.config.SmallRyeConfigBuilder`: + +[source,java] +---- +SmallRyeConfig config = new SmallRyeConfigBuilder() + .addDefaultInterceptors() + .addDefaultSources() + .build(); + +String message = config.getValue("greeting.message", String.class); +---- + +=== With CDI + +In a CDI environment, configuration can be injected in CDI aware beans with `@Inject` and +the `org.eclipse.microprofile.config.inject.ConfigProperty` qualifier. + +[source,java] +---- +@Inject +@ConfigProperty(name = "greeting.message") +String message; + +@Inject +@ConfigProperty(name = "greeting.suffix", defaultValue="!") +String suffix; + +@Inject +@ConfigProperty(name = "greeting.name") +Optional name; + +@Inject +SmallRyeConfig config; +---- + +* If a value if not provided for this `greeting.message`, the application startup fails with a +`javax.enterprise.inject.spi.DeploymentException: No config value of type ++[++class java.lang.String++]++ exists for: greeting.message`. +* The default value `!` is injected if the configuration does not provide a value for `greeting.suffix`. +* The property `greeting.name` is optional - an empty Optional is injected if the configuration does not provide a +value for it. + +== Override Config + +It is possible to override `Config` default initialization `ConfigProvider.getConfig()`, by extending +`io.smallrye.config.SmallRyeConfigFactory` and registering the implementation with the `ServiceLoader` mechanism. + +== Config vs SmallRyeConfig + +The `io.smallrye.config.SmallRyeConfig` is an implementation of `org.eclipse.microprofile.config.Config` and provides +additional APIs and helper methods not available in `org.eclipse.microprofile.config.Config`. To obtain an instance of +`io.smallrye.config.SmallRyeConfig`, the original `org.eclipse.microprofile.config.Config` can be unwrapped: + +[source,java] +---- +Config config = ConfigProvider.getConfig(); +SmallRyeConfig smallRyeConfig = config.unwrap(SmallRyeConfig.class); +---- + +Or if using the builder it can be obtained directly: + +[source,java] +---- +SmallRyeConfig config = new SmallRyeConfigBuilder().build(); +---- + +A few notable APIs provided by `io.smallrye.config.SmallRyeConfig` allow to: + +* Retrive multiple values into a specified `Collection` +* Retrive xref:config/indexed-properties.adoc[Indexed Values] +* Retrive xref:config/mappings.adoc[Config Mappings] instances +* Retrieve the raw value of a configuration +* Check if a property is present +* Retrieve a `Converter` +* Convert values + +== Converters + +The `ConfigSource` retrieves a configuration value as a `String`. Other data types require a conversion using the +`org.eclipse.microprofile.config.spi.Converter` API. + +Most of the common `Converter` types are provided by default: + +* `boolean` and `java.lang.Boolean`; the values "true", "1", "YES", "Y" "ON" represent `true`. Any other value will be +interpreted as `false` +* `byte` and `java.lang.Byte` +* `short` and `java.lang.Short` +* `int`, `java.lang.Integer`, and `java.util.OptionalInt` +* `long`, `java.lang.Long`, and `java.util.OptionalLong` +* `float` and `java.lang.Float`; a dot '.' is used to separate the fractional digits +* `double`, `java.lang.Double`, and `java.util.OptionalDouble`; a dot '.' is used to separate the fractional digits +* `char` and `java.lang.Character` +* `java.lang.Class` based on the result of `Class.forName` +* `java.net.InetAddress` +* `java.util.UUID` +* `java.util.Currency` +* `java.util.regex.Pattern` +* Any class with declared static methods `of`, `valueOf` or `parse` that take a `String` or a `CharSequence` +* Any class with declared constructors that takes a `String` or a `CharSequence` + +All default converters have a priority of `1`. diff --git a/content/docs/smallrye-config/2/config/indexed-properties.adoc b/content/docs/smallrye-config/2/config/indexed-properties.adoc new file mode 100644 index 000000000..7a1e1ab99 --- /dev/null +++ b/content/docs/smallrye-config/2/config/indexed-properties.adoc @@ -0,0 +1,31 @@ += Indexed Properties +:version-root: ../ +include::{version-root}_attributes.adoc[] + +In https://github.com/eclipse/microprofile-config/[MicroProfile Config], a config value with unescaped commas may be +converted to `Collection`. It works for simple cases, but it becomes cumbersome and limited for more advanced use cases. + +Indexed Properties provide a way to use indexes in config property names to map specific elements in a `Collection` +type. Since the indexed element is part of the property name, it can also map complex object types. Consider: + +[source,properties] +---- +# MicroProfile Config - Collection Values +my.collection=dog,cat,turtle + +# SmallRye Config - Indexed Property +my.indexed.collection[0]=dog +my.indexed.collection[1]=cat +my.indexed.collection[2]=turtle +---- + +The indexed property syntax uses the property name and square brackets with an index in between. + +A call to `Config#getValues("my.collection", String.class)`, will automatically create and convert a `List++<++String++>++` +that contains the values `dog`, `cat` and `turtle`. A call to `Config#getValues("my.indexed.collection", String.class)` +returns the exact same result. For compatibility reasons, if SmallRye Config finds the same property name in their +indexed and unindexed format, the unindexed value has priority. + +The indexed property is sorted by its index before being added to the target `Collection`. Any gaps in the indexes do +not resolve to the target `Collection`, which means that the `Collection` result will store all values without empty +elements. diff --git a/content/docs/smallrye-config/2/config/map-support.adoc b/content/docs/smallrye-config/2/config/map-support.adoc new file mode 100644 index 000000000..9dcea9a91 --- /dev/null +++ b/content/docs/smallrye-config/2/config/map-support.adoc @@ -0,0 +1,57 @@ += Map Support +:version-root: ../ +include::{version-root}_attributes.adoc[] + +SmallRye Config allows injecting multiple configuration parameters as a `Map`. The configuration value syntax is +represented by `++<++key1++>++=++<++value1++>++;++<++key2++>++=++<++value2++>++` Consider: + +[source,properties] +---- +server.reasons=200=OK;201=Created +---- + +The previous configuration could be injected directly in a CDI Bean: + +With `@ConfigProperty` + +[source,java] +---- +@ApplicationScoped +public class ConfigBean { + @Inject + @ConfigProperty(name = "server.reasons", defaultValue = "200=OK;201=Created") + Map reasons; +} +---- + +With `@ConfigProperties` + +[source,java] +---- +@ConfigProperties(prefix = "server") +public class Config { + Map reasons; +} +---- + +[NOTE] +==== + +Only the direct sub properties will be converted into a `Map` and +injected into the target bean, the rest will be ignored. In other words, +in the previous example, a property whose name is `reasons.200.a` would +be ignored as not considered as a direct sub property. + +==== + +[NOTE] +==== + +The property will be considered as missing if no direct sub properties +could be found. + +==== + +It is also possible to retrieve the `Map` programmatically by calling the methods +`SmallRyeConfig#getValues("server.reasons", Integer.class, String.class)` or +`SmallRyeConfig#getOptionalValues("server.reasons", Integer.class, String.class)`. diff --git a/content/docs/smallrye-config/2/config/mappings.adoc b/content/docs/smallrye-config/2/config/mappings.adoc new file mode 100644 index 000000000..191c17231 --- /dev/null +++ b/content/docs/smallrye-config/2/config/mappings.adoc @@ -0,0 +1,473 @@ += Mappings +:version-root: ../ +include::{version-root}_attributes.adoc[] + +With SmallRye Config Mappings, it is possible to group multiple configuration properties in a single interface that +share the same prefix (or namespace). It supports the following set of features: + +* Automatic conversion of the configuration type, including `List`, `Set`, `Map`, `Optional` and primitive types. +* Nested Config Mapping groups. +* Configuration Properties Naming Strategies +* Integration with Bean Validation + +A Config Mapping requires an interface with minimal metadata configuration annotated with +`io.smallrye.config.ConfigMapping`: + +[source,java] +---- +@ConfigMapping(prefix = "server") +interface Server { + String host(); + + int port(); +} +---- + +The `Server` interface is able to map configurations with the name `server.host` into the `Server#host()` method and +`server.port` into `Server#port()` method. The configuration property name to lookup is built from the prefix, and the +method name with `.` (dot) as the separator. + +[WARNING] +==== + +If a mapping fails to match a configuration property a `NoSuchElementException` is thrown, unless the mapped +element is an `Optional`. + +==== + +== Registration + +Registration of Config Mappings is automatic in CDI aware environments with the `@ConfigMapping` annotation. + +In non-CDI environments, the Config Mapping can be registered via `SmallRyeConfigBuilder#withMapping`. In this case, +the `@ConfigMapping` is completely optional (but recommendeded to set the prefix). + +[source,java] +---- +SmallRyeConfig config = new SmallRyeConfigBuilder() + .withMapping(Server.class) + .build(); +---- + +== Retrieval + +A config mapping interface can be injected into any CDI aware bean: + +[source,java] +---- +@ApplicationScoped +class BusinessBean { + @Inject + Server server; + + public void businessMethod() { + String host = server.host(); + } +} +---- + +In non-CDI environments, use the API `io.smallrye.config.SmallRyeConfig#getConfigMapping` to retrieve the config +mapping instance: + +[source,java] +---- +SmallRyeConfig config = ConfigProvider.getConfig().unwrap(SmallRyeConfig.class); +Server server = config.getConfigMapping(Server.class); +---- + +[NOTE] +==== + +Config Mapping instances are cached. They are populated when the `Config` instance is initialized. + +==== + +For a Config Mapping to be valid, it needs to match every configuration property name contained in the `Config` under +the specified prefix set in `@ConfigMapping`. This prevents unknown configuration properties in the `Config`. This +behaviour can be disabled with the configuration `smallrye.config.mapping.validate-unknown=false`. + +== Nested Groups + +A nested mapping provides a way to map sub-groups of configuration properties. + +[source,java] +---- +@ConfigMapping(prefix = "server") +public interface Server { + String host(); + + int port(); + + Log log(); + + interface Log { + boolean enabled(); + + String suffix(); + + boolean rotate(); + } +} +---- + +[source,properties] +---- +server.host=localhost +server.port=8080 +server.log.enabled=true +server.log.suffix=.log +server.log.rotate=false +---- + +The method name of a mapping group acts as a sub-prefix in the property name. In this case the matching property to +`Server.Log#enabled` is `server.log.enabled`. + +== Overriding property names + +=== `@WithName` + +If a method name and a property name do not match, the `io.smallrye.config.WithName` annotation can +override the method name mapping and use the name supplied in the annotation. + +[source,java] +---- +@ConfigMapping(prefix = "server") +interface Server { + @WithName("name") + String host(); + + int port(); +} +---- + +[source,properties] +---- +server.name=localhost +server.port=8080 +---- + +=== `@WithParentName` + +The `io.smallrye.config.WithParent` annotation allows configurations mappings to inherit its parent container name, +simplifying the configuration property name required to match the mapping. + +[source,java] +---- +@ConfigMapping(prefix = "server") +interface Server { + @WithParentName + ServerHostAndPort hostAndPort(); + + @WithParentName + ServerInfo info(); +} + +interface ServerHostAndPort { + String host(); + + int port(); +} + +interface ServerInfo { + String name(); +} +---- + +[source,properties] +---- +server.host=localhost +server.port=8080 +server.name=konoha +---- + +Without the `@WithParentName` the method `ServerInfo#name` maps the configuration property `server.info.name`. With +`@WithParentName`, the `Server#info` mapping will inherit the parent name from `Server` and `ServerInfo#name` maps to +the property `server.name` instead. + +=== NamingStrategy + +Method names in camelCase map to kebab-case configuration property names by default. + +[source,java] +---- +@ConfigMapping(prefix = "server") +interface Server { + String theHost(); + + int thePort(); +} +---- + +[source,properties] +---- +server.the-host=localhost +server.the-port=8080 +---- + +The mapping strategy can be adjusted by setting `namingStrategy` value in the `@ConfigMapping` annotation. + +[source,java] +---- +@ConfigMapping(prefix = "server", namingStrategy = ConfigMapping.NamingStrategy.VERBATIM) +public interface ServerVerbatimNamingStrategy { + String theHost(); + + int thePort(); +} +---- + +[source,properties] +---- +server.theHost=localhost +server.thePort=8080 +---- + +The `@ConfigMapping` annotation support the following naming stategies: + +* KEBAB++_++CASE - The method name is derived by replacing case changes with a dash to map the configuration property. +* VERBATIM - The method name is used as is to map the configuration property. +* SNAKE++_++CASE - The method name is derived by replacing case changes with an underscore to map the configuration property. + +== Conversion + +A config mapping interface support automatic conversions of all types available for conversion in `Config`. + +[source,java] +---- +@ConfigMapping +public interface SomeTypes { + @WithName("int") + int intPrimitive(); + + @WithName("int") + Integer intWrapper(); + + @WithName("long") + long longPrimitive(); + + @WithName("long") + Long longWrapper(); + + @WithName("float") + float floatPrimitive(); + + @WithName("float") + Float floatWrapper(); + + @WithName("double") + double doublePrimitive(); + + @WithName("double") + Double doubleWrapper(); + + @WithName("char") + char charPrimitive(); + + @WithName("char") + Character charWrapper(); + + @WithName("boolean") + boolean booleanPrimitive(); + + @WithName("boolean") + Boolean booleanWrapper(); +} +---- + +[source,properties] +---- +int=9 +long=9999999999 +float=99.9 +double=99.99 +char=c +boolean=true +---- + +This is also valid for `Optional` and friends. + +[source,java] +---- +@ConfigMapping +public interface Optionals { + Optional server(); + + Optional optional(); + + @WithName("optional.int") + OptionalInt optionalInt(); + + interface Server { + String host(); + + int port(); + } +} +---- + +In this case, the mapping won’t fail if the configuraton properties values are missing. + +=== `@WithConverter` + +The `io.smallrye.config.WithConverter` annotation provides a way to set a specific `Converter` in a mapping. + +[source,java] +---- +@ConfigMapping +public interface Converters { + @WithConverter(FooBarConverter.class) + String foo(); +} + +public static class FooBarConverter implements Converter { + @Override + public String convert(final String value) { + return "bar"; + } +} +---- + +[source,properties] +---- +foo=foo +---- + +A call to `Converters.foo()` results in the value `bar`. + +== Collections + +A config mapping is also able to map the collections types `List` and `Set`: + +[source,java] +---- +@ConfigMapping(prefix = "server") +public interface ServerCollections { + Set environments(); + + interface Environment { + String name(); + + List apps(); + + interface App { + String name(); + + List services(); + + Optional> databases(); + } + } +} +---- + +[source,properties] +---- +server.environments[0].name=dev +server.environments[0].apps[0].name=rest +server.environments[0].apps[0].services=bookstore,registration +server.environments[0].apps[0].databases=pg,h2 +server.environments[0].apps[1].name=batch +server.environments[0].apps[1].services=stock,warehouse +---- + +The `List` and `Set` mappings can use xref:config/indexed-properties.adoc[Indexed Properties] to map configuration values in +mapping groups. + +== Maps + +A config mapping is also able to map a `Map`: + +[source,java] +---- +@ConfigMapping(prefix = "server") +public interface Server { + String host(); + + int port(); + + Map form(); + + Map> aliases(); + + interface Alias { + String name(); + } +} +---- + +[source,properties] +---- +server.host=localhost +server.port=8080 +server.form.login-page=login.html +server.form.error-page=error.html +server.form.landing-page=index.html +server.aliases.localhost[0].name=prod +server.aliases.localhost[1].name=127.0.0.1 +---- + +The configuration property name needs to specify an additional segment to act as the map key. The `server.form` matches +the `Server#form` `Map` and the segments `login-page`, `error-page` and `landing-page` represent the `Map` +keys. + +For collection types, the key requires the indexed format. The configuration name `server.aliases.localhost++[++0++]++.name` +maps to the `Map++<++String, List++<++Alias++>>++ aliases()` member, where `localhost` is the `Map` key, `++[++0++]++` is the index of the +`List++<++Alias++>++` where the `Alias` element will be stored, containing the name `prod`. + +== Defaults + +The `io.smallrye.config.WithDefault` annotation allows to set a default property value into a mapping (and prevent +errors if the configuration value is not available in any `ConfigSource`). + +[source,java] +---- +public interface Defaults { + @WithDefault("foo") + String foo(); + + @WithDefault("bar") + String bar(); +} +---- + +No configuration properties are required. The `Defaults#foo()` will return the value `foo` and `Defaults#bar()` will +return the value `bar`. + +== ToString + +If the config mapping contains a `toString` method declaration, the config mapping instance will include a proper +implementation of the `toString` method. + +[CAUTION] +==== + +Do not include a `toString` declaration in a config mapping with sensitive information + +==== + +== Validation + +A config mapping may combine annotations from https://beanvalidation.org/[Bean Validation] to validate configuration +properties values. + +[source,java] +---- +@ConfigMapping(prefix = "server") +interface Server { + @Size(min = 2, max = 20) + String host(); + + @Max(10000) + int port(); +} +---- + +The application startup fails with a `io.smallrye.config.ConfigValidationException` if the configuration properties +values do not follow the contraints defined in `Server`. + +[NOTE] +==== + +For validation to work, the `smallrye-config-validator` dependency is required in the classpath. + +==== diff --git a/content/docs/smallrye-config/2/config/profiles.adoc b/content/docs/smallrye-config/2/config/profiles.adoc new file mode 100644 index 000000000..1fee0e314 --- /dev/null +++ b/content/docs/smallrye-config/2/config/profiles.adoc @@ -0,0 +1,222 @@ += Profiles +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Applications often require different configurations depending on the target environment. For example, the local +development environment may be different from the production environment. Profiles allow for multiple configurations +in the same file or separate files and select between them via a profile name. + +== Profile aware properties + +To be able to set properties with the same name, each property needs to be prefixed with a percentage sign `%` followed +by the profile name and a dot `.` in the syntax `%++{++profile-name}.config.name`: + +[NOTE] +==== + +[source,properties] +---- +http.port=8080 +%dev.http.port=8181 +---- + +==== + +To activate the profile `dev`, the configuration `smallrye.config.profile=dev` has to be set into any valid +`ConfigSource`. + +Any lookup to the `http.port` property name will first search by the active profile name `%dev.http.port` and then +fallback to `http.port` if no value is present. In this case a lookup to the property `http.port` with the `dev` profile +active, yields the value `8181`. + +== Profile aware files + +Properties for a specific profile may reside in a `microprofile-config-++{++profile}.properties` named file. The previous +example can be expressed as: + +[NOTE] +==== + +.microprofile-config.properties +[source,properties] +---- +http.port=8080 +---- + +.microprofile-config-dev.properties +[source,properties] +---- +http.port=8181 +---- + +==== + +In this style, the property names in the profile aware file do not need to be prefixed with the profile name. + +[NOTE] +==== + +Properties in the profile aware file have priority over profile aware properties defined in the main file. + +==== + +[WARNING] +==== + +Do not use Profile aware files to set `smallrye.config.profile`. This will not work because the +the profile is required in advance to load the profile aware files. + +==== + +== Priority + +Profile lookups are only valid if the `ConfigSource` has a higher ordinal than a lookup to the regular configuration +name. Consider: + +[NOTE] +==== + +.main.properties +[source,properties] +---- +config_ordinal=1000 +http.port=8080 +---- + +.profile.properties +[source,properties] +---- +config_ordinal=100 +%dev.http.port=8181 +---- + +==== + +Even with the profile `dev` active, the lookup value for `my.prop` is `1234`. This prevents lower ordinal sources to +set a profile property value that cannot be overridden unless the profile property is also overridden. + +== Multiple Profiles + +Multiple Profiles may be active at the same time. The configuration `smallrye.config.profile` accepts a comma-separated +list of profile names: `smallrye.config.profile=common,dev`. Both `common` and `dev` are separate profiles. + +When multiple profiles are active, the rules for profile configuration are the same. If two profiles define the same +configuration, then the last listed profile has priority. Consider: + +[NOTE] +==== + +[source,properties] +---- +smallrye.config.profile=common,dev + +my.prop=1234 +%common.my.prop=1234 +%dev.my.prop=5678 + +%common.commom.prop=common +%dev.dev.prop=dev +%test.test.prop=test +---- + +==== + +Then + +* `common.prop` value is `common` +* `dev.prop` value is `dev` +* `my.prop` value is `5678` +* `test.prop` does not have a value + +It is also possible to define multiple profile properties, with a comma-separated list of profile names: + +[NOTE] +==== + +[source,properties] +---- +%prod,dev.my.prop=1234 +---- + +==== + +The property name `common.prop` is active in both `dev` and `prod` profile. If the same property name exists in +multiple profile properties then, the property name with the most specific profile wins: + +[NOTE] +==== + +[source,properties] +---- +smallrye.config.profile=dev + +%prod,dev.my.prop=1234 + +%dev.my.prop=5678 +---- + +==== + +Then `my.prop` value is `5678`. + +== Parent Profile + +A Parent Profile adds multiple levels of hierarchy to the current profile. The configuration +`smallrye.config.profile.parent` also acccepts a comma-separated list of profile names. + +When the Parent Profile is active, if a property cannot be found in the current active Profile, the config lookup +fallbacks to the Parent Profile. Consider: + +[NOTE] +==== + +[source,properties] +---- +smallrye.config.profile=dev +smallrye.config.profile.parent=common + +my.prop=1234 +%common.my.prop=0 +%dev.my.prop=5678 + +%common.commom.prop=common +%dev.dev.prop=dev +%test.test.prop=test +---- + +==== + +Then + +* `common.prop` value is `common` +* `dev.prop` value is `dev` +* `my.prop` value is `0` +* `test.prop` does not have a value + +[WARNING] +==== + +Do not use Profile aware files to set smallrye.config.profile.parent++`++. This will not work because the +the profile is required in advance to load the profile aware files. + +==== + +=== Multi-level Hierarchy + +The Parent Profile also supports multiple levels of hierarchies: + +[NOTE] +==== + +[source,properties] +---- +smallrye.config.profile=child +%child.smallrye.config.profile.parent=parent +%parent.smallrye.config.profile.parent=grandparent +%grandparent.smallrye.config.profile.parent=greatgrandparent +%greatgrandparent.smallrye.config.profile.parent=end +---- + +==== + +Will load the following profiles in order: `child`, `parent`, `grandparent`, `greatgrandparent`, `end` diff --git a/content/docs/smallrye-config/2/config/secret-keys.adoc b/content/docs/smallrye-config/2/config/secret-keys.adoc new file mode 100644 index 000000000..6b96ba7b6 --- /dev/null +++ b/content/docs/smallrye-config/2/config/secret-keys.adoc @@ -0,0 +1,39 @@ += Secret Keys +:version-root: ../ +include::{version-root}_attributes.adoc[] + +When configuration properties contain passwords or other kinds of secrets, Smallrye Config can hide them to prevent +accidental exposure of such values. + +*This is no way a replacement for securing secrets.* Proper security mechanisms must still be used to secure +secrets. However, there is still the basic problem that passwords and secrets are generally encoded simply as strings. +Secret Keys provides a way to "lock" the configuration so that secrets do not appear unless explicitly enabled. + +To mark specific keys as secrets, register an instance of `io.smallrye.config.SecretKeysConfigSourceInterceptor` by using your interceptor factory as follows: + +[source,java] +---- +public class SecretKeysConfigInterceptorFactory implements ConfigSourceInterceptorFactory { + @Override + public ConfigSourceInterceptor getInterceptor(ConfigSourceInterceptorContext context) { + return new SecretKeysConfigSourceInterceptor(Set.of("secret")); + } +} +---- + +Register this factory so that it can be found at runtime by creating a `META-INF/services/io.smallrye.config.ConfigSourceInterceptorFactory` file that contains the fully qualified name of this factory class. + +From this point forward, every lookup to the configuration name `secret` will throw a `SecurityException`. + +Access the Secret Keys using the APIs `io.smallrye.config.SecretKeys#doUnlocked(java.lang.Runnable)` +and `io.smallrye.config.SecretKeys#doUnlocked(java.util.function.Supplier++<++T++>++)`. + +[source,java] +---- +String secretValue = SecretKeys.doUnlocked(() -> { + config.getValue("secret", String.class); +}); +---- + +Secret Keys are only unlocked in the context of `doUnlocked`. Once the execution completes, the secrets become locked +again. diff --git a/content/docs/smallrye-config/2/converters/custom.adoc b/content/docs/smallrye-config/2/converters/custom.adoc new file mode 100644 index 000000000..eb7d6d3c4 --- /dev/null +++ b/content/docs/smallrye-config/2/converters/custom.adoc @@ -0,0 +1,102 @@ += Custom `Converter` +:version-root: ../ +include::{version-root}_attributes.adoc[] + +It is possible to create a custom `Converter` type as specified in +https://github.com/eclipse/microprofile-config/[MicroProfile Config]. + +A custom `Converter` requires an implementation of `org.eclipse.microprofile.config.spi.Converter`. Each implementation +requires registration via the `ServiceLoader` mechanism in the +`META-INF/services/org.eclipse.microprofile.config.spi.Converter` file. Consider: + +[source,java] +---- +package org.acme.config; + +public class CustomValue { + + private final int number; + + public CustomValue(int number) { + this.number = number; + } + + public int getNumber() { + return number; + } +} +---- + +The corresponding converter can look like: + +[source,java] +---- +package org.acme.config; + +import org.eclipse.microprofile.config.spi.Converter; + +public class CustomValueConverter implements Converter { + + @Override + public CustomValue convert(String value) { + return new CustomValue(Integer.parseInt(value)); + } +} +---- + +And registration in: + +++_++META-INF/services/org.eclipse.microprofile.config.spi.Converter + +[source,properties] +---- +org.acme.config.CustomValue +---- + +[WARNING] +==== + +The custom `Converter` class must be `public`, must have a `public` constructor with no arguments, and must not be +abstract. + +==== + +The `CustomValueConverter` converts the configuration value to the `CustomValue` type automatically. + +[source,java] +---- +Config config = ConfigProvider.getConfig(); +CustomValue value = config.getValue("custom.value", CustomValue.class); +---- + +The `javax.annotation.Priority` annotation overrides the `Converter` priority and change converters precedence to fine +tune the execution order. By default, if no `@Priority` is specified by the `Converter`, the converter is registered +with a priority of `100`. Consider: + +[source,java] +---- +package org.acme.config; + +import javax.annotation.Priority; +import org.eclipse.microprofile.config.spi.Converter; + +@Priority(150) +public class SecretConverter implements Converter { + + @Override + public CustomValue convert(String value) { + final int secretNumber; + if (value.startsFrom("OBF:")) { + secretNumber = Integer.parseInt(SecretDecoder.decode(value)); + } else { + secretNumber = Integer.parseInt(value); + } + + return new CustomValue(secretNumber); + } +} +---- + +Two `Converter`s, (`CustomValueConverter` and `SecretConverter`) can convert the same type `CustomValue`. Since +`SecretConverter` has a priority of `150`, it will be used instead of a `CustomValueConverter` which has a default +priority of `100` (no annotation). diff --git a/content/docs/smallrye-config/2/converters/json.adoc b/content/docs/smallrye-config/2/converters/json.adoc new file mode 100644 index 000000000..1bab43e72 --- /dev/null +++ b/content/docs/smallrye-config/2/converters/json.adoc @@ -0,0 +1,37 @@ += JSON Converter +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The JSON Converter allows to convert a configuration value into a `JsonObject` or `JsonArray`. + +The following dependency is required in the classpath to use the JSON Converter: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.config + smallrye-config-converter-json + {version} + +---- + +`JsonObject` or `JsonArray` are automatically converted from their `String` value. + +[source,java] +---- +@Inject +@ConfigProperty(name = "someJsonArray") +private JsonArray someValue; + +@Inject +@ConfigProperty(name = "someJsonObject") +private JsonObject someValue; +---- + +[source,properties] +---- +someJsonArray=["value1","value2","value3"] +someJsonObject={"foo": "bar", "count":100} +---- + +The value of the configuration properties must be valid JSON, or a `JsonParsingException` is thrown. diff --git a/content/docs/smallrye-config/2/extensions/config-events.adoc b/content/docs/smallrye-config/2/extensions/config-events.adoc new file mode 100644 index 000000000..cd43004ac --- /dev/null +++ b/content/docs/smallrye-config/2/extensions/config-events.adoc @@ -0,0 +1,158 @@ += Config Events +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The Config Events extension allows you to fire change events on Config Sources. + +== Usage + +To use the Config Events, add the following to your Maven `pom.xml`: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.config + smallrye-config-events + {version} + +---- + +== Events + +The CDI Event is a `ChangeEvent` and contains the following fields: + +* String key +* Optional++<++String++>++ oldValue +* String newValue +* Type type +* String fromSource + +The `ChangeEvent` can be of any of the following types: + +* NEW - When you create a new key and value (i.e. the key does not exist anywhere in any config source) +* UPDATE - When you update a value of an existing key (i.e. the key and value exist somewhere in a config source) +* REMOVE - When you remove the value from the source (and that changed the overall config) + +=== Observing Events + +You can listen to all or some of these events, filtering by `type` and/or `key` and/or `source`, example: + +[source,java] +---- +// Getting all config event +public void all(@Observes ChangeEvent changeEvent){ + log.log(Level.SEVERE, "ALL: Received a config change event: {0}", changeEvent); +} + +// Get only new values +public void newValue(@Observes @TypeFilter(Type.NEW) ChangeEvent changeEvent){ + log.log(Level.SEVERE, "NEW: Received a config change event: {0}", changeEvent); +} + +// Get only override values +public void overrideValue(@Observes @TypeFilter(Type.UPDATE) ChangeEvent changeEvent){ + log.log(Level.SEVERE, "UPDATE: Received a config change event: {0}", changeEvent); +} + +// Get only revert values +public void revertValue(@Observes @TypeFilter(Type.REMOVE) ChangeEvent changeEvent){ + log.log(Level.SEVERE, "REMOVE: Received a config change event: {0}", changeEvent); +} + +// Getting all config event when key is some.key +public void allForKey(@Observes @KeyFilter("some.key") ChangeEvent changeEvent){ + log.log(Level.SEVERE, "ALL for key [some.key]: Received a config change event: {0}", changeEvent); +} + +// Getting all config event when key is some.key for new events +public void newForKey(@Observes @TypeFilter(Type.NEW) @KeyFilter("some.key") ChangeEvent changeEvent){ + log.log(Level.SEVERE, "NEW for key [some.key]: Received a config change event: {0}", changeEvent); +} + +// Getting all config event when key is some.key for override events +public void overrideForKey(@Observes @TypeFilter(Type.UPDATE) @KeyFilter("some.key") ChangeEvent changeEvent){ + log.log(Level.SEVERE, "UPDATE for key [some.key]: Received a config change event: {0}", changeEvent); +} + +// Getting all config event when key is some.key for revert events +public void revertForKey(@Observes @TypeFilter(Type.REMOVE) @KeyFilter("some.key") ChangeEvent changeEvent){ + log.log(Level.SEVERE, "REMOVE for key [some.key]: Received a config change event: {0}", changeEvent); +} + +// Getting all config events for a certain source +public void allForSource(@Observes @SourceFilter("MemoryConfigSource") ChangeEvent changeEvent){ + log.log(Level.SEVERE, "ALL for source [MemoryConfigSource]: Received a config change event: {0}", changeEvent); +} + +// Getting all config events for a certain source +public void allForSourceAndKey(@Observes @SourceFilter("MemoryConfigSource") @KeyFilter("some.key") ChangeEvent changeEvent){ + log.log(Level.SEVERE, "ALL for source [MemoryConfigSource] and for key [some.key]: Received a config change event: {0}", changeEvent); +} + +// Getting all config events for a certain source +public void overrideForSourceAndKey(@Observes @TypeFilter(Type.UPDATE) @SourceFilter("MemoryConfigSource") @KeyFilter("some.key") ChangeEvent changeEvent){ + log.log(Level.SEVERE, "UPDATE for source [MemoryConfigSource] and for key [some.key]: Received a config change event: {0}", changeEvent); +} +---- + +Note: You can filter by including the `@TypeFilter` and/or the `@KeyFilter` and/or the `@SourceFilter`. + +=== Pattern matching on field. + +You might want to listen for fields that match a certain regex. + +Example, listen to all keys that starts with `some.`: + +[source,java] +---- +@RegexFilter("^some\\..+") +public void allForPatternMatchOnKey(@Observes ChangeEvent changeEvent){ + log.log(Level.SEVERE, "Pattern match on key: Received a config change event: {0}", changeEvent); +} +---- + +By default, it will match on `key`, however you also listen on another field, for example, listen to all `oldValue` +that starts with `some.`: + +[source,java] +---- +@RegexFilter(onField = Field.oldValue, value = "^some\\..+") +public void allForPatternMatchOnOldValue(@Observes ChangeEvent changeEvent){ + log.log(Level.SEVERE, "Pattern match on old value: Received a config change event: {0}", changeEvent); +} +---- + +You can Match on the following fields of the `ChangeEvent` object: + +* key +* oldValue +* newValue +* fromSource + +== Implementing Events in a ConfigSource + +The `ChangeEventNotifier` allows you to detect changes and fire the appropriate events. + +To use it in your own source: + +* Get a snapshot of the properties before the change. +* Get a snapshot of the properties after the change. +* Call `detectChangesAndFire` method: + +Example: + +[source,java] +---- +Map before = new HashMap<>(configSource.getProperties()); +memoryConfigSource.getProperties().remove(key); +Map after = new HashMap<>(configSource.getProperties()); +ChangeEventNotifier.getInstance().detectChangesAndFire(before, after,configSource.getName()); +---- + +or if you know the change and do not need detection: + +[source,java] +---- +configSource.getProperties().remove(key); +ChangeEventNotifier.getInstance().fire(new ChangeEvent(Type.REMOVE,key,getOptionalOldValue(oldValue),null,configSource.getName())); +---- diff --git a/content/docs/smallrye-config/2/extensions/config-source-injection.adoc b/content/docs/smallrye-config/2/extensions/config-source-injection.adoc new file mode 100644 index 000000000..7d79eafd3 --- /dev/null +++ b/content/docs/smallrye-config/2/extensions/config-source-injection.adoc @@ -0,0 +1,44 @@ += Config Source Injection +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The Config Source Injection extension allows you to use CDI injection to inject a ConfigSource by name in your CDI +aware beans, or by looking it up programatically in the CDI `BeanManager`. + +== Usage + +To use the Config Source Injection, add the following to your Maven `pom.xml`: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.config + smallrye-config-source-injection + {version} + +---- + +=== Injecting Sources + +You can inject a `ConfigSource` by referencing it by name: + +[source,java] +---- +@Inject +@Name("MemoryConfigSource") +private ConfigSource memoryConfigSource; + +@Inject +@Name("SysPropConfigSource") +private ConfigSource systemPropertiesConfigSource; +---- + +You can also get a Map of all config sources. The map key holds the `ConfigSource` name and the map value the +`ConfigSource`: + +[source,java] +---- +@Inject +@ConfigSourceMap +private Map configSourceMap; +---- diff --git a/content/docs/smallrye-config/2/extensions/fallback.adoc b/content/docs/smallrye-config/2/extensions/fallback.adoc new file mode 100644 index 000000000..cb0cde97a --- /dev/null +++ b/content/docs/smallrye-config/2/extensions/fallback.adoc @@ -0,0 +1,37 @@ += Fallback +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The `io.smallrye.config.FallbackConfigSourceInterceptor` allows to fall back to another configuration name, by +providing a transformation function or just a simple key value map. + +When a configuration name does not exist, there might be another configuration name that the config can +fall back to provide the same expected behavior. The fallback function is only applied if the original resolved +configuration name is not found and resolved to the fallback name. + +[source,java] +---- +package org.acme; + +import java.util.function.Function; +import io.smallrye.config.FallbackConfigSourceInterceptor; + +public class MicroProfileConfigFallbackInterceptor extends FallbackConfigSourceInterceptor { + public MicroProfileConfigFallbackInterceptor(final Function mapping) { + super(name -> name.startsWith("microprofile.config") ? + name.replaceAll("microprofile\\.config", "smallrye.config") : + name); + } +} +---- + +And registration in: +_META-INF/services/io.smallrye.config.ConfigSourceInterceptor_ + +[source,properties] +---- +io.smallrye.config.MicroProfileConfigFallbackInterceptor +---- + +The `MicroProfileConfigFallbackInterceptor` can fallback configuration names in the `microprofile.config` namespace +to the `smallrye.config` namespace. diff --git a/content/docs/smallrye-config/2/extensions/interceptors.adoc b/content/docs/smallrye-config/2/extensions/interceptors.adoc new file mode 100644 index 000000000..78063959f --- /dev/null +++ b/content/docs/smallrye-config/2/extensions/interceptors.adoc @@ -0,0 +1,82 @@ += Interceptors +:version-root: ../ +include::{version-root}_attributes.adoc[] + +SmallRye Config provides an interceptor chain that hooks into the configuration values resolution. This is useful to +implement features like xref:config/profiles.adoc[Profiles] , xref:config/expressions.adoc[Property Expressions], or just +logging to find out where the config value was loaded from. + +An interceptor can be created by implementing the +https://github.com/smallrye/smallrye-config/blob/main/implementation/src/main/java/io/smallrye/config/ConfigSourceInterceptor.java[ConfigSourceInterceptor] +interface. + +An interceptor requires an implementation of `io.smallrye.config.ConfigSourceInterceptor`. Each implementation requires +registration via the `ServiceLoader` mechanism in the `META-INF/services/io.smallrye.config.ConfigSourceInterceptor` +file. Alternatively, interceptors may be registered via the Programmatic API in +`SmallRyeConfigBuilder#withInterceptors`. + +The `io.smallrye.config.ConfigSourceInterceptor` is able to intercept the resolution of a configuration name with the +method `ConfigValue getValue(ConfigSourceInterceptorContext context, String name)`. The `ConfigSourceInterceptorContext` +is used to proceed with the interceptor chain. The chain can be short-circuited by returning an instance of +`io.smallrye.config.ConfigValue`. The `ConfigValue` objects hold information about the key name, value, config source +origin and ordinal. + +[NOTE] +==== + +The interceptor chain is applied before any conversion is performed on the configuration value. + +==== + +[source,java] +---- +package org.acme.config; + +import javax.annotation.Priority; + +import io.smallrye.config.ConfigSourceInterceptor; +import io.smallrye.config.ConfigLogging; + +@Priority(Priorities.LIBRARY + 200) +public class LoggingConfigSourceInterceptor implements ConfigSourceInterceptor { + private static final long serialVersionUID = 367246512037404779L; + + @Override + public ConfigValue getValue(final ConfigSourceInterceptorContext context, final String name) { + ConfigValue configValue = doLocked(() -> context.proceed(name)); + if (configValue != null) { + ConfigLogging.log.lookup(configValue.getName(), configValue.getLocation(), configValue.getValue()); + } else { + ConfigLogging.log.notFound(name); + } + return configValue; + } +} +---- + +And registration in: +_META-INF/services/io.smallrye.config.ConfigSourceInterceptor_ + +[source,properties] +---- +org.acme.config.LoggingConfigSourceInterceptor +---- + +The `LoggingConfigSourceInterceptor` logs looks up configuration names in the provided logging platform. The log +information includes config name and value, the config source origin and location if exists. + +Interceptors may also be created with an implementation of `io.smallrye.config.ConfigSourceInterceptorFactory`. Each +implementation requires registration via the `ServiceLoader` mechanism in the +`META-INF/services/io.smallrye.config.ConfigSourceInterceptorFactory` file. Alternatively, interceptors factories may +be registered via the Programmatic API in `SmallRyeConfigBuilder#withInterceptorFactories`. + +The `ConfigSourceInterceptorFactory` can initialize an interceptor with access to the current chain +(so it can be used to configure the interceptor and retrieve configuration values) and set the priority. + +A `ConfigSourceInterceptor` implementation class can specify a priority by way of the standard +`javax.annotation.Priority` annotation. If no priority is explicitly assigned, the default priority value of +`io.smallrye.config.Priorities.APPLICATION` is assumed. If multiple interceptors are registered with the same priority, +then their execution order may be non-deterministic. + +A collection of built-in priority constants can be found in `io.smallrye.config.Priorities`. It is recommended to +use `io.smallrye.config.Priorities.APPLICATION` as a baseline for user defined interceptors. diff --git a/content/docs/smallrye-config/2/extensions/logging.adoc b/content/docs/smallrye-config/2/extensions/logging.adoc new file mode 100644 index 000000000..3a42e6573 --- /dev/null +++ b/content/docs/smallrye-config/2/extensions/logging.adoc @@ -0,0 +1,15 @@ += Logging +:version-root: ../ +include::{version-root}_attributes.adoc[] + +== LoggingConfigSourceInterceptor + +The `io.smallrye.config.LoggingConfigSourceInterceptor` logs lookups of configuration names in the provided logging +platform. The log information includes config name and value, the config source origin and location if it exists. + +The log is done as `debug`, so the debug threshold must be set to `debug` for the `io.smallrye.config` appender to +display the logs. + +This requires registration via the `ServiceLoader` mechanism in the +`META-INF/services/io.smallrye.config.ConfigSourceInterceptorFactory` file of the +`io.smallrye.config.LoggingConfigSourceInterceptor` interceptor. diff --git a/content/docs/smallrye-config/2/extensions/relocate.adoc b/content/docs/smallrye-config/2/extensions/relocate.adoc new file mode 100644 index 000000000..726c07fdb --- /dev/null +++ b/content/docs/smallrye-config/2/extensions/relocate.adoc @@ -0,0 +1,37 @@ += Relocate +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The `io.smallrye.config.RelocateConfigSourceInterceptor` allows to relocate a configuration name to another name, by +providing a transformation function or just a simple key value map. + +When a configuration key is renamed, lookup needs to happen on the new name, but also on the old name if the +config sources are not updated yet. The relocation function gives priority to the new resolved configuration name or +resolves to the old name if no value is found under the new relocation name. + +[source,java] +---- +package org.acme; + +import java.util.function.Function; +import io.smallrye.config.RelocateConfigSourceInterceptor; + +public class MicroProfileConfigRelocateInterceptor extends RelocateConfigSourceInterceptor { + public MicroProfileConfigRelocateInterceptor(final Function mapping) { + super(name -> name.startsWith("microprofile.config") ? + name.replaceAll("microprofile\\.config", "smallrye.config") : + name); + } +} +---- + +And registration in: +_META-INF/services/io.smallrye.config.ConfigSourceInterceptor_ + +[source,properties] +---- +io.smallrye.config.MicroProfileConfigRelocateInterceptor +---- + +The `MicroProfileConfigRelocateInterceptor` can relocate configuration names in the `microprofile.config` namespace +to the `smallrye.config` namespace. diff --git a/content/docs/smallrye-config/2/index.adoc b/content/docs/smallrye-config/2/index.adoc new file mode 100644 index 000000000..8f38127ab --- /dev/null +++ b/content/docs/smallrye-config/2/index.adoc @@ -0,0 +1,82 @@ += SmallRye Config +include::_attributes.adoc[] + +*SmallRye Config* is a library that provides a way to configure applications, frameworks and containers. It is used +in applications servers like https://wildfly.org/[WildFly] and https://openliberty.io[Open Liberty], or frameworks +like https://quarkus.io[Quarkus]. It can also be used completely standalone in any Java application, which makes it a +very flexible library. + +It follows the https://github.com/eclipse/microprofile-config/[MicroProfile Config] specification to provide +the initial config foundations and expands with it own concepts to cover a wide range of use cases observed in the +configuration space. + +== Use SmallRye Config in a Java application + +Add the _dependency_ to your project using your preferred build tool: + +[.tabs] +==== + +Maven:: ++ +-- + +[source,xml,subs="attributes+"] +---- + + io.smallrye.config + smallrye-config + {version} + +---- + +-- + +Gradle (Groovy):: ++ +-- + +[source,groovy,subs="attributes+"] +---- +implementation 'io.smallrye.config:smallrye-config:{versions}' +---- + +-- + +Gradle (Kotlin):: ++ +-- + +[source,kotlin,subs="attributes+"] +---- +implementation("io.smallrye.config:smallrye-config:{versions}") +---- + +-- + +JBang:: ++ +-- + +[source,java,subs="attributes+"] +---- +//DEPS io.smallrye.config:smallrye-config:{versions} +---- + +-- + +==== + +And retrieve a `SmallRyeConfig` instance with: + +[source,java] +---- +SmallRyeConfig config = ConfigProvider.getConfig().unwrap(SmallRyeConfig.class); +---- + +[NOTE] +==== + +`SmallRyeConfig` is the entry point to all the config capabilities provided by SmallRye Config. + +==== diff --git a/content/docs/smallrye-config/3/_attributes.adoc b/content/docs/smallrye-config/3/_attributes.adoc new file mode 100644 index 000000000..1a04bab4a --- /dev/null +++ b/content/docs/smallrye-config/3/_attributes.adoc @@ -0,0 +1,21 @@ +:project-version: 3.18.3 +// Attributes shared by every page. smallrye.io overrides the ones it needs when it publishes. +// Pages in subdirectories set :version-root: (../ per level) before including this file. +ifndef::version-root[:version-root:] +:idprefix: +:idseparator: - +:sectanchors: +:examples-dir: {version-root}_examples +:partialsdir: {version-root}_partials +:project-name: SmallRye Config +:javadoc-version: latest +:version: {project-version} +:generated-dir: {version-root}_generated + +// Added by smallrye.io (tools/docs/Docs.java sync) +:javadoc-version: 3.18.3 +:relfileprefix: /docs/smallrye-config/3/ +:imagesdir: /docs/smallrye-config/3 +:docs-project: smallrye-config +:docs-version: 3 +:docs-ref: 3.18.3 diff --git a/content/docs/smallrye-config/3/_nav.yml b/content/docs/smallrye-config/3/_nav.yml new file mode 100644 index 000000000..8f3c48833 --- /dev/null +++ b/content/docs/smallrye-config/3/_nav.yml @@ -0,0 +1,66 @@ +title: SmallRye Config +nav: +- title: Home + page: index +- title: Config + items: + - title: Getting Started + page: config/getting-started + - title: Expressions + page: config/expressions + - title: Profiles + page: config/profiles + - title: Environment Variables + page: config/environment-variables + - title: Indexed Properties + page: config/indexed-properties + - title: ConfigValue + page: config/config-value + - title: Mappings + page: config/mappings + - title: Map Support + page: config/map-support + - title: Secret Keys + page: config/secret-keys + - title: Customizer + page: config/customizer + - title: Configuration Reference + page: config/configuration +- title: Config Sources + items: + - title: Custom + page: config-sources/custom + - title: Factories + page: config-sources/factories + - title: Locations + page: config-sources/locations + - title: YAML + page: config-sources/yaml + - title: JSON + page: config-sources/json + - title: FileSystem + page: config-sources/filesystem + - title: ZooKeeper + page: config-sources/zookeeper + - title: HOCON + page: config-sources/hocon + - title: KeyStore + page: config-sources/keystore +- title: Converters + items: + - title: Custom + page: converters/custom +- title: Extensions + items: + - title: Interceptors + page: extensions/interceptors + - title: Fallback + page: extensions/fallback + - title: Relocate + page: extensions/relocate + - title: Logging + page: extensions/logging + - title: Events + page: extensions/config-events + - title: Config Source Injection + page: extensions/config-source-injection diff --git a/content/docs/smallrye-config/3/assets/smallrye-icon.svg b/content/docs/smallrye-config/3/assets/smallrye-icon.svg new file mode 100644 index 000000000..c10b011e0 --- /dev/null +++ b/content/docs/smallrye-config/3/assets/smallrye-icon.svg @@ -0,0 +1,21 @@ + + + + + SmallRye Icon + + + + + + + + + diff --git a/content/docs/smallrye-config/3/config-sources/custom.adoc b/content/docs/smallrye-config/3/config-sources/custom.adoc new file mode 100644 index 000000000..bd9cc1444 --- /dev/null +++ b/content/docs/smallrye-config/3/config-sources/custom.adoc @@ -0,0 +1,166 @@ += Config Sources +:version-root: ../ +include::{version-root}_attributes.adoc[] + +== Custom `ConfigSource` + +It’s possible to create a custom ConfigSource as specified in +https://github.com/eclipse/microprofile-config/[MicroProfile Config]. + +With a Custom `ConfigSource` it is possible to read additional configuration values and add them to the `Config` +instance in a defined ordinal. This allows overriding values from other sources or falling back to other values. + +A custom `ConfigSource` requires an implementation of `org.eclipse.microprofile.config.spi.ConfigSource` or +`org.eclipse.microprofile.config.spi.ConfigSourceProvider`. Each implementation requires registration via the +`ServiceLoader` mechanism, either in `META-INF/services/org.eclipse.microprofile.config.spi.ConfigSource` or +`META-INF/services/org.eclipse.microprofile.config.spi.ConfigSourceProvider` files. + +Consider a simple in-memory ConfigSource: + +[source,java] +---- +package org.acme.config; + +import org.eclipse.microprofile.config.spi.ConfigSource; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +public class InMemoryConfigSource implements ConfigSource { + private static final Map configuration = new HashMap<>(); + + static { + configuration.put("my.prop", "1234"); + } + + @Override + public int getOrdinal() { + return 350; + } + + @Override + public Set getPropertyNames() { + return configuration.keySet(); + } + + @Override + public String getValue(final String propertyName) { + return configuration.get(propertyName); + } + + @Override + public String getName() { + return InMemoryConfigSource.class.getSimpleName(); + } +} +---- + +And registration in: + +.META-INF/services/org.eclipse.microprofile.config.spi.ConfigSource +[source,properties] +---- +org.acme.config.InMemoryConfigSource +---- + +The `InMemoryConfigSource` will be ordered between the `System properties` config source, and the +`Environment variables` config source due to the `350` ordinal. + +In this case, `my.prop` from `InMemoryConfigSource` will only be used if the `Config` instance is unable to find a +value in `System Properties`, ignoring all the other lower ordinal config sources. + +== `ConfigSourceFactory` + +Another way to create a `ConfigSource` is via the SmallRye Config `io.smallrye.config.ConfigSourceFactory` API. The +difference between the SmallRye Config factory and the standard way to create a `ConfigSource` as specified in +https://github.com/eclipse/microprofile-config/[MicroProfile Config], is the factory ability to provide a context +with access to the current configuration. + +Each implementation of `io.smallrye.config.ConfigSourceFactory` requires registration via the `ServiceLoader` mechanism +in the `META-INF/services/io.smallrye.config.ConfigSourceFactory` file. + +[source,java] +---- +package org.acme.config; + +import java.util.Collections; +import java.util.OptionalInt; + +import org.eclipse.microprofile.config.spi.ConfigSource; + +import io.smallrye.config.ConfigSourceContext; +import io.smallrye.config.ConfigSourceFactory; +import io.smallrye.config.ConfigValue; +import io.smallrye.config.PropertiesConfigSource; + +public class URLConfigSourceFactory implements ConfigSourceFactory { + @Override + public Iterable getConfigSources(final ConfigSourceContext context) { + final ConfigValue value = context.getValue("config.url"); + if (value == null || value.getValue() == null) { + return Collections.emptyList(); + } + + try { + return Collections.singletonList(new PropertiesConfigSource(new URL(value.getValue()))); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public OptionalInt getPriority() { + return OptionalInt.of(290); + } +} +---- + +And registration in: + +.META-INF/services/io.smallrye.config.ConfigSourceFactory +[source,properties] +---- +org.acme.config.URLConfigSourceFactory +---- + +By implementing `io.smallrye.config.ConfigSourceFactory`, a list of `ConfigSource` may be provided via the +`Iterable++<++ConfigSource++>++ getConfigSources(ConfigSourceContext context)` method. The `ConfigSourceFactory` may also +assign a priority by overriding the method `OptionalInt getPriority()`. The priority value is used to sort multiple +`io.smallrye.config.ConfigSourceFactory` (if found). Higher the value, higher the priority. + +[WARNING] +==== + +The `io.smallrye.config.ConfigSourceFactory` priority does not affect the `ConfigSource` ordinal. These are sorted +independently. + +==== + +When the Factory is initializing, the provided `ConfigSourceContext` may call the method +`ConfigValue getValue(String name)`. This method looks up configuration names in all `ConfigSource`s that were already +initialized by the `Config` instance, including sources with lower ordinals than the ones defined in the +`ConfigSourceFactory`. This means that a `Config` instance is initialized in two steps: first all `ConfigSource` and +`ConfigSourceProvider` and then the `ConfigSourceFactory`. Only configuation values found in the first step are avaible +to the ++`++ConfigSourceFactory. + +The `ConfigSource` list provided by a `ConfigSourceFactory` is not taken into consideration to +configure other sources produced by a lower priority `ConfigSourceFactory`. + +== Override `ConfigSource` ordinal + +The special configuration property name `config++_++ordinal` can be set in any `ConfigSource` to override its default +ordinal. + +For instance, setting the system property `-Dconfig++_++ordinal=200` will override the ordinal for the `System properties` +config source and move it to be looked up after the `Environment Variables` config source. + +== Properties + +The https://github.com/smallrye/smallrye-config/blob/main/implementation/src/main/java/io/smallrye/config/PropertiesConfigSource.java[PropertiesConfigSource] +creates a `ConfigSource` from Java `Properties`, `Map++<++String, String++>++` objects or a `.properties` file (referenced by +its URL). + +== `.env` + +The https://github.com/smallrye/smallrye-config/blob/main/implementation/src/main/java/io/smallrye/config/DotEnvConfigSourceProvider.java[DotEnvConfigSourceProvider] create a `ConfigSource` from a `.env` file. diff --git a/content/docs/smallrye-config/3/config-sources/factories.adoc b/content/docs/smallrye-config/3/config-sources/factories.adoc new file mode 100644 index 000000000..3d5155d44 --- /dev/null +++ b/content/docs/smallrye-config/3/config-sources/factories.adoc @@ -0,0 +1,109 @@ += Config Source Factory +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Another way to create a `ConfigSource` is via the `ConfigSourceFactory`. The difference between the SmallRye Config +Factory and the standard way to make a `ConfigSource` as specified in MicroProfile Config is the Factory's ability to +provide a context with access to the available configuration. With the `ConfigSourceFactory` it is possible to +bootstrap a `ConfigSource` that configures itself with previously initialized `ConfigSource's`. + +By implementing +https://github.com/smallrye/smallrye-config/blob/main/implementation/src/main/java/io/smallrye/config/ConfigSourceFactory.java[ConfigSourceFactory], +a list of `ConfigSource's` may be provided via the `Iterable++<++ConfigSource++>++ getConfigSources(ConfigSourceContext context)` method. The `ConfigSourceFactory` may also +assign a priority by overriding the default method `OptionalInt getPriority()`. The priority only sorts the factories +during initialization. After initialization, the provided `ConfigSources` will use their own ordinal and sorted with +all `ConfigSources` available in the `Config` instance. + +When the Factory initializes, the provided `ConfigSourceContext` may call the method +`ConfigValue getValue(String name)`. This method looks up configuration names in all `ConfigSource's` already +initialized by the `Config` instance, including sources with lower ordinals than the ones defined in the +`ConfigSourceFactory`. The `ConfigSourceFactory` does not consider `ConfigSources's` provided by other +`ConfigSourceFactory's` (the priority does not matter). + +Registration of a `ConfigSourceFactory` is done via the `ServiceLoader` mechanism by providing the +implementation classes in a `META-INF/services/io.smallrye.config.ConfigSourceFactory` file. Alternatively, factories +may be registered via the Programmatic API in `SmallRyeConfigBuilder#withSources`. + +A `ConfigSourceFactory` requires an implementation of `io.smallrye.config.ConfigSourceFactory`. Each implementation +requires registration via the `ServiceLoader` mechanism in the +`META-INF/services/io.smallrye.config.ConfigSourceFactory` file. Alternatively, interceptors may be registered via the +Programmatic API in `SmallRyeConfigBuilder#withSources`. + +[source,java] +---- +package org.acme.config + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Collections; +import java.util.List; + +import org.eclipse.microprofile.config.spi.ConfigSource; + +import io.smallrye.config._private.ConfigMessages; +import io.smallrye.config.PropertiesConfigSource; +import io.smallrye.config.ConfigSourceContext; +import io.smallrye.config.ConfigSourceFactory; +import io.smallrye.config.ConfigValue; + +public class FileSystemConfigSourceFactory implements ConfigSourceFactory { + @Override + public Iterable getConfigSources(final ConfigSourceContext context) { + final ConfigValue value = context.getValue("org.acme.config.file.locations"); + if (value == null || value.getValue() == null) { + return Collections.emptyList(); + } + + try { + return List.of(new PropertiesConfigSource(toURL(value.getValue()), 250)); + } catch (IOException e) { + return Collections.emptyList(); + } + } + + private URL toURL(final String value) { + try { + return new URI(value).toURL(); + } catch (URISyntaxException | MalformedURLException e) { + throw ConfigMessages.msg.uriSyntaxInvalid(e, value); + } + } +} +---- + +And registration in: + +.META-INF/services/io.smallrye.config.ConfigSourceFactory +[source,properties] +---- +org.acme.config.FileSystemConfigSourceFactory +---- + +The `FileSystemConfigSourceFactory` look ups the configuration value for `org.acme.config.file.locations`, and uses it +to set up an additional `ConfigSource`. + +Alternatively, a `ConfigurableConfigSourceFactory` accepts a `ConfigMapping` interface to configure the `ConfigSource`: + +[source,java] +---- +@ConfigMapping(prefix = "org.acme.config.file") +interface FileSystemConfig { + List locations(); +} +---- + +[source,java] +---- +public class FileSystemConfigurableConfigSourceFactory implements ConfigurableConfigSourceFactory { + @Override + public Iterable getConfigSources(ConfigSourceContext context, FileSystemConfig config) { + + } +} +---- + +With a `ConfigurableConfigSourceFactory` it is not required to look up the configuration values with +`ConfigSourceContext`. The values are automatically mapped with the defined `@ConfigMapping`. diff --git a/content/docs/smallrye-config/3/config-sources/filesystem.adoc b/content/docs/smallrye-config/3/config-sources/filesystem.adoc new file mode 100644 index 000000000..66e16db05 --- /dev/null +++ b/content/docs/smallrye-config/3/config-sources/filesystem.adoc @@ -0,0 +1,51 @@ += FileSystem Config Source +:version-root: ../ +include::{version-root}_attributes.adoc[] + +This Config Source loads configuration values for each file found in a directory. Each file corresponds to a single +property, where the file name is the configuration property name and the file content the configuration value. + +For instance, if a directory structure looks like: + +.... +foo/ +|__num.max +|__num.size +.... + +The `FileSystem` Config Source will provide 2 properties: + +* num.max +* num.size + +[WARNING] +==== + +Nested directories are not supported. + +==== + +This Config Source can be used to read configuration from +https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap[Kubernetes ConfigMap]. Check the +https://github.com/smallrye/smallrye-config/blob/main/examples/configmap/README.adoc[Kubernetes ConfigMap ConfigSource Example]. + +The same mapping rules as defined for environment variables are applied, so the `FileSystem` Config Source will search +for a given property name `num.max`: + +* Exact match (`num.max`) +* Replace each character that is neither alphanumeric nor ++_++ with ++_++ (`num++_++max`) +* Replace each character that is neither alphanumeric nor ++_++ with ++_++; then convert the name to upper case (`NUM++_++MAX`) + +The following dependency is required in the classpath to use the `FileSystem` Config Source: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.config + smallrye-config-source-file-system + {version} + +---- + +The configuration property `smallrye.config.source.file.locations` sets the directory paths to look up the files. It +accepts multiple locations separated by a comma and each must represent a valid URI to a directory. diff --git a/content/docs/smallrye-config/3/config-sources/hocon.adoc b/content/docs/smallrye-config/3/config-sources/hocon.adoc new file mode 100644 index 000000000..23b29b437 --- /dev/null +++ b/content/docs/smallrye-config/3/config-sources/hocon.adoc @@ -0,0 +1,46 @@ += HOCON Config Source +:version-root: ../ +include::{version-root}_attributes.adoc[] + +This Config Source allows to use the https://github.com/lightbend/config/blob/main/HOCON.md[HOCON] file format to +load configuration values. The HOCON Config Source loads the +configuration from the file `META-INF/microprofile-config.conf`. It has a lower ordinal (`50`) than the +`microprofile-config.properties`. + +The following dependency is required in the classpath to use the HOCON Config Source: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.config + smallrye-config-source-hocon + {version} + +---- + +Expressions defined as `$++{++value}` (unquoted) are resolved internally by the HOCON Config Source as described in the +https://github.com/lightbend/config/blob/main/HOCON.md#substitutions[HOCON Substitutions] documentation. Quoted +Expressions defined as `"$++{++value}"` are resolved by xref:config/expressions.adoc[SmallRye Config Property Expressions]. + +Consider: + +*hocon.conf* + +[source,conf] +---- +{ + foo: "bar", + hocon: ${foo}, + config: "${foo}" +} +---- + +*application.properties* + +[source,properties] +---- +config_ordinal=1000 +foo=baz +---- + +The value of `hocon` is `bar` and the value of `config` is `baz` (if the properties source has a higher ordinal). diff --git a/content/docs/smallrye-config/3/config-sources/json.adoc b/content/docs/smallrye-config/3/config-sources/json.adoc new file mode 100644 index 000000000..54fe1a808 --- /dev/null +++ b/content/docs/smallrye-config/3/config-sources/json.adoc @@ -0,0 +1,6 @@ += JSON Config Source +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The xref:config-sources/yaml.adoc[YAML Config Source] also accepts the JSON format as its contents. The configuration file +`META-INF/microprofile-config.yaml`, still requires to use the `yaml` extension. diff --git a/content/docs/smallrye-config/3/config-sources/keystore.adoc b/content/docs/smallrye-config/3/config-sources/keystore.adoc new file mode 100644 index 000000000..19c2dac49 --- /dev/null +++ b/content/docs/smallrye-config/3/config-sources/keystore.adoc @@ -0,0 +1,67 @@ += KeyStore Config Source +:version-root: ../ +include::{version-root}_attributes.adoc[] + +This Config Source allows to use a Java `KeyStore` to load configuration values. It uses an ordinal of `100`. + +The following dependency is required in the classpath to use the KeyStore Config Source: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.config + smallrye-config-source-keystore + {version} + +---- + +== Create a KeyStore + +The following command creates a simple KeyStore + +[source,bash] +---- +keytool -importpass -alias my.secret -keystore keystore -storepass secret -storetype PKCS12 -v +---- + +The `-alias my.secret` stores the configuration property name `my.secret` in the KeyStore. The command will +interactively ask for the value to be stored in the KeyStore. + +== Read the KeyStore + +The KeyStore Config Source supports reading multiple keystore files: + +[source,properties] +---- +smallrye.config.source.keystore.one.path=keystore-one +smallrye.config.source.keystore.one.password=password + +smallrye.config.source.keystore.two.path=keystore-two +smallrye.config.source.keystore.two.password=password +---- + +The names are arbitrary and can be any name. The name `one` and `two` are used to distinguish both KeyStores. + +If a stored configuration property requires a xref:config/secret-keys.adoc[Secret Handler] to decode a value, set +the handler name with `smallrye.config.source.keystore."name".handler`. + +== Configuration + +[width="100%",cols="95%,2%,3%",options="header",] +|=== +|Configuration Property |Type |Default +|`smallrye.config.source.keystore."name".path` + +The KeyStore path. |String | +|`smallrye.config.source.keystore."name".password` + +The KeyStore password. |String | +|`smallrye.config.source.keystore."name".type` + +The KeyStore type. |String |`PKCS12` +|`smallrye.config.source.keystore."name".handler` + +An Optional secret keys handler. |String | +|`smallrye.config.source.keystore."name".aliases."key".name` + +An Optional aliases key name. |String | +|`smallrye.config.source.keystore."name".aliases."key".password` + +An Optional aliases key password. |String | +|`smallrye.config.source.keystore."name".aliases."key".handler` + +An Optional aliases key secret keys handler. |String | +|=== diff --git a/content/docs/smallrye-config/3/config-sources/locations.adoc b/content/docs/smallrye-config/3/config-sources/locations.adoc new file mode 100644 index 000000000..84bdc6c76 --- /dev/null +++ b/content/docs/smallrye-config/3/config-sources/locations.adoc @@ -0,0 +1,82 @@ += Locations +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Additionally, to the default config locations specified by the +https://github.com/eclipse/microprofile-config/[MicroProfile Config] specification, SmallRye Config provides a way to +scan additional locations for configuration properties files. + +The `smallrye.config.locations` configuration property accepts multiple locations separated by a comma `,` and each +ust represent a valid `URI`. The supported `URI` schemes are: + +* file or directory (`file:`) +* classpath resource +* jar resource (`jar:`) +* http resource (`http:`) + +Each `URI` scheme loads all discovered resources in a `ConfigSource`. All loaded sources use the same ordinal of the +source that found the `smallrye.config.locations` configuration property. For instance, if `smallrye.config.locations` +is set as a system property, then all loaded sources have their ordinals set to `400` (system properties use `400` as +their ordinal). The ordinal may be overridden directly in the resource by setting the `config++_++ordinal` property. +Sources are sorted first by their ordinal, then by location order, and finally by loading order. + +If a profile is active, and the `URI` represents a single resource (for instance a file), then resources that match +the active profile are also loaded. The profile resource name must follow the pattern: `++{++location}-++{++profile}`. A +profile resource is only loaded if the unprofiled resource is also available in the same location. This is to keep a +consistent loading order and pair all the resources together. + +Profile resources are not taken into account if the location is a directory since there is no reliable way to discover +which file is the main resource. Properties that use the profile prefix syntax `%profile.` will work as expected. + +_All properties files from a directory_ + +[source,properties] +---- +# loads all files from a relative path +smallrye.config.locations=./src/main/resources/ + +# loads all files from an absolute path +smallrye.config.locations=/user/local/config +---- + +For relative paths, the JVM `user.dir` property defines the current directory. + +_A specific file_ + +../src/main/resources/additional.properties + +[source,properties] +---- +smallrye.config.locations=./src/main/resources/additional.properties +---- + +If a profile `dev` is active, and an `additional-dev.properties` file exists, this will also be loaded. + +_All `additional.properties` files from the classpath_ + +.additional.properties +[source,properties] +---- +smallrye.config.locations=additional.properties +---- + +If a profile `prod` is active, and an `additional-prod.properties` resources exists next to the `additional.properties` +resource, this will also be loaded. + +_The `resources.properties` file from a specific jar_ + +.jar:file:///user/local/app/lib/resources-.jar!/resources.properties +[source,properties] +---- +smallrye.config.locations=jar:file:///user/local/app/lib/resources-.jar!/resources.properties +---- + +If a profile `test` is active, and an `additional-test.properties` resource exists, this will also be loaded. + +_The `config.properties` file from a web server_ + +.http://localhost:8080/config/config.properties +[source,properties] +---- +smallrye.config.locations=http://localhost:8080/config/config.properties +---- diff --git a/content/docs/smallrye-config/3/config-sources/yaml.adoc b/content/docs/smallrye-config/3/config-sources/yaml.adoc new file mode 100644 index 000000000..0c8bb5e3f --- /dev/null +++ b/content/docs/smallrye-config/3/config-sources/yaml.adoc @@ -0,0 +1,21 @@ += YAML Config Source +:version-root: ../ +include::{version-root}_attributes.adoc[] + +This Config Source allows to use a `yaml` file to load configuration values. The YAML Config Source loads the configuration from the following files: + +[arabic] +. (`265`) `application.yaml++|++yml` in `config` folder, located in the current working directory +. (`255`) `application.yaml++|++yml` in the classpath +. (`110`) MicroProfile Config configuration file `META-INF/microprofile-config.yaml++|++yml` in the classpath + +The following dependency is required in the classpath to use the YAML Config Source: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.config + smallrye-config-source-yaml + {version} + +---- diff --git a/content/docs/smallrye-config/3/config-sources/zookeeper.adoc b/content/docs/smallrye-config/3/config-sources/zookeeper.adoc new file mode 100644 index 000000000..37f27785d --- /dev/null +++ b/content/docs/smallrye-config/3/config-sources/zookeeper.adoc @@ -0,0 +1,30 @@ += ZooKeeper Config Source +:version-root: ../ +include::{version-root}_attributes.adoc[] + +This Config Source allows using https://zookeeper.apache.org/index.html[Apache ZooKeeper] to load configuration +values. + +The following dependency is required in the classpath to use the ZooKeeper Config Source: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.config + smallrye-config-source-zookeeper + {version} + +---- + +It also requires to set up additional configuration properties to identify the ZooKeeper instance: + +[source,properties] +---- +io.smallrye.configsource.zookeeper.url=localhost:2181 +io.smallrye.configsource.zookeeper.applicationId=applicationId +---- + +The ZooKeeper Config Source will look for configuration values in a ZooKeeper instance running in the url set in +`io.smallrye.configsource.zookeeper.url` and in the znodes available in `/applicationId/`. + +This Config Source has an ordinal of `150`. diff --git a/content/docs/smallrye-config/3/config/config-value.adoc b/content/docs/smallrye-config/3/config/config-value.adoc new file mode 100644 index 000000000..63d58d715 --- /dev/null +++ b/content/docs/smallrye-config/3/config/config-value.adoc @@ -0,0 +1,8 @@ += Config Value +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The `io.smallrye.config.ConfigValue` is a metadata object that holds additional information after the lookup of a +configuration property. It is able to hold information of configuration property name, value, profile, the +`ConfigSource` from where the configuration was loaded, the ordinal of the `ConfigSource` and a line number from where +the configuration was loaded if exists. diff --git a/content/docs/smallrye-config/3/config/configuration.adoc b/content/docs/smallrye-config/3/config/configuration.adoc new file mode 100644 index 000000000..5cb6ba5f5 --- /dev/null +++ b/content/docs/smallrye-config/3/config/configuration.adoc @@ -0,0 +1,20 @@ += Configuration Reference +:version-root: ../ +include::{version-root}_attributes.adoc[] + +[width="100%",cols="92%,4%,4%",options="header",] +|=== +|Configuration Property |Type |Default +|`smallrye.config.profile` + +The main xref:config/profiles.adoc[Profile] to activate. |String++[]++ | +|`smallrye.config.profile.parent` + +The parent xref:config/profiles.adoc#parent-profile[Profile] to activate. |String | +|`smallrye.config.locations` + +xref:config-sources/locations.adoc[Additional config locations] to be loaded with the Config. The configuration supports multiple locations separated by a comma and each must represent a valid `java.net.URI`. |URI++[]++ | +|`smallrye.config.mapping.validate-unknown` + +xref:config/mappings.adoc#retrieval[Validates] that a `@ConfigMapping` maps every available configuration name contained in the mapping prefix. |boolean |false +|`smallrye.config.secret-handlers` + +The names of the secret handlers to be loaded. A value of `all` loads all available secret handlers and a value of `none` skips the load. |String++[]++ |all +|`smallrye.config.log.values` + +Enable logging of configuration values lookup in DEBUG log level. |boolean |false +|=== diff --git a/content/docs/smallrye-config/3/config/customizer.adoc b/content/docs/smallrye-config/3/config/customizer.adoc new file mode 100644 index 000000000..e841a645b --- /dev/null +++ b/content/docs/smallrye-config/3/config/customizer.adoc @@ -0,0 +1,49 @@ += Customizer +:version-root: ../ +include::{version-root}_attributes.adoc[] + +A `SmallRyeConfigBuilderCustomizer` allows to customize a `SmallRyeConfigBuilder`, used to create the `SmallRyeConfig` +instance. + +Registration of a `SmallRyeConfigBuilderCustomizer` is done via the `ServiceLoader` mechanism by providing the +implementation classes in a `META-INF/services/io.smallrye.config.SmallRyeConfigBuilderCustomizer` file. Alternatively, +customizers may be registered via the Programmatic API in `SmallRyeConfigBuilder#withCustomizers`. + +The `SmallRyeConfigBuilderCustomizer` may also +assign a priority by overriding the default method `int priority()`. Customizers are sorted by ascending priority and +executed in that order, meaning that higher numeric priorities will execute last, possible overriding values set by +previous customizers. + +.CustomConfigBuilder +[source,java] +---- +package org.acme.config; + +import io.smallrye.config.SmallRyeConfigBuilder; +import io.smallrye.config.SmallRyeConfigBuilderCustomizer; + +public class CustomConfigBuilder implements SmallRyeConfigBuilderCustomizer { + @Override + public void configBuilder(final SmallRyeConfigBuilder builder) { + builder.withDefaultValue("my.default", "1234"); + } +} +---- + +And registration in: + +.META-INF/services/io.smallrye.config.SmallRyeConfigBuilderCustomizer +[source,properties] +---- +org.acme.config.CustomConfigBuilder +---- + +The `CustomConfigBuilder` will be executed when creating a new `SmallRyeConfig` from a `SmallRyeConfigBuilder`: + +[source,java] +---- +SmallRyeConfig config = new SmallRyeConfigBuilder().build(); +config.getValue("my.default", int.class); +---- + +A look up to `my.default` returns the value `1234`, registered by the `CustomConfigBuilder`. diff --git a/content/docs/smallrye-config/3/config/environment-variables.adoc b/content/docs/smallrye-config/3/config/environment-variables.adoc new file mode 100644 index 000000000..15579d8e6 --- /dev/null +++ b/content/docs/smallrye-config/3/config/environment-variables.adoc @@ -0,0 +1,80 @@ += Environment Variables +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Environment Variable names follow the conversion rules specified by https://github.com/eclipse/microprofile-config/blob/master/spec/src/main/asciidoc/configsources.asciidoc#default-configsources[MicroProfile Config]. + +SmallRye Config specifies additional conversion rules: + +* A property with double quotes `foo."bar".baz` or `foo.bar."baz"`, replace each character that is neither alphanumeric +nor `++_++` with `++_++`: `FOO++__++BAR++__++BAZ` or `FOO++_++BAR++__++BAZ++_++` ; +* A property with dashes `foo.bar-baz`, replace each character that is neither alphanumeric nor `++_++` +with `++_++`: `FOO++_++BAR++_++BAZ` +* An indexed property `foo.bar++[++0++]++` or `foo.bar++[++0++]++.baz`, replace each character that is neither alphanumeric nor `++_++` +with `++_++`: `FOO++_++BAR++_++0++_++` or `FOO++_++BAR++_++0++__++BAZ`. + +[CAUTION] +==== + +Environment Variables format cannot represent the entire spectrum of common property names. + +==== + +The lookup of configuration values from Environment Variables will always use the dotted format name. For +instance, the lookup of the Environment Variable `FOO++_++BAR` value, requires the property name `foo.bar`: + +[source,java] +---- +Config config = Config.getOrCreate(); +String value = config.getValue("foo.bar", String.class); +---- + +When SmallRyeConfig performs the lookup on the Environment Variables Config Source, it applies the conversion rules to +find the matching property name and retrieve the value. + +In some situations, looking up the exact property name is impossible. For instance, when SmallRye Config has to look up +a configuration that is part of a `Map`, and the property name contains a dynamic segment (a `Map` key). In this case, +SmallRye Config relies upon each source's list of property names. These must be converted back to their most likely +dotted format for Environment Variables. + +By default, the underscore `++_++` of an Environment Variable name always maps to a dot `.`. If the property name +contains a dash or some other special character, that property name can be specified in another Config +Source, with the expected dotted format. It will provide additional information to SmallRye Config to perform a +two-way conversion and match the property names. + +Consider: + +..env + +[source,console] +---- +FOO_BAR_BAZ=VALUE +---- + +Will map to `foo.bar.baz` and value `value`. + +If `foo.bar-baz` is available in any source: + +..env + +[source,console] +---- +FOO_BAR_BAZ=VALUE +---- + +.application.properties +[source,properties] +---- +foo.bar-baz=default +---- + +Will map to `foo.bar-baz` and value `value`. + +[NOTE] +==== + +The property name in dotted format needs to exist somewhere to provide this additional information. It can be set in a +low ordinal source, even without value. The Environment Variables source will override the value and map the correct +configuration name. + +==== diff --git a/content/docs/smallrye-config/3/config/expressions.adoc b/content/docs/smallrye-config/3/config/expressions.adoc new file mode 100644 index 000000000..736d5dd41 --- /dev/null +++ b/content/docs/smallrye-config/3/config/expressions.adoc @@ -0,0 +1,32 @@ += Property Expressions +:version-root: ../ +include::{version-root}_attributes.adoc[] + +SmallRye Config provides property expressions expansion on configuration values. An expression string is a mix of plain +strings and expression segments, which are wrapped by the sequence $++{++ ... }. + +For instance, the following configuration properties file: + +[source,properties] +---- +remote.host=smallrye.io +callable.url=https://${remote.host}/ +---- + +The resolved value of the `callable.url` property is `https://smallrye.io/`. + +Additionally, the Expression Expansion engine supports the following segments: + +* `$++{++expression:value}` - Provides a default value after the `:` if the expansion doesn’t find a value. +* `$++{++my.prop$++{++compose}}` - Composed expressions. Inner expressions are resolved first. +* `$++{++my.prop}$++{++my.prop}` - Multiple expressions. + +If an expression cannot be expanded and no default is supplied a `NoSuchElementException` is thrown. + +Expression expansion may be selectively disabled with `io.smallrye.config.Expressions`: + +[source,java] +---- +Config config = Config.getOrCreate(); +String url = Expressions.withoutExpansion(() -> config.getValue("callable.url", String.class)); +---- diff --git a/content/docs/smallrye-config/3/config/getting-started.adoc b/content/docs/smallrye-config/3/config/getting-started.adoc new file mode 100644 index 000000000..d2eb46c6d --- /dev/null +++ b/content/docs/smallrye-config/3/config/getting-started.adoc @@ -0,0 +1,182 @@ += Getting Started +:version-root: ../ +include::{version-root}_attributes.adoc[] + +== Config Sources + +By default, SmallRye Config reads configuration properties from multiple configuration sources (by descending ordinal): + +[arabic] +. (`400`) System properties +. (`300`) Environment variables +. (`295`) `.env` file in the current working directory +. (`260`) `application.properties` in `config` folder, located in the current working directory +. (`250`) `application.properties` in the classpath +. (`100`) MicroProfile Config configuration file `META-INF/microprofile-config.properties` in the classpath + +A configuration source is handled by a `ConfigSource`. A `ConfigSource` provides configuration values from a specific +place. + +The final configuration is the aggregation of the properties defined by all these sources. A configuration property +lookup starts by the highest ordinal configuration source available and works it way down to other sources until a +match is found. This means that any configuration property may override a value just by setting a different value in a +higher ordinal config source. For example, a property configured using an Environment Variable overrides the value +provided using the `microprofile-config.properties` file. + +=== System Properties + +System properties can be handed to the application through the `-D` flag during startup. For instance, +`java -Dmy.prop -jar my.jar`. + +=== Environment Variables + +Environment variables are set directly in the host operating system. Environment variables names follow the conversion +rules detailed by xref:config/environment-variables.adoc[Environment Variables]. + +=== MicroProfile Config configuration file + +The MicroProfile Config configuration file `META-INF/microprofile-config.properties` in the classpath. It follows the +standard convention for `properties` files. + +.META-INF/microprofile-config.properties +[source,properties] +---- +greeting.message=hello +goodbye.message=bye +---- + +=== Additional Config Sources + +SmallRye Config provides additional extensions which cover other configuration formats and stores: + +* xref:config-sources/yaml.adoc[YAML] +* xref:config-sources/filesystem.adoc[File System] +* xref:config-sources/zookeeper.adoc[ZooKeeper] +* xref:config-sources/hocon.adoc[HOCON] + +It is also possible to create a xref:config-sources/custom.adoc[Custom ConfigSource]. + +== Retrieving the Configuration + +=== Programmatically + +The `io.smallrye.config.Config#getOrCreate()` API allows to access the `io.smallrye.config.Config` API +programmatically: + +[source,java] +---- +import io.smallrye.config.Config; + +Config config = Config.getOrCreate(); +String message = config.getValue("greeting.message", String.class); +---- + +The `Config` instance will be created and registered to the current context class loader if no such configuration is +already created and registered. This means that subsequent calls to `Config.getOrCreate()` or +`Config.get()` will return the same `Config` instance if the context class loader is the same. + +To obtain a detached instanced, use the `io.smallrye.config.SmallRyeConfigBuilder`: + +[source,java] +---- +SmallRyeConfig config = new SmallRyeConfigBuilder() + .addDefaultInterceptors() + .addDefaultSources() + .build(); + +String message = config.getValue("greeting.message", String.class); +---- + +=== With CDI + +In a CDI environment, configuration can be injected in CDI aware beans with `@Inject` and +the `org.eclipse.microprofile.config.inject.ConfigProperty` qualifier. + +[source,java] +---- +@Inject +@ConfigProperty(name = "greeting.message") +String message; + +@Inject +@ConfigProperty(name = "greeting.suffix", defaultValue="!") +String suffix; + +@Inject +@ConfigProperty(name = "greeting.name") +Optional name; + +@Inject +SmallRyeConfig config; +---- + +* If a value if not provided for this `greeting.message`, the application startup fails with a +`jakarta.enterprise.inject.spi.DeploymentException: No config value of type ++[++class java.lang.String++]++ exists for: greeting.message`. +* The default value `!` is injected if the configuration does not provide a value for `greeting.suffix`. +* The property `greeting.name` is optional - an empty Optional is injected if the configuration does not provide a +value for it. + +== Override Config + +It is possible to override `Config` default initialization `Config.getOrCreate()`, by extending +`io.smallrye.config.SmallRyeConfigFactory` and registering the implementation with the `ServiceLoader` mechanism. + +== Config vs SmallRyeConfig + +The `io.smallrye.config.Config` is an extension of `org.eclipse.microprofile.config.Config` and provides +additional APIs and helper methods not available in `org.eclipse.microprofile.config.Config`. To obtain an instance of +`io.smallrye.config.Config`, the original `org.eclipse.microprofile.config.Config` can be unwrapped: + +[source,java] +---- +import io.smallrye.config.SmallRyeConfig; +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.ConfigProvider; + +Config config = ConfigProvider.getConfig(); +SmallRyeConfig smallRyeConfig = config.unwrap(SmallRyeConfig.class); +---- + +Or obtained directly: + +[source,java] +---- +Config config = Config.getOrCreate(); +---- + +A few notable APIs provided by `io.smallrye.config.Config` allow to: + +* Retrieve multiple values into a specified `Collection` +* Retrieve xref:config/indexed-properties.adoc[Indexed Values] +* Retrieve xref:config/mappings.adoc[Config Mappings] instances +* Retrieve the raw value of a configuration +* Check if a property is present +* Retrieve a `Converter` +* Convert values + +== Converters + +The `ConfigSource` retrieves a configuration value as a `String`. Other data types require a conversion using the +`org.eclipse.microprofile.config.spi.Converter` API. + +Most of the common `Converter` types are provided by default: + +* `boolean` and `java.lang.Boolean`; the values "true", "1", "YES", "Y" "ON" represent `true`. Any other value will be +interpreted as `false` +* `byte` and `java.lang.Byte` +* `short` and `java.lang.Short` +* `int`, `java.lang.Integer`, and `java.util.OptionalInt` +* `long`, `java.lang.Long`, and `java.util.OptionalLong` +* `float` and `java.lang.Float`; a dot '.' is used to separate the fractional digits +* `double`, `java.lang.Double`, and `java.util.OptionalDouble`; a dot '.' is used to separate the fractional digits +* `char` and `java.lang.Character` +* `java.lang.Class` based on the result of `Class.forName` +* `java.net.InetAddress` +* `java.util.UUID` +* `java.util.Currency` +* `java.util.regex.Pattern` +* `java.nio.file.Path` +* Any class with declared static methods `of`, `valueOf` or `parse` that take a `String` or a `CharSequence` +* Any class with declared constructors that takes a `String` or a `CharSequence` + +All default converters have a priority of `1`. diff --git a/content/docs/smallrye-config/3/config/indexed-properties.adoc b/content/docs/smallrye-config/3/config/indexed-properties.adoc new file mode 100644 index 000000000..78b32cc10 --- /dev/null +++ b/content/docs/smallrye-config/3/config/indexed-properties.adoc @@ -0,0 +1,32 @@ += Indexed Properties +:version-root: ../ +include::{version-root}_attributes.adoc[] + +In https://github.com/eclipse/microprofile-config/[MicroProfile Config], a config value with unescaped commas may be +converted to `Collection`. It works for simple cases, but it becomes cumbersome and limited for more advanced use cases. + +Indexed Properties provide a way to use indexes in config property names to map specific elements in a `Collection` +type. Since the indexed element is part of the property name, it can also map complex object types. Consider: + +[source,properties] +---- +# MicroProfile Config - Collection Values +my.collection=dog,cat,turtle + +# SmallRye Config - Indexed Property +my.indexed.collection[0]=dog +my.indexed.collection[1]=cat +my.indexed.collection[2]=turtle +---- + +The indexed property syntax uses the property name and square brackets with an index in between. + +A call to `Config#getValues("my.collection", String.class)`, will automatically create and convert a `List++<++String++>++` +that contains the values `dog`, `cat` and `turtle`. A call to `Config#getValues("my.indexed.collection", String.class)` +returns the exact same result. The indexed property format is prioritized when both styles are found in the same +configuration source. When available in multiple sources, the higher ordinal source wins, like any other configuration +lookup. + +The indexed property is sorted by its index before being added to the target `Collection`. Any gaps in the indexes do +not resolve to the target `Collection`, which means that the `Collection` result will store all values without empty +elements. diff --git a/content/docs/smallrye-config/3/config/map-support.adoc b/content/docs/smallrye-config/3/config/map-support.adoc new file mode 100644 index 000000000..72b42740c --- /dev/null +++ b/content/docs/smallrye-config/3/config/map-support.adoc @@ -0,0 +1,60 @@ += Map Support +:version-root: ../ +include::{version-root}_attributes.adoc[] + +SmallRye Config allows injecting multiple configuration parameters as a `Map`. The configuration value syntax is +represented by `property.name.map-key=value` Consider: + +[source,properties] +---- +server.reasons.200=OK +server.reasons.201=Created +---- + +The previous configuration could be injected directly in a CDI Bean: + +With `@ConfigProperty` + +[source,java] +---- +@ApplicationScoped +public class ConfigBean { + @Inject + @ConfigProperty(name = "server.reasons") + Map reasons; +} +---- + +With `@ConfigProperties` + +[source,java] +---- +@ConfigProperties(prefix = "server") +public class Config { + Map reasons; +} +---- + +The `Map` will contains the keys `200` and `201`, which map to the values `OK` and `Created`. + +[NOTE] +==== + +Only the direct sub properties will be converted into a `Map` and +injected into the target bean, the rest will be ignored. In other words, +in the previous example, a property whose name is `reasons.200.a` would +be ignored as not considered as a direct sub property. + +==== + +[NOTE] +==== + +The property will be considered as missing if no direct sub properties +could be found. + +==== + +It is also possible to retrieve the `Map` programmatically by calling the methods +`SmallRyeConfig#getValues("server.reasons", Integer.class, String.class)` or +`SmallRyeConfig#getOptionalValues("server.reasons", Integer.class, String.class)`. diff --git a/content/docs/smallrye-config/3/config/mappings.adoc b/content/docs/smallrye-config/3/config/mappings.adoc new file mode 100644 index 000000000..a8dabf642 --- /dev/null +++ b/content/docs/smallrye-config/3/config/mappings.adoc @@ -0,0 +1,727 @@ += Mappings +:version-root: ../ +include::{version-root}_attributes.adoc[] + +With SmallRye Config Mappings, it is possible to group multiple configuration properties in a single interface that +share the same prefix (or namespace). It supports the following set of features: + +* Automatic conversion of the configuration type, including `List`, `Set`, `Map`, `Optional` and primitive types. +* Nested Config Mapping groups. +* Configuration Properties Naming Strategies +* Integration with Bean Validation + +== Mapping Rules + +A complex object type uses the following rules to map configuration values to their member values: + +* A configuration path is built by taking the object type prefix (or namespace) and the mapping member name +* The member name is converted to its kebab-case format +* If the member name is represented as a getter, the member name is taken from its property name equivalent, and then +converted to its kebab-case format +* The configuration value is automatically converted to the member type +* The configuration path is required to exist with a valid configuration value or the mapping will fail + +[NOTE] +==== + +Kebab-case - the method name is derived by replacing case changes with a dash to map the configuration property. + +==== + +A Config Mapping requires an interface with minimal metadata configuration annotated with +`io.smallrye.config.ConfigMapping`: + +[source,java] +---- +@ConfigMapping(prefix = "server") +interface Server { + String host(); + + int port(); +} +---- + +The `Server` interface is able to map configurations with the name `server.host` into the `Server#host()` method and +`server.port` into `Server#port()` method. The configuration property name to lookup is built from the prefix, and the +method name with `.` (dot) as the separator. + +[WARNING] +==== + +If a mapping fails to match a configuration property the config system throws a `NoSuchElementException`, unless +the mapped element is an `Optional`. + +==== + +The `@ConfigMapping` interface must obey the following rules: + +* A mapping method cannot accept parameters +* A mapping method return type cannot be void +* A mapping cannot use self-reference types +* `default` methods are allowed + +== Registration + +Registration of Config Mappings is automatic in CDI aware environments with the `@ConfigMapping` annotation. + +In non-CDI environments, the Config Mapping can be registered via `SmallRyeConfigBuilder#withMapping`. In this case, +the `@ConfigMapping` is completely optional (but recommendeded to set the prefix). + +[source,java] +---- +SmallRyeConfig config = new SmallRyeConfigBuilder() + .withMapping(Server.class) + .build(); +---- + +== Retrieval + +A config mapping interface can be injected into any CDI aware bean: + +[source,java] +---- +@ApplicationScoped +class BusinessBean { + @Inject + Server server; + + public void businessMethod() { + String host = server.host(); + } +} +---- + +In non-CDI environments, use the API `io.smallrye.config.SmallRyeConfig#getConfigMapping` to retrieve the config +mapping instance: + +[source,java] +---- +Config config = Config.getOrCreate(); +Server server = config.getConfigMapping(Server.class); +---- + +[NOTE] +==== + +Config Mapping instances are cached. They are populated when the `SmallRyeConfig` instance is initialized and +their values are not updated on `ConfigSource` changes. + +==== + +For a Config Mapping to be valid, it needs to match every configuration property name contained in the `Config` under +the specified prefix set in `@ConfigMapping`. This prevents unknown configuration properties in the `Config`. This +behaviour can be disabled with the configuration `smallrye.config.mapping.validate-unknown=false`, or by ignoring +specified paths with `io.smallrye.config.SmallRyeConfigBuilder.withMappingIgnore`. + +== Defaults + +The `io.smallrye.config.WithDefault` annotation allows to set a default property value into a mapping (and prevent +errors if the configuration value is not available in any `ConfigSource`). + +[source,java] +---- +public interface Defaults { + @WithDefault("foo") + String foo(); + + @WithDefault("bar") + String bar(); +} +---- + +No configuration properties are required. The `Defaults#foo()` will return the value `foo` and `Defaults#bar()` will +return the value `bar`. + +== Nested Groups + +A nested mapping provides a way to map sub-groups of configuration properties. + +* A nested type contributes with its name (converted to its kebab-case format) +* The configuration path is built by taking the root object type prefix (or namespace), the nested type name and the +member name of the nested type + +[source,java] +---- +@ConfigMapping(prefix = "server") +public interface Server { + String host(); + + int port(); + + Log log(); + + interface Log { + boolean enabled(); + + String suffix(); + + boolean rotate(); + } +} +---- + +[source,properties] +---- +server.host=localhost +server.port=8080 +server.log.enabled=true +server.log.suffix=.log +server.log.rotate=false +---- + +The method name of a mapping group acts as a sub-prefix in the property name. In this case the matching property to +`Server.Log#enabled` is `server.log.enabled`. + +== Hierarchy + +A config mapping can extend another mapping and inherit all its super members: + +[source,java] +---- +public interface Parent { + String name(); +} + +@ConfigMapping(prefix = "child") +public interface Child extends Parent { + +} +---- + +And override members: + +[source,java] +---- +public interface Parent { + String name(); +} + +@ConfigMapping(prefix = "child") +public interface Child extends Parent { + @WithName("child-name") + String name(); +} +---- + +== Overriding property names + +=== `@WithName` + +If a method name and a property name do not match, the `io.smallrye.config.WithName` annotation can +override the method name mapping and use the name supplied in the annotation. + +[source,java] +---- +@ConfigMapping(prefix = "server") +interface Server { + @WithName("name") + String host(); + + int port(); +} +---- + +[source,properties] +---- +server.name=localhost +server.port=8080 +---- + +=== `@WithParentName` + +The `io.smallrye.config.WithParentName` annotation allows configurations mappings to inherit its parent container name, +simplifying the configuration property name required to match the mapping. + +[source,java] +---- +@ConfigMapping(prefix = "server") +interface Server { + @WithParentName + ServerHostAndPort hostAndPort(); + + @WithParentName + ServerInfo info(); +} + +interface ServerHostAndPort { + String host(); + + int port(); +} + +interface ServerInfo { + String name(); +} +---- + +[source,properties] +---- +server.host=localhost +server.port=8080 +server.name=konoha +---- + +Without the `@WithParentName` the method `ServerInfo#name` maps the configuration property `server.info.name`. With +`@WithParentName`, the `Server#info` mapping will inherit the parent name from `Server` and `ServerInfo#name` maps to +the property `server.name` instead. + +=== NamingStrategy + +Method names in camelCase map to kebab-case configuration property names by default. + +[source,java] +---- +@ConfigMapping(prefix = "server") +interface Server { + String theHost(); + + int thePort(); +} +---- + +[source,properties] +---- +server.the-host=localhost +server.the-port=8080 +---- + +The mapping strategy can be adjusted by setting `namingStrategy` value in the `@ConfigMapping` annotation. + +[source,java] +---- +@ConfigMapping(prefix = "server", namingStrategy = ConfigMapping.NamingStrategy.VERBATIM) +public interface ServerVerbatimNamingStrategy { + String theHost(); + + int thePort(); +} +---- + +[source,properties] +---- +server.theHost=localhost +server.thePort=8080 +---- + +The `@ConfigMapping` annotation support the following naming stategies: + +* KEBAB++_++CASE - The method name is derived by replacing case changes with a dash to map the configuration property. +* VERBATIM - The method name is used as is to map the configuration property. +* SNAKE++_++CASE - The method name is derived by replacing case changes with an underscore to map the configuration property. + +== Conversion + +A config mapping interface support automatic conversions of all types available for conversion in `Config`. + +[source,java] +---- +@ConfigMapping +public interface SomeTypes { + @WithName("int") + int intPrimitive(); + + @WithName("int") + Integer intWrapper(); + + @WithName("long") + long longPrimitive(); + + @WithName("long") + Long longWrapper(); + + @WithName("float") + float floatPrimitive(); + + @WithName("float") + Float floatWrapper(); + + @WithName("double") + double doublePrimitive(); + + @WithName("double") + Double doubleWrapper(); + + @WithName("char") + char charPrimitive(); + + @WithName("char") + Character charWrapper(); + + @WithName("boolean") + boolean booleanPrimitive(); + + @WithName("boolean") + Boolean booleanWrapper(); +} +---- + +[source,properties] +---- +int=9 +long=9999999999 +float=99.9 +double=99.99 +char=c +boolean=true +---- + +This is also valid for `Optional` and friends. + +[source,java] +---- +@ConfigMapping +public interface Optionals { + Optional server(); + + Optional optional(); + + @WithName("optional.int") + OptionalInt optionalInt(); + + interface Server { + String host(); + + int port(); + } +} +---- + +In this case, the mapping won’t fail if the configuraton properties values are missing. + +=== `@WithConverter` + +The `io.smallrye.config.WithConverter` annotation provides a way to set a specific `Converter` in a mapping. + +[source,java] +---- +@ConfigMapping +public interface Converters { + @WithConverter(FooBarConverter.class) + String foo(); +} + +public static class FooBarConverter implements Converter { + @Override + public String convert(final String value) { + return "bar"; + } +} +---- + +[source,properties] +---- +foo=foo +---- + +A call to `Converters.foo()` results in the value `bar`. + +== Collections + +A config mapping is also able to map the collections types `List` and `Set`. + +* A member with a `Collection` type requires the configuration name to be in its indexed format +* Each configuration name, plus its index maps the configuration value to the corresponding `Collection` element in the +object type +* The index must be part of the configuration path, by appending the index between square brackets to the`Collection` +member +* The index specified in the configuration name is used to order the element in the `Collection` +* Missing elements or gaps are removed + +[source,java] +---- +@ConfigMapping(prefix = "server") +public interface ServerCollections { + Set environments(); + + interface Environment { + String name(); + + List apps(); + + interface App { + String name(); + + List services(); + + Optional> databases(); + } + } +} +---- + +[source,properties] +---- +server.environments[0].name=dev +server.environments[0].apps[0].name=rest +server.environments[0].apps[0].services=bookstore,registration +server.environments[0].apps[0].databases=pg,h2 +server.environments[0].apps[1].name=batch +server.environments[0].apps[1].services=stock,warehouse +---- + +The `List` and `Set` mappings can use xref:config/indexed-properties.adoc[Indexed Properties] to map configuration values in +mapping groups. + +[NOTE] +==== + +A `List` mapping is backed by an `ArrayList`, and a `Set` mapping is backed by a `HashSet`. + +==== + +== Maps + +A config mapping is also able to map a `Map`. + +* A member with a `Map` type requires an additional configuration name added to the configuration path of the `Map` +member to act as a map key +* The additional configuration name maps a Map entry with the configuration name as the `Map` entry key and +the configuration value as the Map entry value + +[source,java] +---- +@ConfigMapping(prefix = "server") +public interface Server { + String host(); + + int port(); + + Map form(); + + Map> aliases(); + + interface Alias { + String name(); + } +} +---- + +[source,properties] +---- +server.host=localhost +server.port=8080 +server.form.index=index.html +server.form.login.page=login.html +server.form.error.page=error.html +server.aliases.localhost[0].name=prod +server.aliases.localhost[1].name=127.0.0.1 +server.aliases.\"io.smallrye\"[0].name=smallrye +---- + +The configuration property name needs to specify an additional segment to act as the map key. The `server.form` matches +the `Server#form` `Map` and the segments `index`, `login.page` and `error.page` represent the `Map` +keys. + +[NOTE] +==== + +A `Map` mapping is backed by an `HashMap`. + +==== + +When populating a `Map`, `SmallRyeConfig` requires the configuration names listed in +`SmallRyeConfig#getPropertyNames` to find the `Map` keys. If a `ConfigSource` does not support +`getPropertyNames` (empty), the names must be provided by another `ConfigSource` that can do so. After retrieving the +map keys, `SmallRyeConfig` performs the lookup of the values with the regular `ConfigSource` ordinal ordering. Even if +a `ConfigSource` does not provide `getPropertyNames` it can provide the value by having the name listed in another +capable `ConfigSource`. + +For collection types, the key requires the indexed format. The configuration name `server.aliases.localhost++[++0++]++.name` +maps to the `Map++<++String, List++<++Alias++>>++ aliases()` member, where `localhost` is the `Map` key, `++[++0++]++` is the index of the +`List++<++Alias++>++` collection where the `Alias` element will be stored, containing the name `prod`. + +[TIP] +.Map quoted keys requirement +==== + +They `Map` key part in the configuration property name may require quotes to delimit the key if the key contains +a dot, otherwise they are optional. If the key name is delimted by quotes, all configuration property names related +with the same name must use the quoted format across all sources, including Environment Variables. + +==== + +=== `@WithUnnamedKey` + +The `io.smallrye.config.WithUnnamedKey` annotation allows to omit a single map key in the configuration path: + +[source,java] +---- +@ConfigMapping(prefix = "server") +public interface Server { + @WithUnnamedKey("localhost") + Map aliases(); + + interface Alias { + String name(); + } +} +---- + +[source,properties] +---- +server.aliases.name=localhost +server.aliases.prod.name=prod +---- + +The `sever.aliases.name` is an unnamed `Map` property, because it does not contain the `Map` key to populate the `Map` +entry. Due to `@WithUnnamedKey("localhost")` the `Map` key is not required in the configuration path. The key used to +look up the Map entry is given by `io.smallrye.config.WithUnnamedKey#value`: + +[source,java] +---- +Server server = config.getConfigMapping(Server.class); +Map localhost = server.aliases.get("localhost"); +---- + +[WARNING] +==== + +If the unnamed key (in this case `localhost`) is explicitly set in a property name, the mapping will throw an error. + +==== + +=== `@WithKeys` + +The `io.smallrye.config.WithKeys` annotation allows to define which `Map` keys must be loaded by +the configuration: + +[source,java] +---- +@ConfigMapping(prefix = "server") +public interface Server { + @WithKeys(KeysProvider.class) + Map aliases(); + + interface Alias { + String name(); + } + + class KeysProvider implements Supplier> { + @Override + public Iterable get() { + return List.of("dev", "test", "prod"); + } + } +} +---- + +In this case, `SmallRyeConfig` will look for the map keys `dev`, `test` and `prod` instead of discovering the keys +with `SmallRyeConfig#getPropertyNames`: + +[source,properties] +---- +servers.alias.dev.name=dev +servers.alias.test.name=test +servers.alias.prod.name=prod +---- + +The provided list will effectively substitute the lookup in `SmallRyeConfig#getPropertyNames`, thus enabling a +`ConfigSource` that does not list its properties, to contribute configuration to the `Map`. Each key must exist in +the final configuration (relative to the `Map` path segment), or the mapping will fail with a +`ConfigValidationException`. + +=== `@WithDefaults` + +The `io.smallrye.config.WithDefaults` is a marker annotation to use only in a `Map` to return the default value for +the value element on any key lookup: + +[source,java] +---- +@ConfigMapping(prefix = "server") +public interface Server { + @WithDefaults + Map aliases(); + + interface Alias { + @WithDefault("localhost") + String name(); + } +} +---- + +[source,properties] +---- +server.aliases.prod.name=prod +---- + +A look-up to the `aliases` `Map` with the key `localhost`, `any` or any other key, returns a `Alias` instance, where +`Alias.name` is `localhost`, which is the default value. A look-up to `prod` returns a `Alias` instance, where +`Alias.name` is `prod` because the property is defined in the configuration as `server.aliases.prod.name=prod`. + +The `Map` can only iterate and size the defined keys. In this case, the `aliases` `Map` only iterates the `prod` key, +and the size is `1`. + +[source,java] +---- +Server server = config.getConfigMapping(Server.class); +Map localhost = server.aliases.get("localhost"); +Map any = server.aliases.get("any"); +Map any = server.aliases.get("prod"); +---- + +== Optionals + +* A mapping can wrap any complex type with an `Optional` +* `Optional` mappings do not require the configuration path and value to be present + +== Secrets + +* A mapping can mark a member type as a secret with `Secret++<++T++>++`: + +[source,java] +---- +@ConfigMapping(prefix = "credentials") +public interface Credentials { + String username(); + + Secret password(); +} +---- + +A `Secret` value modifies the behaviour of the config system by: + +* Omitting the name of the secret in `SmallRyeConfig#getPropertyNames()` +* Omitting the name and value of the secret in the mapping `toString` method +* Throwing a `SecurityException` when trying to retrieve the value via `SmallRyeConfig` programmatic API + +A Secret can be of any type that can be converted by a registered `org.eclipse.microprofile.config.spi.Converter` of +the same type. + +== toString, equals, hashcode + +If the config mapping contains a `toString` method declaration, the config mapping instance will include a proper +implementation of the `toString` method. The `equals` and `hashcode` methods are included automatically. + +[CAUTION] +==== + +Do not include a `toString` declaration in a config mapping with sensitive information. + +==== + +== Validation + +A config mapping may combine annotations from https://beanvalidation.org/[Bean Validation] to validate configuration +properties values. + +[source,java] +---- +@ConfigMapping(prefix = "server") +interface Server { + @Size(min = 2, max = 20) + String host(); + + @Max(10000) + int port(); +} +---- + +The application startup fails with a `io.smallrye.config.ConfigValidationException` if the configuration properties +values do not follow the contraints defined in `Server`. + +[NOTE] +==== + +For validation to work, the `smallrye-config-validator` dependency is required in the classpath. + +==== diff --git a/content/docs/smallrye-config/3/config/profiles.adoc b/content/docs/smallrye-config/3/config/profiles.adoc new file mode 100644 index 000000000..ca2dd361e --- /dev/null +++ b/content/docs/smallrye-config/3/config/profiles.adoc @@ -0,0 +1,192 @@ += Profiles +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Applications often require different configurations depending on the target environment. For example, the local +development environment may be different from the production environment. Profiles allow for multiple configurations +in the same file or separate files and select between them via a profile name. + +== Profile aware properties + +To be able to set properties with the same name, each property needs to be prefixed with a percentage sign `%` followed +by the profile name and a dot `.` in the syntax `%++{++profile-name}.config.name`: + +.META-INF/microprofile-config.properties +[source,properties] +---- +http.port=8080 +%dev.http.port=8181 +---- + +To activate the profile `dev`, the configuration `smallrye.config.profile=dev` has to be set into any valid +`ConfigSource`. + +Any lookup to the `http.port` property name will first search by the active profile name `%dev.http.port` and then +fallback to `http.port` if no value is present. In this case a lookup to the property `http.port` with the `dev` profile +active, yields the value `8181`. + +[WARNING] +==== + +The profile must be set in one of the primary sources (system properties, environment variables, +`application.properties`, or any other source that does not require configuration) to determine the proper +configuration. + +==== + +== Profile aware files + +Properties for a specific profile may reside in a `microprofile-config-++{++profile}.properties` named file. The previous +example can be expressed as: + +.META-INF/microprofile-config.properties +[source,properties] +---- +http.port=8080 +---- + +.META-INF/microprofile-config-dev.properties +[source,properties] +---- +http.port=8181 +---- + +In this style, the property names in the profile aware file do not need to be prefixed with the profile name. + +[NOTE] +==== + +Properties in the profile aware file have priority over profile aware properties defined in the main file. + +==== + +[WARNING] +==== + +Do not use Profile aware files to set `smallrye.config.profile`. This will not work because the +the profile is required in advance to load the profile aware files. + +==== + +== Priority + +Profile lookups are only valid if the `ConfigSource` has a higher ordinal than a lookup to the regular configuration +name. Consider: + +.main.properties +[source,properties] +---- +config_ordinal=1000 +http.port=8080 +---- + +.profile.properties +[source,properties] +---- +config_ordinal=100 +%dev.http.port=8181 +---- + +Even with the profile `dev` active, the lookup value for `my.prop` is `1234`. This prevents lower ordinal sources to +set a profile property value that cannot be overridden unless the profile property is also overridden. + +== Multiple Profiles + +Multiple Profiles may be active at the same time. The configuration `smallrye.config.profile` accepts a comma-separated +list of profile names: `smallrye.config.profile=common,dev`. Both `common` and `dev` are separate profiles. + +When multiple profiles are active, the rules for profile configuration are the same. If two profiles define the same +configuration, then the last listed profile has priority. Consider: + +[source,properties] +---- +smallrye.config.profile=common,dev + +my.prop=1234 +%common.my.prop=1234 +%dev.my.prop=5678 + +%common.commom.prop=common +%dev.dev.prop=dev +%test.test.prop=test +---- + +Then + +* `common.prop` value is `common` +* `dev.prop` value is `dev` +* `my.prop` value is `5678` +* `test.prop` does not have a value + +It is also possible to define multiple profile properties, with a comma-separated list of profile names: + +[source,properties] +---- +%prod,dev.my.prop=1234 +---- + +The property name `common.prop` is active in both `dev` and `prod` profile. If the same property name exists in +multiple profile properties then, the property name with the most specific profile wins: + +[source,properties] +---- +smallrye.config.profile=dev + +%prod,dev.my.prop=1234 + +%dev.my.prop=5678 +---- + +Then `my.prop` value is `5678`. + +== Parent Profile + +A Parent Profile adds multiple levels of hierarchy to the current profile. The configuration +`smallrye.config.profile.parent` also acccepts a comma-separated list of profile names. + +When the Parent Profile is active, if a property cannot be found in the current active Profile, the config lookup +fallbacks to the Parent Profile. Consider: + +[source,properties] +---- +smallrye.config.profile=dev +smallrye.config.profile.parent=common + +my.prop=1234 +%common.my.prop=0 +%dev.my.prop=5678 + +%common.commom.prop=common +%dev.dev.prop=dev +%test.test.prop=test +---- + +Then + +* `common.prop` value is `common` +* `dev.prop` value is `dev` +* `my.prop` value is `0` +* `test.prop` does not have a value + +[WARNING] +==== + +Do not use Profile aware files to set smallrye.config.profile.parent++`++. This will not work because the +the profile is required in advance to load the profile aware files. + +==== + +=== Multi-level Hierarchy + +The Parent Profile also supports multiple levels of hierarchies: + +[source,properties] +---- +smallrye.config.profile=child +%child.smallrye.config.profile.parent=parent +%parent.smallrye.config.profile.parent=grandparent +%grandparent.smallrye.config.profile.parent=greatgrandparent +%greatgrandparent.smallrye.config.profile.parent=end +---- + +Will load the following profiles in order: `child`, `parent`, `grandparent`, `greatgrandparent`, `end` diff --git a/content/docs/smallrye-config/3/config/secret-keys.adoc b/content/docs/smallrye-config/3/config/secret-keys.adoc new file mode 100644 index 000000000..b0d4cf6b5 --- /dev/null +++ b/content/docs/smallrye-config/3/config/secret-keys.adoc @@ -0,0 +1,126 @@ += Secret Keys +:version-root: ../ +include::{version-root}_attributes.adoc[] + +== Secret Keys Expressions + +In SmallRye Config, a secret configuration may be expressed as `$++{++handler::value}`, where the `handler` is the name of +a `io.smallrye.config.SecretKeysHandler` to decode or decrypt the `value` separated by a double colon `::`. + +It is possible to create a custom `SecretKeysHandler` and provide different ways to decode or decrypt configuration +values. + +A custom `SecretKeysHandler` requires an implementation of `io.smallrye.config.SecretKeysHandler` or +`io.smallrye.config.SecretKeysHandlerFactory`. Each implementation requires registration via the `ServiceLoader` +mechanism, either in `META-INF/services/io.smallrye.config.SecretKeysHandler` or +`META-INF/services/io.smallrye.config.SecretKeysHandlerFactory` files. + +[CAUTION] +==== + +It is not possible to mix Secret Keys Expressions with Property Expressions. + +==== + +=== Crypto + +The `smallrye-config-crypto` artifact contains a few out-of-the-box `SecretKeysHandler`s ready for use. It requires +the following dependency: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.config + smallrye-config-crypto + {version} + +---- + +==== AES/GCM/NoPadding `$++{++aes-gcm-nopadding::...}` + +* The encoding length is 128. +* The secret and the encryption key (without padding) must be base 64 encoded. + +[NOTE] +==== + +.application.properties +[source,properties] +---- +smallrye.config.secret-handler.aes-gcm-nopadding.encryption-key=DDne5obnfH1RSeTg71xSZg + +my.secret=${aes-gcm-nopadding::DLTb_9zxThxeT5iAQqswEl5Dn1ju4FdM9hIyVip35t5V} +---- + +The `$++{++aes-gcm-nopadding::...}` `SecretKeyHandler` requires +`smallrye.config.secret-handler.aes-gcm-nopadding.encryption-key` configuration to state the encryption key to be +used by the `aes-gcm-nopaddin` handler. + +A lookup to `my.secret` will use the `SecretKeysHandler` name `aes-gcm-nopadding` to decode the value +`DJNrZ6LfpupFv6QbXyXhvzD8eVDnDa++_++kTliQBpuzTobDZxlg`. + +==== + +[NOTE] +==== + +It is possible to generate the encrypted secret with the following http://jbang.dev/[JBang] script: + +[source,shell] +---- +jbang https://raw.githubusercontent.com/smallrye/smallrye-config/main/documentation/src/main/docs/config/secret-handlers/encryptor.java -s= -k=` +---- + +==== + +===== Configuration + +[width="100%",cols="90%,5%,5%",options="header",] +|=== +|Configuration Property |Type |Default +|`smallrye.config.secret-handler.aes-gcm-nopadding.encryption-key` + +The encryption key to use to decode secrets encoded by the `AES/GCM/NoPadding` algorithm. |String | +|`"smallrye.config.secret-handler.aes-gcm-nopadding.encryption-key-decode"` + +Decode the encryption key in Base64, if the plain text key was used to encrypt the secret. |boolean |false +|=== + +== Secret Keys Names + +When configuration properties contain passwords or other kinds of secrets, Smallrye Config can hide them to prevent +accidental exposure of such values. + +*This is no way a replacement for securing secrets.* Proper security mechanisms must still be used to secure +secrets. However, there is still the fundamental problem that passwords and secrets are generally encoded simply as +strings. Secret Keys provides a way to "lock" the configuration so that secrets do not appear unless explicitly enabled. + +To mark specific keys as secrets, register an instance of `io.smallrye.config.SecretKeysConfigSourceInterceptor` by +using the interceptor factory as follows: + +[source,java] +---- +public class SecretKeysConfigInterceptorFactory implements ConfigSourceInterceptorFactory { + @Override + public ConfigSourceInterceptor getInterceptor(ConfigSourceInterceptorContext context) { + return new SecretKeysConfigSourceInterceptor(Set.of("secret")); + } +} +---- + +Register the factory so that it can be found at runtime by creating a +`META-INF/services/io.smallrye.config.ConfigSourceInterceptorFactory` file that contains the fully qualified name of +this factory class. + +From this point forward, every lookup to the configuration name `secret` will throw a `SecurityException`. + +Access the Secret Keys using the APIs `io.smallrye.config.SecretKeys#doUnlocked(java.lang.Runnable)` +and `io.smallrye.config.SecretKeys#doUnlocked(java.util.function.Supplier++<++T++>++)`. + +[source,java] +---- +String secretValue = SecretKeys.doUnlocked(() -> { + config.getValue("secret", String.class); +}); +---- + +Secret Keys are only unlocked in the context of `doUnlocked`. Once the execution completes, the secrets become locked +again. diff --git a/content/docs/smallrye-config/3/converters/custom.adoc b/content/docs/smallrye-config/3/converters/custom.adoc new file mode 100644 index 000000000..6e495919c --- /dev/null +++ b/content/docs/smallrye-config/3/converters/custom.adoc @@ -0,0 +1,101 @@ += Custom `Converter` +:version-root: ../ +include::{version-root}_attributes.adoc[] + +It is possible to create a custom `Converter` type as specified in +https://github.com/eclipse/microprofile-config/[MicroProfile Config]. + +A custom `Converter` requires an implementation of `org.eclipse.microprofile.config.spi.Converter`. Each implementation +requires registration via the `ServiceLoader` mechanism in the +`META-INF/services/org.eclipse.microprofile.config.spi.Converter` file. Consider: + +[source,java] +---- +package org.acme.config; + +public class CustomValue { + + private final int number; + + public CustomValue(int number) { + this.number = number; + } + + public int getNumber() { + return number; + } +} +---- + +The corresponding converter can look like: + +[source,java] +---- +package org.acme.config; + +import org.eclipse.microprofile.config.spi.Converter; + +public class CustomValueConverter implements Converter { + + @Override + public CustomValue convert(String value) { + return new CustomValue(Integer.parseInt(value)); + } +} +---- + +And registration in: + +.META-INF/services/org.eclipse.microprofile.config.spi.Converter +[source,properties] +---- +org.acme.config.CustomValue +---- + +[WARNING] +==== + +The custom `Converter` class must be `public`, must have a `public` constructor with no arguments, and must not be +abstract. + +==== + +The `CustomValueConverter` converts the configuration value to the `CustomValue` type automatically. + +[source,java] +---- +Config config = Config.getOrCreate(); +CustomValue value = config.getValue("custom.value", CustomValue.class); +---- + +The `jakarta.annotation.Priority` annotation overrides the `Converter` priority and change converters precedence to fine +tune the execution order. By default, if no `@Priority` is specified by the `Converter`, the converter is registered +with a priority of `100`. Consider: + +[source,java] +---- +package org.acme.config; + +import jakarta.annotation.Priority; +import org.eclipse.microprofile.config.spi.Converter; + +@Priority(150) +public class SecretConverter implements Converter { + + @Override + public CustomValue convert(String value) { + final int secretNumber; + if (value.startsFrom("OBF:")) { + secretNumber = Integer.parseInt(SecretDecoder.decode(value)); + } else { + secretNumber = Integer.parseInt(value); + } + + return new CustomValue(secretNumber); + } +} +---- + +Two `Converter`s, (`CustomValueConverter` and `SecretConverter`) can convert the same type `CustomValue`. Since +`SecretConverter` has a priority of `150`, it will be used instead of a `CustomValueConverter` which has a default +priority of `100` (no annotation). diff --git a/content/docs/smallrye-config/3/extensions/config-events.adoc b/content/docs/smallrye-config/3/extensions/config-events.adoc new file mode 100644 index 000000000..cd43004ac --- /dev/null +++ b/content/docs/smallrye-config/3/extensions/config-events.adoc @@ -0,0 +1,158 @@ += Config Events +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The Config Events extension allows you to fire change events on Config Sources. + +== Usage + +To use the Config Events, add the following to your Maven `pom.xml`: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.config + smallrye-config-events + {version} + +---- + +== Events + +The CDI Event is a `ChangeEvent` and contains the following fields: + +* String key +* Optional++<++String++>++ oldValue +* String newValue +* Type type +* String fromSource + +The `ChangeEvent` can be of any of the following types: + +* NEW - When you create a new key and value (i.e. the key does not exist anywhere in any config source) +* UPDATE - When you update a value of an existing key (i.e. the key and value exist somewhere in a config source) +* REMOVE - When you remove the value from the source (and that changed the overall config) + +=== Observing Events + +You can listen to all or some of these events, filtering by `type` and/or `key` and/or `source`, example: + +[source,java] +---- +// Getting all config event +public void all(@Observes ChangeEvent changeEvent){ + log.log(Level.SEVERE, "ALL: Received a config change event: {0}", changeEvent); +} + +// Get only new values +public void newValue(@Observes @TypeFilter(Type.NEW) ChangeEvent changeEvent){ + log.log(Level.SEVERE, "NEW: Received a config change event: {0}", changeEvent); +} + +// Get only override values +public void overrideValue(@Observes @TypeFilter(Type.UPDATE) ChangeEvent changeEvent){ + log.log(Level.SEVERE, "UPDATE: Received a config change event: {0}", changeEvent); +} + +// Get only revert values +public void revertValue(@Observes @TypeFilter(Type.REMOVE) ChangeEvent changeEvent){ + log.log(Level.SEVERE, "REMOVE: Received a config change event: {0}", changeEvent); +} + +// Getting all config event when key is some.key +public void allForKey(@Observes @KeyFilter("some.key") ChangeEvent changeEvent){ + log.log(Level.SEVERE, "ALL for key [some.key]: Received a config change event: {0}", changeEvent); +} + +// Getting all config event when key is some.key for new events +public void newForKey(@Observes @TypeFilter(Type.NEW) @KeyFilter("some.key") ChangeEvent changeEvent){ + log.log(Level.SEVERE, "NEW for key [some.key]: Received a config change event: {0}", changeEvent); +} + +// Getting all config event when key is some.key for override events +public void overrideForKey(@Observes @TypeFilter(Type.UPDATE) @KeyFilter("some.key") ChangeEvent changeEvent){ + log.log(Level.SEVERE, "UPDATE for key [some.key]: Received a config change event: {0}", changeEvent); +} + +// Getting all config event when key is some.key for revert events +public void revertForKey(@Observes @TypeFilter(Type.REMOVE) @KeyFilter("some.key") ChangeEvent changeEvent){ + log.log(Level.SEVERE, "REMOVE for key [some.key]: Received a config change event: {0}", changeEvent); +} + +// Getting all config events for a certain source +public void allForSource(@Observes @SourceFilter("MemoryConfigSource") ChangeEvent changeEvent){ + log.log(Level.SEVERE, "ALL for source [MemoryConfigSource]: Received a config change event: {0}", changeEvent); +} + +// Getting all config events for a certain source +public void allForSourceAndKey(@Observes @SourceFilter("MemoryConfigSource") @KeyFilter("some.key") ChangeEvent changeEvent){ + log.log(Level.SEVERE, "ALL for source [MemoryConfigSource] and for key [some.key]: Received a config change event: {0}", changeEvent); +} + +// Getting all config events for a certain source +public void overrideForSourceAndKey(@Observes @TypeFilter(Type.UPDATE) @SourceFilter("MemoryConfigSource") @KeyFilter("some.key") ChangeEvent changeEvent){ + log.log(Level.SEVERE, "UPDATE for source [MemoryConfigSource] and for key [some.key]: Received a config change event: {0}", changeEvent); +} +---- + +Note: You can filter by including the `@TypeFilter` and/or the `@KeyFilter` and/or the `@SourceFilter`. + +=== Pattern matching on field. + +You might want to listen for fields that match a certain regex. + +Example, listen to all keys that starts with `some.`: + +[source,java] +---- +@RegexFilter("^some\\..+") +public void allForPatternMatchOnKey(@Observes ChangeEvent changeEvent){ + log.log(Level.SEVERE, "Pattern match on key: Received a config change event: {0}", changeEvent); +} +---- + +By default, it will match on `key`, however you also listen on another field, for example, listen to all `oldValue` +that starts with `some.`: + +[source,java] +---- +@RegexFilter(onField = Field.oldValue, value = "^some\\..+") +public void allForPatternMatchOnOldValue(@Observes ChangeEvent changeEvent){ + log.log(Level.SEVERE, "Pattern match on old value: Received a config change event: {0}", changeEvent); +} +---- + +You can Match on the following fields of the `ChangeEvent` object: + +* key +* oldValue +* newValue +* fromSource + +== Implementing Events in a ConfigSource + +The `ChangeEventNotifier` allows you to detect changes and fire the appropriate events. + +To use it in your own source: + +* Get a snapshot of the properties before the change. +* Get a snapshot of the properties after the change. +* Call `detectChangesAndFire` method: + +Example: + +[source,java] +---- +Map before = new HashMap<>(configSource.getProperties()); +memoryConfigSource.getProperties().remove(key); +Map after = new HashMap<>(configSource.getProperties()); +ChangeEventNotifier.getInstance().detectChangesAndFire(before, after,configSource.getName()); +---- + +or if you know the change and do not need detection: + +[source,java] +---- +configSource.getProperties().remove(key); +ChangeEventNotifier.getInstance().fire(new ChangeEvent(Type.REMOVE,key,getOptionalOldValue(oldValue),null,configSource.getName())); +---- diff --git a/content/docs/smallrye-config/3/extensions/config-source-injection.adoc b/content/docs/smallrye-config/3/extensions/config-source-injection.adoc new file mode 100644 index 000000000..7d79eafd3 --- /dev/null +++ b/content/docs/smallrye-config/3/extensions/config-source-injection.adoc @@ -0,0 +1,44 @@ += Config Source Injection +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The Config Source Injection extension allows you to use CDI injection to inject a ConfigSource by name in your CDI +aware beans, or by looking it up programatically in the CDI `BeanManager`. + +== Usage + +To use the Config Source Injection, add the following to your Maven `pom.xml`: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.config + smallrye-config-source-injection + {version} + +---- + +=== Injecting Sources + +You can inject a `ConfigSource` by referencing it by name: + +[source,java] +---- +@Inject +@Name("MemoryConfigSource") +private ConfigSource memoryConfigSource; + +@Inject +@Name("SysPropConfigSource") +private ConfigSource systemPropertiesConfigSource; +---- + +You can also get a Map of all config sources. The map key holds the `ConfigSource` name and the map value the +`ConfigSource`: + +[source,java] +---- +@Inject +@ConfigSourceMap +private Map configSourceMap; +---- diff --git a/content/docs/smallrye-config/3/extensions/fallback.adoc b/content/docs/smallrye-config/3/extensions/fallback.adoc new file mode 100644 index 000000000..05af7251d --- /dev/null +++ b/content/docs/smallrye-config/3/extensions/fallback.adoc @@ -0,0 +1,59 @@ += Fallback +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The `io.smallrye.config.FallbackConfigSourceInterceptor` allows to fall back to another configuration name, by +providing a transformation function or just a simple key value map. + +When a configuration name does not exist, there might be another configuration name that the config can +fall back to provide the same expected behavior. The fallback function is only applied if the original resolved +configuration name is not found and resolved to the fallback name. + +[source,java] +---- +package org.acme.config; + +import io.smallrye.config.FallbackConfigSourceInterceptor; + +public class MicroProfileConfigFallbackInterceptor extends FallbackConfigSourceInterceptor { + public MicroProfileConfigFallbackInterceptor() { + super(name -> name.startsWith("mp.config") ? + name.replaceAll("mp\\.config", "smallrye.config") : + name); + } +} +---- + +And registration in: + +.META-INF/services/io.smallrye.config.ConfigSourceInterceptor +[source,properties] +---- +org.acme.config.MicroProfileConfigFallbackInterceptor +---- + +The `MicroProfileConfigFallbackInterceptor` can fallback configuration names in the `mp.config` namespace +to the `smallrye.config` namespace. + +[NOTE] +==== + +.application.properties +[source,properties] +---- +mp.config.profile=test +smallrye.config.profile=prod +---- + +A lookup to `mp.config.profile` returns the value `test`. + +.application.properties +[source,properties] +---- +smallrye.config.profile=prod +---- + +A lookup to `mp.config.profile` returns the value `prod`. The config is not able to find a value for +`mp.config.profile`, so the interceptor fallbacks and lookups the value of `smallrye.config.profile`. + +==== diff --git a/content/docs/smallrye-config/3/extensions/interceptors.adoc b/content/docs/smallrye-config/3/extensions/interceptors.adoc new file mode 100644 index 000000000..78d60980a --- /dev/null +++ b/content/docs/smallrye-config/3/extensions/interceptors.adoc @@ -0,0 +1,84 @@ += Interceptors +:version-root: ../ +include::{version-root}_attributes.adoc[] + +SmallRye Config provides an interceptor chain that hooks into the configuration values resolution. This is useful to +implement features like xref:config/profiles.adoc[Profiles] , xref:config/expressions.adoc[Property Expressions], or just +logging to find out where the config value was loaded from. + +An interceptor can be created by implementing the +https://github.com/smallrye/smallrye-config/blob/main/implementation/src/main/java/io/smallrye/config/ConfigSourceInterceptor.java[ConfigSourceInterceptor] +interface. + +An interceptor requires an implementation of `io.smallrye.config.ConfigSourceInterceptor`. Each implementation requires +registration via the `ServiceLoader` mechanism in the `META-INF/services/io.smallrye.config.ConfigSourceInterceptor` +file. Alternatively, interceptors may be registered via the Programmatic API in +`SmallRyeConfigBuilder#withInterceptors`. + +The `io.smallrye.config.ConfigSourceInterceptor` is able to intercept the resolution of a configuration name with the +method `ConfigValue getValue(ConfigSourceInterceptorContext context, String name)`. The `ConfigSourceInterceptorContext` +is used to proceed with the interceptor chain. The chain can be short-circuited by returning an instance of +`io.smallrye.config.ConfigValue`. The `ConfigValue` objects hold information about the key name, value, config source +origin and ordinal. + +[NOTE] +==== + +The interceptor chain is applied before any conversion is performed on the configuration value. + +==== + +[source,java] +---- +package org.acme.config; + +import static io.smallrye.config.SecretKeys.doLocked; + +import jakarta.annotation.Priority; + +import io.smallrye.config.ConfigSourceInterceptor; +import io.smallrye.config._private.ConfigLogging; + +@Priority(Priorities.LIBRARY + 200) +public class LoggingConfigSourceInterceptor implements ConfigSourceInterceptor { + private static final long serialVersionUID = 367246512037404779L; + + @Override + public ConfigValue getValue(final ConfigSourceInterceptorContext context, final String name) { + ConfigValue configValue = doLocked(() -> context.proceed(name)); + if (configValue != null) { + ConfigLogging.log.lookup(configValue.getName(), configValue.getLocation(), configValue.getValue()); + } else { + ConfigLogging.log.notFound(name); + } + return configValue; + } +} +---- + +And registration in: + +.META-INF/services/io.smallrye.config.ConfigSourceInterceptor +[source,properties] +---- +org.acme.config.LoggingConfigSourceInterceptor +---- + +The `LoggingConfigSourceInterceptor` logs looks up configuration names in the provided logging platform. The log +information includes config name and value, the config source origin and location if exists. + +Interceptors may also be created with an implementation of `io.smallrye.config.ConfigSourceInterceptorFactory`. Each +implementation requires registration via the `ServiceLoader` mechanism in the +`META-INF/services/io.smallrye.config.ConfigSourceInterceptorFactory` file. Alternatively, interceptors factories may +be registered via the Programmatic API in `SmallRyeConfigBuilder#withInterceptorFactories`. + +The `ConfigSourceInterceptorFactory` can initialize an interceptor with access to the current chain +(so it can be used to configure the interceptor and retrieve configuration values) and set the priority. + +A `ConfigSourceInterceptor` implementation class can specify a priority by way of the standard +`jakarta.annotation.Priority` annotation. If no priority is explicitly assigned, the default priority value of +`io.smallrye.config.Priorities.APPLICATION` is assumed. If multiple interceptors are registered with the same priority, +then their execution order may be non-deterministic. + +A collection of built-in priority constants can be found in `io.smallrye.config.Priorities`. It is recommended to +use `io.smallrye.config.Priorities.APPLICATION` as a baseline for user defined interceptors. diff --git a/content/docs/smallrye-config/3/extensions/logging.adoc b/content/docs/smallrye-config/3/extensions/logging.adoc new file mode 100644 index 000000000..3a42e6573 --- /dev/null +++ b/content/docs/smallrye-config/3/extensions/logging.adoc @@ -0,0 +1,15 @@ += Logging +:version-root: ../ +include::{version-root}_attributes.adoc[] + +== LoggingConfigSourceInterceptor + +The `io.smallrye.config.LoggingConfigSourceInterceptor` logs lookups of configuration names in the provided logging +platform. The log information includes config name and value, the config source origin and location if it exists. + +The log is done as `debug`, so the debug threshold must be set to `debug` for the `io.smallrye.config` appender to +display the logs. + +This requires registration via the `ServiceLoader` mechanism in the +`META-INF/services/io.smallrye.config.ConfigSourceInterceptorFactory` file of the +`io.smallrye.config.LoggingConfigSourceInterceptor` interceptor. diff --git a/content/docs/smallrye-config/3/extensions/relocate.adoc b/content/docs/smallrye-config/3/extensions/relocate.adoc new file mode 100644 index 000000000..e33ee4f6a --- /dev/null +++ b/content/docs/smallrye-config/3/extensions/relocate.adoc @@ -0,0 +1,51 @@ += Relocate +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The `io.smallrye.config.RelocateConfigSourceInterceptor` allows to relocate a configuration name to another name, by +providing a transformation function or just a simple key value map. + +When a configuration key is renamed, lookup needs to happen on the new name, but also on the old name if the +config sources are not updated yet. The relocation function gives priority to the new resolved configuration name or +resolves to the old name if no value is found under the new relocation name. + +[source,java] +---- +package org.acme.config; + +import io.smallrye.config.RelocateConfigSourceInterceptor; + +public class MicroProfileConfigRelocateInterceptor extends RelocateConfigSourceInterceptor { + public MicroProfileConfigRelocateInterceptor() { + super(name -> name.startsWith("mp.config") ? + name.replaceAll("mp\\.config", "smallrye.config") : + name); + } +} +---- + +And registration in: + +.META-INF/services/io.smallrye.config.ConfigSourceInterceptor +[source,properties] +---- +org.acme.config.MicroProfileConfigRelocateInterceptor +---- + +The `MicroProfileConfigRelocateInterceptor` can relocate configuration names in the `mp.config` namespace +to the `smallrye.config` namespace. + +[NOTE] +==== + +.application.properties +[source,properties] +---- +mp.config.profile=test +smallrye.config.profile=prod +---- + +A lookup to `mp.config.profile` returns the value `prod`. The config finds a valid value in the relocated name +`smallrye.config.profile`, so the interceptor will use this value instead of the one in `mp.config.profile`. + +==== diff --git a/content/docs/smallrye-config/3/index.adoc b/content/docs/smallrye-config/3/index.adoc new file mode 100644 index 000000000..dca56db0f --- /dev/null +++ b/content/docs/smallrye-config/3/index.adoc @@ -0,0 +1,97 @@ += SmallRye Config +include::_attributes.adoc[] + +*SmallRye Config* is a library that provides a way to configure applications, frameworks and containers. It is used +in applications servers like https://wildfly.org/[WildFly], https://openliberty.io[Open Liberty] and +https://tomee.apache.org[TomEE] or frameworks like https://quarkus.io[Quarkus]. It can also be used completely +standalone in any Java application, which makes it a very flexible library. + +It follows the https://github.com/eclipse/microprofile-config/[MicroProfile Config] specification to provide +the initial config foundations and expands with it own concepts to cover a wide range of use cases observed in the +configuration space. + +== Use SmallRye Config in a Java application + +Add the _dependency_ to your project using your preferred build tool: + +[.tabs] +==== + +Maven:: ++ +-- + +[source,xml,subs="attributes+"] +---- + + io.smallrye.config + smallrye-config + {version} + +---- + +-- + +Gradle (Groovy):: ++ +-- + +[source,groovy,subs="attributes+"] +---- +implementation 'io.smallrye.config:smallrye-config:{version}' +---- + +-- + +Gradle (Kotlin):: ++ +-- + +[source,kotlin,subs="attributes+"] +---- +implementation("io.smallrye.config:smallrye-config:{version}") +---- + +-- + +JBang:: ++ +-- + +[source,java,subs="attributes+"] +---- +//DEPS io.smallrye.config:smallrye-config:{version} +---- + +-- + +==== + +And retrieve a `SmallRyeConfig` instance with: + +[source,java] +---- +Config config = io.smallrye.config.Config.getOrCreate(); +---- + +[NOTE] +==== + +The `SmallRyeConfig` instance will be created and registered to the context class loader if no such configuration +is already created and registered. + +==== + +Or build your own: + +[source,java] +---- +SmallRyeConfig config = new SmallRyeConfigBuilder().build(); +---- + +[NOTE] +==== + +`io.smallrye.config.Config` is the entry point to all the config capabilities provided by SmallRye Config. + +==== diff --git a/content/docs/smallrye-config/latest/_attributes.adoc b/content/docs/smallrye-config/latest/_attributes.adoc new file mode 100644 index 000000000..6594580bf --- /dev/null +++ b/content/docs/smallrye-config/latest/_attributes.adoc @@ -0,0 +1,21 @@ +:project-version: 4.0.0 +// Attributes shared by every page. smallrye.io overrides the ones it needs when it publishes. +// Pages in subdirectories set :version-root: (../ per level) before including this file. +ifndef::version-root[:version-root:] +:idprefix: +:idseparator: - +:sectanchors: +:examples-dir: {version-root}_examples +:partialsdir: {version-root}_partials +:project-name: SmallRye Config +:javadoc-version: latest +:version: {project-version} +:generated-dir: {version-root}_generated + +// Added by smallrye.io (tools/docs/Docs.java sync) +:javadoc-version: 4.0.0 +:relfileprefix: /docs/smallrye-config/latest/ +:imagesdir: /docs/smallrye-config/latest +:docs-project: smallrye-config +:docs-version: latest +:docs-ref: 4.0.0 diff --git a/content/docs/smallrye-config/latest/_nav.yml b/content/docs/smallrye-config/latest/_nav.yml new file mode 100644 index 000000000..6a14b50be --- /dev/null +++ b/content/docs/smallrye-config/latest/_nav.yml @@ -0,0 +1,66 @@ +title: SmallRye Config +nav: +- title: Home + page: index +- title: Config + items: + - title: Getting Started + page: config/getting-started + - title: Expressions + page: config/expressions + - title: Profiles + page: config/profiles + - title: Environment Variables + page: config/environment-variables + - title: Indexed Properties + page: config/indexed-properties + - title: ConfigValue + page: config/config-value + - title: Mappings + page: config/mappings + - title: Instance Builder + page: config/instance-builder + - title: Map Support + page: config/map-support + - title: Secret Keys + page: config/secret-keys + - title: Customizer + page: config/customizer + - title: Configuration Reference + page: config/configuration +- title: Config Sources + items: + - title: Custom + page: config-sources/custom + - title: Factories + page: config-sources/factories + - title: Locations + page: config-sources/locations + - title: YAML + page: config-sources/yaml + - title: TOML + page: config-sources/toml + - title: JSON + page: config-sources/json + - title: FileSystem + page: config-sources/filesystem + - title: ZooKeeper + page: config-sources/zookeeper + - title: HOCON + page: config-sources/hocon + - title: KeyStore + page: config-sources/keystore +- title: Converters + items: + - title: Custom + page: converters/custom +- title: Extensions + items: + - title: Interceptors + page: extensions/interceptors + - title: Fallback + page: extensions/fallback + - title: Relocate + page: extensions/relocate + - title: Logging + page: extensions/logging diff --git a/content/docs/smallrye-config/latest/assets/smallrye-icon.svg b/content/docs/smallrye-config/latest/assets/smallrye-icon.svg new file mode 100644 index 000000000..c10b011e0 --- /dev/null +++ b/content/docs/smallrye-config/latest/assets/smallrye-icon.svg @@ -0,0 +1,21 @@ + + + + + SmallRye Icon + + + + + + + + + diff --git a/content/docs/smallrye-config/latest/config-sources/custom.adoc b/content/docs/smallrye-config/latest/config-sources/custom.adoc new file mode 100644 index 000000000..bd9cc1444 --- /dev/null +++ b/content/docs/smallrye-config/latest/config-sources/custom.adoc @@ -0,0 +1,166 @@ += Config Sources +:version-root: ../ +include::{version-root}_attributes.adoc[] + +== Custom `ConfigSource` + +It’s possible to create a custom ConfigSource as specified in +https://github.com/eclipse/microprofile-config/[MicroProfile Config]. + +With a Custom `ConfigSource` it is possible to read additional configuration values and add them to the `Config` +instance in a defined ordinal. This allows overriding values from other sources or falling back to other values. + +A custom `ConfigSource` requires an implementation of `org.eclipse.microprofile.config.spi.ConfigSource` or +`org.eclipse.microprofile.config.spi.ConfigSourceProvider`. Each implementation requires registration via the +`ServiceLoader` mechanism, either in `META-INF/services/org.eclipse.microprofile.config.spi.ConfigSource` or +`META-INF/services/org.eclipse.microprofile.config.spi.ConfigSourceProvider` files. + +Consider a simple in-memory ConfigSource: + +[source,java] +---- +package org.acme.config; + +import org.eclipse.microprofile.config.spi.ConfigSource; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +public class InMemoryConfigSource implements ConfigSource { + private static final Map configuration = new HashMap<>(); + + static { + configuration.put("my.prop", "1234"); + } + + @Override + public int getOrdinal() { + return 350; + } + + @Override + public Set getPropertyNames() { + return configuration.keySet(); + } + + @Override + public String getValue(final String propertyName) { + return configuration.get(propertyName); + } + + @Override + public String getName() { + return InMemoryConfigSource.class.getSimpleName(); + } +} +---- + +And registration in: + +.META-INF/services/org.eclipse.microprofile.config.spi.ConfigSource +[source,properties] +---- +org.acme.config.InMemoryConfigSource +---- + +The `InMemoryConfigSource` will be ordered between the `System properties` config source, and the +`Environment variables` config source due to the `350` ordinal. + +In this case, `my.prop` from `InMemoryConfigSource` will only be used if the `Config` instance is unable to find a +value in `System Properties`, ignoring all the other lower ordinal config sources. + +== `ConfigSourceFactory` + +Another way to create a `ConfigSource` is via the SmallRye Config `io.smallrye.config.ConfigSourceFactory` API. The +difference between the SmallRye Config factory and the standard way to create a `ConfigSource` as specified in +https://github.com/eclipse/microprofile-config/[MicroProfile Config], is the factory ability to provide a context +with access to the current configuration. + +Each implementation of `io.smallrye.config.ConfigSourceFactory` requires registration via the `ServiceLoader` mechanism +in the `META-INF/services/io.smallrye.config.ConfigSourceFactory` file. + +[source,java] +---- +package org.acme.config; + +import java.util.Collections; +import java.util.OptionalInt; + +import org.eclipse.microprofile.config.spi.ConfigSource; + +import io.smallrye.config.ConfigSourceContext; +import io.smallrye.config.ConfigSourceFactory; +import io.smallrye.config.ConfigValue; +import io.smallrye.config.PropertiesConfigSource; + +public class URLConfigSourceFactory implements ConfigSourceFactory { + @Override + public Iterable getConfigSources(final ConfigSourceContext context) { + final ConfigValue value = context.getValue("config.url"); + if (value == null || value.getValue() == null) { + return Collections.emptyList(); + } + + try { + return Collections.singletonList(new PropertiesConfigSource(new URL(value.getValue()))); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public OptionalInt getPriority() { + return OptionalInt.of(290); + } +} +---- + +And registration in: + +.META-INF/services/io.smallrye.config.ConfigSourceFactory +[source,properties] +---- +org.acme.config.URLConfigSourceFactory +---- + +By implementing `io.smallrye.config.ConfigSourceFactory`, a list of `ConfigSource` may be provided via the +`Iterable++<++ConfigSource++>++ getConfigSources(ConfigSourceContext context)` method. The `ConfigSourceFactory` may also +assign a priority by overriding the method `OptionalInt getPriority()`. The priority value is used to sort multiple +`io.smallrye.config.ConfigSourceFactory` (if found). Higher the value, higher the priority. + +[WARNING] +==== + +The `io.smallrye.config.ConfigSourceFactory` priority does not affect the `ConfigSource` ordinal. These are sorted +independently. + +==== + +When the Factory is initializing, the provided `ConfigSourceContext` may call the method +`ConfigValue getValue(String name)`. This method looks up configuration names in all `ConfigSource`s that were already +initialized by the `Config` instance, including sources with lower ordinals than the ones defined in the +`ConfigSourceFactory`. This means that a `Config` instance is initialized in two steps: first all `ConfigSource` and +`ConfigSourceProvider` and then the `ConfigSourceFactory`. Only configuation values found in the first step are avaible +to the ++`++ConfigSourceFactory. + +The `ConfigSource` list provided by a `ConfigSourceFactory` is not taken into consideration to +configure other sources produced by a lower priority `ConfigSourceFactory`. + +== Override `ConfigSource` ordinal + +The special configuration property name `config++_++ordinal` can be set in any `ConfigSource` to override its default +ordinal. + +For instance, setting the system property `-Dconfig++_++ordinal=200` will override the ordinal for the `System properties` +config source and move it to be looked up after the `Environment Variables` config source. + +== Properties + +The https://github.com/smallrye/smallrye-config/blob/main/implementation/src/main/java/io/smallrye/config/PropertiesConfigSource.java[PropertiesConfigSource] +creates a `ConfigSource` from Java `Properties`, `Map++<++String, String++>++` objects or a `.properties` file (referenced by +its URL). + +== `.env` + +The https://github.com/smallrye/smallrye-config/blob/main/implementation/src/main/java/io/smallrye/config/DotEnvConfigSourceProvider.java[DotEnvConfigSourceProvider] create a `ConfigSource` from a `.env` file. diff --git a/content/docs/smallrye-config/latest/config-sources/factories.adoc b/content/docs/smallrye-config/latest/config-sources/factories.adoc new file mode 100644 index 000000000..a3c26281c --- /dev/null +++ b/content/docs/smallrye-config/latest/config-sources/factories.adoc @@ -0,0 +1,130 @@ += Config Source Factory +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Another way to create a `ConfigSource` is via the `ConfigSourceFactory`. The difference between the SmallRye Config +Factory and the standard way to make a `ConfigSource` as specified in MicroProfile Config is the Factory's ability to +provide a context with access to the available configuration. With the `ConfigSourceFactory` it is possible to +bootstrap a `ConfigSource` that configures itself with previously initialized `ConfigSource's`. + +By implementing +https://github.com/smallrye/smallrye-config/blob/main/implementation/src/main/java/io/smallrye/config/ConfigSourceFactory.java[ConfigSourceFactory], +a list of `ConfigSource's` may be provided via the `Iterable++<++ConfigSource++>++ getConfigSources(ConfigSourceContext context)` method. The `ConfigSourceFactory` may also +assign a priority by overriding the default method `OptionalInt getPriority()`. The priority only sorts the factories +during initialization. After initialization, the provided `ConfigSources` will use their own ordinal and sorted with +all `ConfigSources` available in the `Config` instance. + +When the Factory initializes, the provided `ConfigSourceContext` may call the method +`ConfigValue getValue(String name)`. This method looks up configuration names in all `ConfigSource's` already +initialized by the `Config` instance, including sources with lower ordinals than the ones defined in the +`ConfigSourceFactory`. The `ConfigSourceFactory` does not consider `ConfigSources's` provided by other +`ConfigSourceFactory's` (the priority does not matter). + +Registration of a `ConfigSourceFactory` is done via the `ServiceLoader` mechanism by providing the +implementation classes in a `META-INF/services/io.smallrye.config.ConfigSourceFactory` file. Alternatively, factories +may be registered via the Programmatic API in `SmallRyeConfigBuilder#withSources`. + +A `ConfigSourceFactory` requires an implementation of `io.smallrye.config.ConfigSourceFactory`. Each implementation +requires registration via the `ServiceLoader` mechanism in the +`META-INF/services/io.smallrye.config.ConfigSourceFactory` file. Alternatively, interceptors may be registered via the +Programmatic API in `SmallRyeConfigBuilder#withSources`. + +[source,java] +---- +package org.acme.config + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Collections; +import java.util.List; + +import org.eclipse.microprofile.config.spi.ConfigSource; + +import io.smallrye.config._private.ConfigMessages; +import io.smallrye.config.PropertiesConfigSource; +import io.smallrye.config.ConfigSourceContext; +import io.smallrye.config.ConfigSourceFactory; +import io.smallrye.config.ConfigValue; + +public class FileSystemConfigSourceFactory implements ConfigSourceFactory { + @Override + public Iterable getConfigSources(final ConfigSourceContext context) { + final ConfigValue value = context.getValue("org.acme.config.file.locations"); + if (value == null || value.getValue() == null) { + return Collections.emptyList(); + } + + try { + return List.of(new PropertiesConfigSource(toURL(value.getValue()), 250)); + } catch (IOException e) { + return Collections.emptyList(); + } + } + + private URL toURL(final String value) { + try { + return new URI(value).toURL(); + } catch (URISyntaxException | MalformedURLException e) { + throw ConfigMessages.msg.uriSyntaxInvalid(e, value); + } + } +} +---- + +And registration in: + +.META-INF/services/io.smallrye.config.ConfigSourceFactory +[source,properties] +---- +org.acme.config.FileSystemConfigSourceFactory +---- + +The `FileSystemConfigSourceFactory` look ups the configuration value for `org.acme.config.file.locations`, and uses it +to set up an additional `ConfigSource`. + +== `ConfigurableConfigSourceFactory` + +A `ConfigurableConfigSourceFactory` accepts a `@ConfigMapping` interface to configure the `ConfigSource`, removing the +need to look up configuration values manually via `ConfigSourceContext`: + +[source,java] +---- +@ConfigMapping(prefix = "org.acme.config.file") +interface FileSystemConfig { + List locations(); +} +---- + +[source,java] +---- +public class FileSystemConfigurableConfigSourceFactory implements ConfigurableConfigSourceFactory { + @Override + public Iterable getConfigSources(ConfigSourceContext context, FileSystemConfig config) { + List sources = new ArrayList<>(); + for (URL url : config.locations()) { + try { + sources.add(new PropertiesConfigSource(url, 250)); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + return sources; + } +} +---- + +And registration in: + +.META-INF/services/io.smallrye.config.ConfigSourceFactory +[source,properties] +---- +org.acme.config.FileSystemConfigurableConfigSourceFactory +---- + +The `ConfigMapping` interface is automatically populated from all `ConfigSource`s already initialized at the time the +factory runs. Values are resolved using the same two-step initialization as a regular `ConfigSourceFactory`: sources +from `ConfigSource` / `ConfigSourceProvider` registrations are available, but sources from other +`ConfigSourceFactory` implementations are not. diff --git a/content/docs/smallrye-config/latest/config-sources/filesystem.adoc b/content/docs/smallrye-config/latest/config-sources/filesystem.adoc new file mode 100644 index 000000000..66e16db05 --- /dev/null +++ b/content/docs/smallrye-config/latest/config-sources/filesystem.adoc @@ -0,0 +1,51 @@ += FileSystem Config Source +:version-root: ../ +include::{version-root}_attributes.adoc[] + +This Config Source loads configuration values for each file found in a directory. Each file corresponds to a single +property, where the file name is the configuration property name and the file content the configuration value. + +For instance, if a directory structure looks like: + +.... +foo/ +|__num.max +|__num.size +.... + +The `FileSystem` Config Source will provide 2 properties: + +* num.max +* num.size + +[WARNING] +==== + +Nested directories are not supported. + +==== + +This Config Source can be used to read configuration from +https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap[Kubernetes ConfigMap]. Check the +https://github.com/smallrye/smallrye-config/blob/main/examples/configmap/README.adoc[Kubernetes ConfigMap ConfigSource Example]. + +The same mapping rules as defined for environment variables are applied, so the `FileSystem` Config Source will search +for a given property name `num.max`: + +* Exact match (`num.max`) +* Replace each character that is neither alphanumeric nor ++_++ with ++_++ (`num++_++max`) +* Replace each character that is neither alphanumeric nor ++_++ with ++_++; then convert the name to upper case (`NUM++_++MAX`) + +The following dependency is required in the classpath to use the `FileSystem` Config Source: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.config + smallrye-config-source-file-system + {version} + +---- + +The configuration property `smallrye.config.source.file.locations` sets the directory paths to look up the files. It +accepts multiple locations separated by a comma and each must represent a valid URI to a directory. diff --git a/content/docs/smallrye-config/latest/config-sources/hocon.adoc b/content/docs/smallrye-config/latest/config-sources/hocon.adoc new file mode 100644 index 000000000..23b29b437 --- /dev/null +++ b/content/docs/smallrye-config/latest/config-sources/hocon.adoc @@ -0,0 +1,46 @@ += HOCON Config Source +:version-root: ../ +include::{version-root}_attributes.adoc[] + +This Config Source allows to use the https://github.com/lightbend/config/blob/main/HOCON.md[HOCON] file format to +load configuration values. The HOCON Config Source loads the +configuration from the file `META-INF/microprofile-config.conf`. It has a lower ordinal (`50`) than the +`microprofile-config.properties`. + +The following dependency is required in the classpath to use the HOCON Config Source: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.config + smallrye-config-source-hocon + {version} + +---- + +Expressions defined as `$++{++value}` (unquoted) are resolved internally by the HOCON Config Source as described in the +https://github.com/lightbend/config/blob/main/HOCON.md#substitutions[HOCON Substitutions] documentation. Quoted +Expressions defined as `"$++{++value}"` are resolved by xref:config/expressions.adoc[SmallRye Config Property Expressions]. + +Consider: + +*hocon.conf* + +[source,conf] +---- +{ + foo: "bar", + hocon: ${foo}, + config: "${foo}" +} +---- + +*application.properties* + +[source,properties] +---- +config_ordinal=1000 +foo=baz +---- + +The value of `hocon` is `bar` and the value of `config` is `baz` (if the properties source has a higher ordinal). diff --git a/content/docs/smallrye-config/latest/config-sources/json.adoc b/content/docs/smallrye-config/latest/config-sources/json.adoc new file mode 100644 index 000000000..54fe1a808 --- /dev/null +++ b/content/docs/smallrye-config/latest/config-sources/json.adoc @@ -0,0 +1,6 @@ += JSON Config Source +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The xref:config-sources/yaml.adoc[YAML Config Source] also accepts the JSON format as its contents. The configuration file +`META-INF/microprofile-config.yaml`, still requires to use the `yaml` extension. diff --git a/content/docs/smallrye-config/latest/config-sources/keystore.adoc b/content/docs/smallrye-config/latest/config-sources/keystore.adoc new file mode 100644 index 000000000..19c2dac49 --- /dev/null +++ b/content/docs/smallrye-config/latest/config-sources/keystore.adoc @@ -0,0 +1,67 @@ += KeyStore Config Source +:version-root: ../ +include::{version-root}_attributes.adoc[] + +This Config Source allows to use a Java `KeyStore` to load configuration values. It uses an ordinal of `100`. + +The following dependency is required in the classpath to use the KeyStore Config Source: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.config + smallrye-config-source-keystore + {version} + +---- + +== Create a KeyStore + +The following command creates a simple KeyStore + +[source,bash] +---- +keytool -importpass -alias my.secret -keystore keystore -storepass secret -storetype PKCS12 -v +---- + +The `-alias my.secret` stores the configuration property name `my.secret` in the KeyStore. The command will +interactively ask for the value to be stored in the KeyStore. + +== Read the KeyStore + +The KeyStore Config Source supports reading multiple keystore files: + +[source,properties] +---- +smallrye.config.source.keystore.one.path=keystore-one +smallrye.config.source.keystore.one.password=password + +smallrye.config.source.keystore.two.path=keystore-two +smallrye.config.source.keystore.two.password=password +---- + +The names are arbitrary and can be any name. The name `one` and `two` are used to distinguish both KeyStores. + +If a stored configuration property requires a xref:config/secret-keys.adoc[Secret Handler] to decode a value, set +the handler name with `smallrye.config.source.keystore."name".handler`. + +== Configuration + +[width="100%",cols="95%,2%,3%",options="header",] +|=== +|Configuration Property |Type |Default +|`smallrye.config.source.keystore."name".path` + +The KeyStore path. |String | +|`smallrye.config.source.keystore."name".password` + +The KeyStore password. |String | +|`smallrye.config.source.keystore."name".type` + +The KeyStore type. |String |`PKCS12` +|`smallrye.config.source.keystore."name".handler` + +An Optional secret keys handler. |String | +|`smallrye.config.source.keystore."name".aliases."key".name` + +An Optional aliases key name. |String | +|`smallrye.config.source.keystore."name".aliases."key".password` + +An Optional aliases key password. |String | +|`smallrye.config.source.keystore."name".aliases."key".handler` + +An Optional aliases key secret keys handler. |String | +|=== diff --git a/content/docs/smallrye-config/latest/config-sources/locations.adoc b/content/docs/smallrye-config/latest/config-sources/locations.adoc new file mode 100644 index 000000000..84bdc6c76 --- /dev/null +++ b/content/docs/smallrye-config/latest/config-sources/locations.adoc @@ -0,0 +1,82 @@ += Locations +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Additionally, to the default config locations specified by the +https://github.com/eclipse/microprofile-config/[MicroProfile Config] specification, SmallRye Config provides a way to +scan additional locations for configuration properties files. + +The `smallrye.config.locations` configuration property accepts multiple locations separated by a comma `,` and each +ust represent a valid `URI`. The supported `URI` schemes are: + +* file or directory (`file:`) +* classpath resource +* jar resource (`jar:`) +* http resource (`http:`) + +Each `URI` scheme loads all discovered resources in a `ConfigSource`. All loaded sources use the same ordinal of the +source that found the `smallrye.config.locations` configuration property. For instance, if `smallrye.config.locations` +is set as a system property, then all loaded sources have their ordinals set to `400` (system properties use `400` as +their ordinal). The ordinal may be overridden directly in the resource by setting the `config++_++ordinal` property. +Sources are sorted first by their ordinal, then by location order, and finally by loading order. + +If a profile is active, and the `URI` represents a single resource (for instance a file), then resources that match +the active profile are also loaded. The profile resource name must follow the pattern: `++{++location}-++{++profile}`. A +profile resource is only loaded if the unprofiled resource is also available in the same location. This is to keep a +consistent loading order and pair all the resources together. + +Profile resources are not taken into account if the location is a directory since there is no reliable way to discover +which file is the main resource. Properties that use the profile prefix syntax `%profile.` will work as expected. + +_All properties files from a directory_ + +[source,properties] +---- +# loads all files from a relative path +smallrye.config.locations=./src/main/resources/ + +# loads all files from an absolute path +smallrye.config.locations=/user/local/config +---- + +For relative paths, the JVM `user.dir` property defines the current directory. + +_A specific file_ + +../src/main/resources/additional.properties + +[source,properties] +---- +smallrye.config.locations=./src/main/resources/additional.properties +---- + +If a profile `dev` is active, and an `additional-dev.properties` file exists, this will also be loaded. + +_All `additional.properties` files from the classpath_ + +.additional.properties +[source,properties] +---- +smallrye.config.locations=additional.properties +---- + +If a profile `prod` is active, and an `additional-prod.properties` resources exists next to the `additional.properties` +resource, this will also be loaded. + +_The `resources.properties` file from a specific jar_ + +.jar:file:///user/local/app/lib/resources-.jar!/resources.properties +[source,properties] +---- +smallrye.config.locations=jar:file:///user/local/app/lib/resources-.jar!/resources.properties +---- + +If a profile `test` is active, and an `additional-test.properties` resource exists, this will also be loaded. + +_The `config.properties` file from a web server_ + +.http://localhost:8080/config/config.properties +[source,properties] +---- +smallrye.config.locations=http://localhost:8080/config/config.properties +---- diff --git a/content/docs/smallrye-config/latest/config-sources/toml.adoc b/content/docs/smallrye-config/latest/config-sources/toml.adoc new file mode 100644 index 000000000..84319a738 --- /dev/null +++ b/content/docs/smallrye-config/latest/config-sources/toml.adoc @@ -0,0 +1,22 @@ += TOML Config Source +:version-root: ../ +include::{version-root}_attributes.adoc[] + +This Config Source allows to use a https://toml.io[TOML] file to load configuration values. The TOML Config Source +loads the configuration from the following files: + +[arabic] +. (`265`) `application.toml` in `config` folder, located in the current working directory +. (`255`) `application.toml` in the classpath +. (`110`) MicroProfile Config configuration file `META-INF/microprofile-config.toml` in the classpath + +The following dependency is required in the classpath to use the TOML Config Source: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.config + smallrye-config-source-toml + {version} + +---- diff --git a/content/docs/smallrye-config/latest/config-sources/yaml.adoc b/content/docs/smallrye-config/latest/config-sources/yaml.adoc new file mode 100644 index 000000000..0c8bb5e3f --- /dev/null +++ b/content/docs/smallrye-config/latest/config-sources/yaml.adoc @@ -0,0 +1,21 @@ += YAML Config Source +:version-root: ../ +include::{version-root}_attributes.adoc[] + +This Config Source allows to use a `yaml` file to load configuration values. The YAML Config Source loads the configuration from the following files: + +[arabic] +. (`265`) `application.yaml++|++yml` in `config` folder, located in the current working directory +. (`255`) `application.yaml++|++yml` in the classpath +. (`110`) MicroProfile Config configuration file `META-INF/microprofile-config.yaml++|++yml` in the classpath + +The following dependency is required in the classpath to use the YAML Config Source: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.config + smallrye-config-source-yaml + {version} + +---- diff --git a/content/docs/smallrye-config/latest/config-sources/zookeeper.adoc b/content/docs/smallrye-config/latest/config-sources/zookeeper.adoc new file mode 100644 index 000000000..37f27785d --- /dev/null +++ b/content/docs/smallrye-config/latest/config-sources/zookeeper.adoc @@ -0,0 +1,30 @@ += ZooKeeper Config Source +:version-root: ../ +include::{version-root}_attributes.adoc[] + +This Config Source allows using https://zookeeper.apache.org/index.html[Apache ZooKeeper] to load configuration +values. + +The following dependency is required in the classpath to use the ZooKeeper Config Source: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.config + smallrye-config-source-zookeeper + {version} + +---- + +It also requires to set up additional configuration properties to identify the ZooKeeper instance: + +[source,properties] +---- +io.smallrye.configsource.zookeeper.url=localhost:2181 +io.smallrye.configsource.zookeeper.applicationId=applicationId +---- + +The ZooKeeper Config Source will look for configuration values in a ZooKeeper instance running in the url set in +`io.smallrye.configsource.zookeeper.url` and in the znodes available in `/applicationId/`. + +This Config Source has an ordinal of `150`. diff --git a/content/docs/smallrye-config/latest/config/config-value.adoc b/content/docs/smallrye-config/latest/config/config-value.adoc new file mode 100644 index 000000000..63d58d715 --- /dev/null +++ b/content/docs/smallrye-config/latest/config/config-value.adoc @@ -0,0 +1,8 @@ += Config Value +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The `io.smallrye.config.ConfigValue` is a metadata object that holds additional information after the lookup of a +configuration property. It is able to hold information of configuration property name, value, profile, the +`ConfigSource` from where the configuration was loaded, the ordinal of the `ConfigSource` and a line number from where +the configuration was loaded if exists. diff --git a/content/docs/smallrye-config/latest/config/configuration.adoc b/content/docs/smallrye-config/latest/config/configuration.adoc new file mode 100644 index 000000000..cb7a1b582 --- /dev/null +++ b/content/docs/smallrye-config/latest/config/configuration.adoc @@ -0,0 +1,20 @@ += Configuration Reference +:version-root: ../ +include::{version-root}_attributes.adoc[] + +[width="100%",cols="92%,4%,4%",options="header",] +|=== +|Configuration Property |Type |Default +|`smallrye.config.profile` + +The main xref:config/profiles.adoc[Profile] to activate. |String++[]++ | +|`smallrye.config.profile.parent` + +The parent xref:config/profiles.adoc#parent-profile[Profile] to activate. |String | +|`smallrye.config.locations` + +xref:config-sources/locations.adoc[Additional config locations] to be loaded with the Config. The configuration supports multiple locations separated by a comma and each must represent a valid `java.net.URI`. |URI++[]++ | +|`smallrye.config.mapping.validate-unknown` + +xref:config/mappings.adoc#retrieval[Validates] that a `@ConfigMapping` maps every available configuration name contained in the mapping prefix. |boolean |true +|`smallrye.config.secret-handlers` + +The names of the secret handlers to be loaded. A value of `all` loads all available secret handlers and a value of `none` skips the load. |String++[]++ |all +|`smallrye.config.log.values` + +Enable logging of configuration values lookup in DEBUG log level. |boolean |false +|=== diff --git a/content/docs/smallrye-config/latest/config/customizer.adoc b/content/docs/smallrye-config/latest/config/customizer.adoc new file mode 100644 index 000000000..e841a645b --- /dev/null +++ b/content/docs/smallrye-config/latest/config/customizer.adoc @@ -0,0 +1,49 @@ += Customizer +:version-root: ../ +include::{version-root}_attributes.adoc[] + +A `SmallRyeConfigBuilderCustomizer` allows to customize a `SmallRyeConfigBuilder`, used to create the `SmallRyeConfig` +instance. + +Registration of a `SmallRyeConfigBuilderCustomizer` is done via the `ServiceLoader` mechanism by providing the +implementation classes in a `META-INF/services/io.smallrye.config.SmallRyeConfigBuilderCustomizer` file. Alternatively, +customizers may be registered via the Programmatic API in `SmallRyeConfigBuilder#withCustomizers`. + +The `SmallRyeConfigBuilderCustomizer` may also +assign a priority by overriding the default method `int priority()`. Customizers are sorted by ascending priority and +executed in that order, meaning that higher numeric priorities will execute last, possible overriding values set by +previous customizers. + +.CustomConfigBuilder +[source,java] +---- +package org.acme.config; + +import io.smallrye.config.SmallRyeConfigBuilder; +import io.smallrye.config.SmallRyeConfigBuilderCustomizer; + +public class CustomConfigBuilder implements SmallRyeConfigBuilderCustomizer { + @Override + public void configBuilder(final SmallRyeConfigBuilder builder) { + builder.withDefaultValue("my.default", "1234"); + } +} +---- + +And registration in: + +.META-INF/services/io.smallrye.config.SmallRyeConfigBuilderCustomizer +[source,properties] +---- +org.acme.config.CustomConfigBuilder +---- + +The `CustomConfigBuilder` will be executed when creating a new `SmallRyeConfig` from a `SmallRyeConfigBuilder`: + +[source,java] +---- +SmallRyeConfig config = new SmallRyeConfigBuilder().build(); +config.getValue("my.default", int.class); +---- + +A look up to `my.default` returns the value `1234`, registered by the `CustomConfigBuilder`. diff --git a/content/docs/smallrye-config/latest/config/environment-variables.adoc b/content/docs/smallrye-config/latest/config/environment-variables.adoc new file mode 100644 index 000000000..15579d8e6 --- /dev/null +++ b/content/docs/smallrye-config/latest/config/environment-variables.adoc @@ -0,0 +1,80 @@ += Environment Variables +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Environment Variable names follow the conversion rules specified by https://github.com/eclipse/microprofile-config/blob/master/spec/src/main/asciidoc/configsources.asciidoc#default-configsources[MicroProfile Config]. + +SmallRye Config specifies additional conversion rules: + +* A property with double quotes `foo."bar".baz` or `foo.bar."baz"`, replace each character that is neither alphanumeric +nor `++_++` with `++_++`: `FOO++__++BAR++__++BAZ` or `FOO++_++BAR++__++BAZ++_++` ; +* A property with dashes `foo.bar-baz`, replace each character that is neither alphanumeric nor `++_++` +with `++_++`: `FOO++_++BAR++_++BAZ` +* An indexed property `foo.bar++[++0++]++` or `foo.bar++[++0++]++.baz`, replace each character that is neither alphanumeric nor `++_++` +with `++_++`: `FOO++_++BAR++_++0++_++` or `FOO++_++BAR++_++0++__++BAZ`. + +[CAUTION] +==== + +Environment Variables format cannot represent the entire spectrum of common property names. + +==== + +The lookup of configuration values from Environment Variables will always use the dotted format name. For +instance, the lookup of the Environment Variable `FOO++_++BAR` value, requires the property name `foo.bar`: + +[source,java] +---- +Config config = Config.getOrCreate(); +String value = config.getValue("foo.bar", String.class); +---- + +When SmallRyeConfig performs the lookup on the Environment Variables Config Source, it applies the conversion rules to +find the matching property name and retrieve the value. + +In some situations, looking up the exact property name is impossible. For instance, when SmallRye Config has to look up +a configuration that is part of a `Map`, and the property name contains a dynamic segment (a `Map` key). In this case, +SmallRye Config relies upon each source's list of property names. These must be converted back to their most likely +dotted format for Environment Variables. + +By default, the underscore `++_++` of an Environment Variable name always maps to a dot `.`. If the property name +contains a dash or some other special character, that property name can be specified in another Config +Source, with the expected dotted format. It will provide additional information to SmallRye Config to perform a +two-way conversion and match the property names. + +Consider: + +..env + +[source,console] +---- +FOO_BAR_BAZ=VALUE +---- + +Will map to `foo.bar.baz` and value `value`. + +If `foo.bar-baz` is available in any source: + +..env + +[source,console] +---- +FOO_BAR_BAZ=VALUE +---- + +.application.properties +[source,properties] +---- +foo.bar-baz=default +---- + +Will map to `foo.bar-baz` and value `value`. + +[NOTE] +==== + +The property name in dotted format needs to exist somewhere to provide this additional information. It can be set in a +low ordinal source, even without value. The Environment Variables source will override the value and map the correct +configuration name. + +==== diff --git a/content/docs/smallrye-config/latest/config/expressions.adoc b/content/docs/smallrye-config/latest/config/expressions.adoc new file mode 100644 index 000000000..f76d0df5c --- /dev/null +++ b/content/docs/smallrye-config/latest/config/expressions.adoc @@ -0,0 +1,75 @@ += Property Expressions +:version-root: ../ +include::{version-root}_attributes.adoc[] + +SmallRye Config provides property expressions expansion on configuration values. An expression string is a mix of plain +strings and expression segments, which are wrapped by the sequence $++{++ ... }. + +For instance, the following configuration properties file: + +[source,properties] +---- +remote.host=smallrye.io +callable.url=https://${remote.host}/ +---- + +The resolved value of the `callable.url` property is `https://smallrye.io/`. + +Additionally, the Expression Expansion engine supports the following segments: + +* `$++{++expression:value}` - Provides a default value after the `:` if the expansion doesn’t find a value. +* `$++{++my.prop$++{++compose}}` - Composed expressions. Inner expressions are resolved first. +* `$++{++my.prop}$++{++my.prop}` - Multiple expressions. + +If an expression cannot be expanded and no default is supplied a `NoSuchElementException` is thrown. + +== Escaping + +A `$` that begins an expression segment can be escaped with a backslash to treat it as a literal character: + +[source,properties] +---- +# value is: ${my.prop} +expression=\${my.prop} +---- + +A backslash that immediately precedes a `$` can itself be escaped with another backslash: + +[source,properties] +---- +# my.prop=value +# value is: \value +expression=\\${my.prop} +---- + +Escaping only applies to values that contain an expression segment. In a value that has no `$++{++ ... }` segment, +every backslash is treated as a literal character and nothing needs escaping (for example, a Windows path such as +`C:++\++some++\++path` is used as-is). + +As soon as a value contains an expression segment, the whole value is processed for escapes and backslash becomes an +escape character everywhere in it, not just before a `$`. Standard escape sequences are interpreted (`++\++n`, `++\++r`, `++\++t`, +`++\++b`, `++\++f`) and a backslash before any other character is dropped. To keep a literal backslash in such a value, double +it: + +[source,properties] +---- +# value is: C:\some\path\value (my.prop=value) +expression=C:\\some\\path\\${my.prop} +---- + +== Disabling Expansion + +Expression expansion may be selectively disabled for a block of code using `io.smallrye.config.Expressions.withoutExpansion()`. +The raw (unexpanded) value is returned for any property looked up within the block: + +[source,properties] +---- +callable.url=https://${remote.host}/ +---- + +[source,java] +---- +Config config = Config.getOrCreate(); +// returns "https://${remote.host}/" +String url = Expressions.withoutExpansion(() -> config.getValue("callable.url", String.class)); +---- diff --git a/content/docs/smallrye-config/latest/config/getting-started.adoc b/content/docs/smallrye-config/latest/config/getting-started.adoc new file mode 100644 index 000000000..d2eb46c6d --- /dev/null +++ b/content/docs/smallrye-config/latest/config/getting-started.adoc @@ -0,0 +1,182 @@ += Getting Started +:version-root: ../ +include::{version-root}_attributes.adoc[] + +== Config Sources + +By default, SmallRye Config reads configuration properties from multiple configuration sources (by descending ordinal): + +[arabic] +. (`400`) System properties +. (`300`) Environment variables +. (`295`) `.env` file in the current working directory +. (`260`) `application.properties` in `config` folder, located in the current working directory +. (`250`) `application.properties` in the classpath +. (`100`) MicroProfile Config configuration file `META-INF/microprofile-config.properties` in the classpath + +A configuration source is handled by a `ConfigSource`. A `ConfigSource` provides configuration values from a specific +place. + +The final configuration is the aggregation of the properties defined by all these sources. A configuration property +lookup starts by the highest ordinal configuration source available and works it way down to other sources until a +match is found. This means that any configuration property may override a value just by setting a different value in a +higher ordinal config source. For example, a property configured using an Environment Variable overrides the value +provided using the `microprofile-config.properties` file. + +=== System Properties + +System properties can be handed to the application through the `-D` flag during startup. For instance, +`java -Dmy.prop -jar my.jar`. + +=== Environment Variables + +Environment variables are set directly in the host operating system. Environment variables names follow the conversion +rules detailed by xref:config/environment-variables.adoc[Environment Variables]. + +=== MicroProfile Config configuration file + +The MicroProfile Config configuration file `META-INF/microprofile-config.properties` in the classpath. It follows the +standard convention for `properties` files. + +.META-INF/microprofile-config.properties +[source,properties] +---- +greeting.message=hello +goodbye.message=bye +---- + +=== Additional Config Sources + +SmallRye Config provides additional extensions which cover other configuration formats and stores: + +* xref:config-sources/yaml.adoc[YAML] +* xref:config-sources/filesystem.adoc[File System] +* xref:config-sources/zookeeper.adoc[ZooKeeper] +* xref:config-sources/hocon.adoc[HOCON] + +It is also possible to create a xref:config-sources/custom.adoc[Custom ConfigSource]. + +== Retrieving the Configuration + +=== Programmatically + +The `io.smallrye.config.Config#getOrCreate()` API allows to access the `io.smallrye.config.Config` API +programmatically: + +[source,java] +---- +import io.smallrye.config.Config; + +Config config = Config.getOrCreate(); +String message = config.getValue("greeting.message", String.class); +---- + +The `Config` instance will be created and registered to the current context class loader if no such configuration is +already created and registered. This means that subsequent calls to `Config.getOrCreate()` or +`Config.get()` will return the same `Config` instance if the context class loader is the same. + +To obtain a detached instanced, use the `io.smallrye.config.SmallRyeConfigBuilder`: + +[source,java] +---- +SmallRyeConfig config = new SmallRyeConfigBuilder() + .addDefaultInterceptors() + .addDefaultSources() + .build(); + +String message = config.getValue("greeting.message", String.class); +---- + +=== With CDI + +In a CDI environment, configuration can be injected in CDI aware beans with `@Inject` and +the `org.eclipse.microprofile.config.inject.ConfigProperty` qualifier. + +[source,java] +---- +@Inject +@ConfigProperty(name = "greeting.message") +String message; + +@Inject +@ConfigProperty(name = "greeting.suffix", defaultValue="!") +String suffix; + +@Inject +@ConfigProperty(name = "greeting.name") +Optional name; + +@Inject +SmallRyeConfig config; +---- + +* If a value if not provided for this `greeting.message`, the application startup fails with a +`jakarta.enterprise.inject.spi.DeploymentException: No config value of type ++[++class java.lang.String++]++ exists for: greeting.message`. +* The default value `!` is injected if the configuration does not provide a value for `greeting.suffix`. +* The property `greeting.name` is optional - an empty Optional is injected if the configuration does not provide a +value for it. + +== Override Config + +It is possible to override `Config` default initialization `Config.getOrCreate()`, by extending +`io.smallrye.config.SmallRyeConfigFactory` and registering the implementation with the `ServiceLoader` mechanism. + +== Config vs SmallRyeConfig + +The `io.smallrye.config.Config` is an extension of `org.eclipse.microprofile.config.Config` and provides +additional APIs and helper methods not available in `org.eclipse.microprofile.config.Config`. To obtain an instance of +`io.smallrye.config.Config`, the original `org.eclipse.microprofile.config.Config` can be unwrapped: + +[source,java] +---- +import io.smallrye.config.SmallRyeConfig; +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.ConfigProvider; + +Config config = ConfigProvider.getConfig(); +SmallRyeConfig smallRyeConfig = config.unwrap(SmallRyeConfig.class); +---- + +Or obtained directly: + +[source,java] +---- +Config config = Config.getOrCreate(); +---- + +A few notable APIs provided by `io.smallrye.config.Config` allow to: + +* Retrieve multiple values into a specified `Collection` +* Retrieve xref:config/indexed-properties.adoc[Indexed Values] +* Retrieve xref:config/mappings.adoc[Config Mappings] instances +* Retrieve the raw value of a configuration +* Check if a property is present +* Retrieve a `Converter` +* Convert values + +== Converters + +The `ConfigSource` retrieves a configuration value as a `String`. Other data types require a conversion using the +`org.eclipse.microprofile.config.spi.Converter` API. + +Most of the common `Converter` types are provided by default: + +* `boolean` and `java.lang.Boolean`; the values "true", "1", "YES", "Y" "ON" represent `true`. Any other value will be +interpreted as `false` +* `byte` and `java.lang.Byte` +* `short` and `java.lang.Short` +* `int`, `java.lang.Integer`, and `java.util.OptionalInt` +* `long`, `java.lang.Long`, and `java.util.OptionalLong` +* `float` and `java.lang.Float`; a dot '.' is used to separate the fractional digits +* `double`, `java.lang.Double`, and `java.util.OptionalDouble`; a dot '.' is used to separate the fractional digits +* `char` and `java.lang.Character` +* `java.lang.Class` based on the result of `Class.forName` +* `java.net.InetAddress` +* `java.util.UUID` +* `java.util.Currency` +* `java.util.regex.Pattern` +* `java.nio.file.Path` +* Any class with declared static methods `of`, `valueOf` or `parse` that take a `String` or a `CharSequence` +* Any class with declared constructors that takes a `String` or a `CharSequence` + +All default converters have a priority of `1`. diff --git a/content/docs/smallrye-config/latest/config/indexed-properties.adoc b/content/docs/smallrye-config/latest/config/indexed-properties.adoc new file mode 100644 index 000000000..78b32cc10 --- /dev/null +++ b/content/docs/smallrye-config/latest/config/indexed-properties.adoc @@ -0,0 +1,32 @@ += Indexed Properties +:version-root: ../ +include::{version-root}_attributes.adoc[] + +In https://github.com/eclipse/microprofile-config/[MicroProfile Config], a config value with unescaped commas may be +converted to `Collection`. It works for simple cases, but it becomes cumbersome and limited for more advanced use cases. + +Indexed Properties provide a way to use indexes in config property names to map specific elements in a `Collection` +type. Since the indexed element is part of the property name, it can also map complex object types. Consider: + +[source,properties] +---- +# MicroProfile Config - Collection Values +my.collection=dog,cat,turtle + +# SmallRye Config - Indexed Property +my.indexed.collection[0]=dog +my.indexed.collection[1]=cat +my.indexed.collection[2]=turtle +---- + +The indexed property syntax uses the property name and square brackets with an index in between. + +A call to `Config#getValues("my.collection", String.class)`, will automatically create and convert a `List++<++String++>++` +that contains the values `dog`, `cat` and `turtle`. A call to `Config#getValues("my.indexed.collection", String.class)` +returns the exact same result. The indexed property format is prioritized when both styles are found in the same +configuration source. When available in multiple sources, the higher ordinal source wins, like any other configuration +lookup. + +The indexed property is sorted by its index before being added to the target `Collection`. Any gaps in the indexes do +not resolve to the target `Collection`, which means that the `Collection` result will store all values without empty +elements. diff --git a/content/docs/smallrye-config/latest/config/instance-builder.adoc b/content/docs/smallrye-config/latest/config/instance-builder.adoc new file mode 100644 index 000000000..1bd05231f --- /dev/null +++ b/content/docs/smallrye-config/latest/config/instance-builder.adoc @@ -0,0 +1,234 @@ += Instance Builder +:version-root: ../ +include::{version-root}_attributes.adoc[] + +With the `io.smallrye.config.ConfigInstanceBuilder` API, it is possible to create instances of a configuration interface +programmatically, without requiring a `SmallRyeConfig` or any configuration source. This is useful for testing, default +configurations, or any scenario where configuration values are known at build time. + +== Usage + +A configuration interface instance is created with `ConfigInstanceBuilder.forInterface()`: + +[source,java] +---- +interface Server { + String host(); + + int port(); +} + +Server server = ConfigInstanceBuilder.forInterface(Server.class) + .with(Server::host, "localhost") + .with(Server::port, 8080) + .build(); +---- + +The builder uses method references to identify which property to set, providing compile-time type safety without +string-based property names. The `with` method accepts a method reference to the configuration interface member and the +value to set. + +[NOTE] +==== + +The configuration interface does not require the `@ConfigMapping` annotation to work with the builder. Any valid +configuration interface is accepted. + +==== + +== Primitive Types + +The builder provides dedicated `with()` overloads for `int`, `long`, `double`, and `boolean` primitive types. Other +primitive types (`byte`, `short`, `float`, `char`) are set via the generic `with()` method using their boxed types: + +[source,java] +---- +@ConfigMapping +interface Primitives { + int intValue(); + + boolean booleanValue(); + + byte byteValue(); +} + +Primitives primitives = ConfigInstanceBuilder.forInterface(Primitives.class) + .with(Primitives::intValue, 42) + .with(Primitives::booleanValue, true) + .with(Primitives::byteValue, Byte.valueOf((byte) 1)) + .build(); +---- + +== Optional Properties + +The `withOptional()` method sets optional properties. If an optional property is not set, it defaults to empty: + +[source,java] +---- +interface AppConfig { + Optional name(); + + OptionalInt timeout(); +} + +AppConfig config = ConfigInstanceBuilder.forInterface(AppConfig.class) + .withOptional(AppConfig::name, "MyApp") + .withOptional(AppConfig::timeout, 30) + .build(); +---- + +The `withOptional` method wraps the value in `Optional.of()`, `OptionalInt.of()`, `OptionalLong.of()`, or +`OptionalDouble.of()` depending on the property type. + +== Defaults + +Properties annotated with `io.smallrye.config.WithDefault` are automatically applied when no value is explicitly set +in the builder: + +[source,java] +---- +interface ServerDefaults { + @WithDefault("localhost") + String host(); + + @WithDefault("8080") + int port(); +} + +ServerDefaults server = ConfigInstanceBuilder.forInterface(ServerDefaults.class).build(); +---- + +The `ServerDefaults#host()` will return the value `localhost` and `ServerDefaults#port()` will return the value `8080`. + +Explicitly set values override the `@WithDefault` annotation: + +[source,java] +---- +ServerDefaults server = ConfigInstanceBuilder.forInterface(ServerDefaults.class) + .with(ServerDefaults::host, "0.0.0.0") + .build(); +---- + +The `ServerDefaults#host()` will return the value `0.0.0.0` and `ServerDefaults#port()` will return the value `8080`. + +== Nested Groups + +Nested configuration groups are built separately and composed into the parent builder: + +[source,java] +---- +interface AppConfig { + String name(); + + DatabaseConfig database(); +} + +interface DatabaseConfig { + String url(); + + int poolSize(); +} + +AppConfig config = ConfigInstanceBuilder.forInterface(AppConfig.class) + .with(AppConfig::name, "MyApp") + .with(AppConfig::database, ConfigInstanceBuilder.forInterface(DatabaseConfig.class) + .with(DatabaseConfig::url, "jdbc:h2:mem:test") + .with(DatabaseConfig::poolSize, 10) + .build()) + .build(); +---- + +If a nested group has `@WithDefault` values for all its members, the nested group instance is automatically built with +those defaults when not explicitly set in the builder. + +== Collections and Maps + +`List`, `Set`, and `Map` types are set directly with their values: + +[source,java] +---- +interface AppConfig { + List hosts(); + + Map labels(); +} + +AppConfig config = ConfigInstanceBuilder.forInterface(AppConfig.class) + .with(AppConfig::hosts, List.of("host1", "host2")) + .with(AppConfig::labels, Map.of("env", "prod", "region", "us-east")) + .build(); +---- + +Collections and Maps annotated with `@WithDefault` are automatically populated when not explicitly set. + +== Converters + +The builder uses the same converter mechanism as `SmallRyeConfig`. Converters are discovered via the +`java.util.ServiceLoader` mechanism, or can be registered explicitly with `ConfigInstanceBuilder.registerConverter()`: + +[source,java] +---- +ConfigInstanceBuilder.registerConverter(Duration.class, new MyDurationConverter()); +---- + +Properties annotated with `io.smallrye.config.WithConverter` use the specified converter for both `@WithDefault` value +conversion and any value set via the builder: + +[source,java] +---- +interface AppConfig { + @WithDefault("30s") + @WithConverter(DurationConverter.class) + Duration timeout(); + + @WithConverter(MemorySizeConverter.class) + MemorySize maxBodySize(); +} +---- + +[NOTE] +==== + +Converters registered with `registerConverter` are global and shared across all `ConfigInstanceBuilder` instances. + +==== + +== Required Properties + +Properties without a `@WithDefault` are considered required. Calling `build()` throws a `NoSuchElementException` if +any required property is missing: + +[source,java] +---- +interface Server { + String host(); + + @WithDefault("8080") + int port(); +} + +// Throws NoSuchElementException because host is required +ConfigInstanceBuilder.forInterface(Server.class).build(); + +// Works because host is provided +ConfigInstanceBuilder.forInterface(Server.class) + .with(Server::host, "localhost") + .build(); +---- + +== Builder Reuse + +A builder can be used to produce multiple instances. Each `build()` call creates a new independent instance: + +[source,java] +---- +ConfigInstanceBuilder builder = ConfigInstanceBuilder.forInterface(Server.class) + .with(Server::host, "localhost") + .with(Server::port, 8080); + +Server first = builder.build(); +Server second = builder.build(); +---- + +The `first` and `second` instances are equal but not the same object. Values set after a `build()` call affect only +subsequent builds. diff --git a/content/docs/smallrye-config/latest/config/map-support.adoc b/content/docs/smallrye-config/latest/config/map-support.adoc new file mode 100644 index 000000000..72b42740c --- /dev/null +++ b/content/docs/smallrye-config/latest/config/map-support.adoc @@ -0,0 +1,60 @@ += Map Support +:version-root: ../ +include::{version-root}_attributes.adoc[] + +SmallRye Config allows injecting multiple configuration parameters as a `Map`. The configuration value syntax is +represented by `property.name.map-key=value` Consider: + +[source,properties] +---- +server.reasons.200=OK +server.reasons.201=Created +---- + +The previous configuration could be injected directly in a CDI Bean: + +With `@ConfigProperty` + +[source,java] +---- +@ApplicationScoped +public class ConfigBean { + @Inject + @ConfigProperty(name = "server.reasons") + Map reasons; +} +---- + +With `@ConfigProperties` + +[source,java] +---- +@ConfigProperties(prefix = "server") +public class Config { + Map reasons; +} +---- + +The `Map` will contains the keys `200` and `201`, which map to the values `OK` and `Created`. + +[NOTE] +==== + +Only the direct sub properties will be converted into a `Map` and +injected into the target bean, the rest will be ignored. In other words, +in the previous example, a property whose name is `reasons.200.a` would +be ignored as not considered as a direct sub property. + +==== + +[NOTE] +==== + +The property will be considered as missing if no direct sub properties +could be found. + +==== + +It is also possible to retrieve the `Map` programmatically by calling the methods +`SmallRyeConfig#getValues("server.reasons", Integer.class, String.class)` or +`SmallRyeConfig#getOptionalValues("server.reasons", Integer.class, String.class)`. diff --git a/content/docs/smallrye-config/latest/config/mappings.adoc b/content/docs/smallrye-config/latest/config/mappings.adoc new file mode 100644 index 000000000..b7454239f --- /dev/null +++ b/content/docs/smallrye-config/latest/config/mappings.adoc @@ -0,0 +1,860 @@ += Mappings +:version-root: ../ +include::{version-root}_attributes.adoc[] + +With SmallRye Config Mappings, it is possible to group multiple configuration properties in a single interface that +share the same prefix (or namespace). It supports the following set of features: + +* Automatic conversion of the configuration type, including `List`, `Set`, `Map`, `Optional` and primitive types. +* Nested Config Mapping groups. +* Configuration Properties Naming Strategies +* Integration with Bean Validation + +== Mapping Rules + +A complex object type uses the following rules to map configuration values to their member values: + +* A configuration path is built by taking the object type prefix (or namespace) and the mapping member name +* The member name is converted to its kebab-case format +* If the member name is represented as a getter, the member name is taken from its property name equivalent, and then +converted to its kebab-case format +* The configuration value is automatically converted to the member type +* The configuration path is required to exist with a valid configuration value or the mapping will fail + +[NOTE] +==== + +Kebab-case - the method name is derived by replacing case changes with a dash to map the configuration property. + +==== + +A Config Mapping requires an interface with minimal metadata configuration annotated with +`io.smallrye.config.ConfigMapping`: + +[source,java] +---- +@ConfigMapping(prefix = "server") +interface Server { + String host(); + + int port(); +} +---- + +The `Server` interface is able to map configurations with the name `server.host` into the `Server#host()` method and +`server.port` into `Server#port()` method. The configuration property name to lookup is built from the prefix, and the +method name with `.` (dot) as the separator. + +[WARNING] +==== + +If a mapping fails to match a configuration property the config system throws a `NoSuchElementException`, unless +the mapped element is an `Optional`. + +==== + +The `@ConfigMapping` interface must obey the following rules: + +* A mapping method cannot accept parameters +* A mapping method return type cannot be void +* A mapping cannot use self-reference types +* `default` methods are allowed + +== Registration + +Registration of Config Mappings is automatic in CDI aware environments with the `@ConfigMapping` annotation. + +In non-CDI environments, the Config Mapping can be registered via `SmallRyeConfigBuilder#withMapping`. In this case, +the `@ConfigMapping` is completely optional (but recommendeded to set the prefix). + +[source,java] +---- +SmallRyeConfig config = new SmallRyeConfigBuilder() + .withMapping(Server.class) + .build(); +---- + +== Retrieval + +A config mapping interface can be injected into any CDI aware bean: + +[source,java] +---- +@ApplicationScoped +class BusinessBean { + @Inject + Server server; + + public void businessMethod() { + String host = server.host(); + } +} +---- + +In non-CDI environments, use the API `io.smallrye.config.SmallRyeConfig#getConfigMapping` to retrieve the config +mapping instance: + +[source,java] +---- +Config config = Config.getOrCreate(); +Server server = config.getConfigMapping(Server.class); +---- + +[NOTE] +==== + +Config Mapping instances are cached. They are populated when the `SmallRyeConfig` instance is initialized and +their values are not updated on `ConfigSource` changes. + +==== + +For a Config Mapping to be valid, it needs to match every configuration property name contained in the `Config` under +the specified prefix set in `@ConfigMapping`. This prevents unknown configuration properties in the `Config`. + +=== Disabling validation + +Validation can be disabled entirely in two ways: + +Via configuration property: + +[source,properties] +---- +smallrye.config.mapping.validate-unknown=false +---- + +Or programmatically via the builder: + +[source,java] +---- +SmallRyeConfig config = new SmallRyeConfigBuilder() + .withMapping(Server.class) + .withValidateUnknown(false) + .build(); +---- + +=== Ignoring specific paths + +Rather than disabling validation entirely, specific paths can be ignored with +`SmallRyeConfigBuilder#withMappingIgnore`. The path argument supports three patterns: + +[cols=",",options="header",] +|=== +|Pattern |Meaning +|`foo.bar` |Ignores the exact configuration name `foo.bar` +|`foo.++*++` |Ignores all direct child names under `foo` +|`foo.++**++` |Ignores all names under `foo` at any level (recursive) +|=== + +[source,java] +---- +SmallRyeConfig config = new SmallRyeConfigBuilder() + .withMapping(Server.class) + .withMappingIgnore("server.foo") // ignore the exact name + .withMappingIgnore("server.extras.*") // ignore direct children of server.extras + .withMappingIgnore("server.legacy.**") // ignore everything under server.legacy + .build(); +---- + +Multiple calls to `withMappingIgnore` accumulate — each call adds to the set of ignored paths. + +== Defaults + +The `io.smallrye.config.WithDefault` annotation allows to set a default property value into a mapping (and prevent +errors if the configuration value is not available in any `ConfigSource`). + +[source,java] +---- +public interface Defaults { + @WithDefault("foo") + String foo(); + + @WithDefault("bar") + String bar(); +} +---- + +No configuration properties are required. The `Defaults#foo()` will return the value `foo` and `Defaults#bar()` will +return the value `bar`. + +== Nested Groups + +A nested mapping provides a way to map sub-groups of configuration properties. + +* A nested type contributes with its name (converted to its kebab-case format) +* The configuration path is built by taking the root object type prefix (or namespace), the nested type name and the +member name of the nested type + +[source,java] +---- +@ConfigMapping(prefix = "server") +public interface Server { + String host(); + + int port(); + + Log log(); + + interface Log { + boolean enabled(); + + String suffix(); + + boolean rotate(); + } +} +---- + +[source,properties] +---- +server.host=localhost +server.port=8080 +server.log.enabled=true +server.log.suffix=.log +server.log.rotate=false +---- + +The method name of a mapping group acts as a sub-prefix in the property name. In this case the matching property to +`Server.Log#enabled` is `server.log.enabled`. + +== Hierarchy + +A config mapping can extend another mapping and inherit all its super members: + +[source,java] +---- +public interface Parent { + String name(); +} + +@ConfigMapping(prefix = "child") +public interface Child extends Parent { + +} +---- + +And override members: + +[source,java] +---- +public interface Parent { + String name(); +} + +@ConfigMapping(prefix = "child") +public interface Child extends Parent { + @WithName("child-name") + String name(); +} +---- + +== Overriding property names + +=== `@WithName` + +If a method name and a property name do not match, the `io.smallrye.config.WithName` annotation can +override the method name mapping and use the name supplied in the annotation. + +[source,java] +---- +@ConfigMapping(prefix = "server") +interface Server { + @WithName("name") + String host(); + + int port(); +} +---- + +[source,properties] +---- +server.name=localhost +server.port=8080 +---- + +=== `@WithParentName` + +The `io.smallrye.config.WithParentName` annotation allows configurations mappings to inherit its parent container name, +simplifying the configuration property name required to match the mapping. + +[source,java] +---- +@ConfigMapping(prefix = "server") +interface Server { + @WithParentName + ServerHostAndPort hostAndPort(); + + @WithParentName + ServerInfo info(); +} + +interface ServerHostAndPort { + String host(); + + int port(); +} + +interface ServerInfo { + String name(); +} +---- + +[source,properties] +---- +server.host=localhost +server.port=8080 +server.name=konoha +---- + +Without the `@WithParentName` the method `ServerInfo#name` maps the configuration property `server.info.name`. With +`@WithParentName`, the `Server#info` mapping will inherit the parent name from `Server` and `ServerInfo#name` maps to +the property `server.name` instead. + +=== NamingStrategy + +Method names in camelCase map to kebab-case configuration property names by default. + +[source,java] +---- +@ConfigMapping(prefix = "server") +interface Server { + String theHost(); + + int thePort(); +} +---- + +[source,properties] +---- +server.the-host=localhost +server.the-port=8080 +---- + +The mapping strategy can be adjusted by setting `namingStrategy` value in the `@ConfigMapping` annotation. + +[source,java] +---- +@ConfigMapping(prefix = "server", namingStrategy = ConfigMapping.NamingStrategy.VERBATIM) +public interface ServerVerbatimNamingStrategy { + String theHost(); + + int thePort(); +} +---- + +[source,properties] +---- +server.theHost=localhost +server.thePort=8080 +---- + +The `@ConfigMapping` annotation support the following naming stategies: + +* KEBAB++_++CASE - The method name is derived by replacing case changes with a dash to map the configuration property. +* VERBATIM - The method name is used as is to map the configuration property. +* SNAKE++_++CASE - The method name is derived by replacing case changes with an underscore to map the configuration property. + +=== `beanStyleGetters` + +The `beanStyleGetters` attribute (default `false`) enables matching bean-style getter names (`get`/`is` prefixed) to +their property name equivalent. For example, `getHost()` and `isEnabled()` map to the properties `host` and `enabled` +respectively: + +[source,java] +---- +@ConfigMapping(prefix = "server", beanStyleGetters = true) +public interface Server { + String getHost(); + + int getPort(); + + boolean isEnabled(); +} +---- + +[source,properties] +---- +server.host=localhost +server.port=8080 +server.enabled=true +---- + +[WARNING] +==== + +Bean-style getter matching allows multiple method names to match the same configuration name. For instance, +`getFoo` and `isFoo` both match `foo`, which may not be intended. Prefer simple method names that match +one-to-one with their configuration names. + +==== + +== Conversion + +A config mapping interface support automatic conversions of all types available for conversion in `Config`. + +[source,java] +---- +@ConfigMapping +public interface SomeTypes { + @WithName("int") + int intPrimitive(); + + @WithName("int") + Integer intWrapper(); + + @WithName("long") + long longPrimitive(); + + @WithName("long") + Long longWrapper(); + + @WithName("float") + float floatPrimitive(); + + @WithName("float") + Float floatWrapper(); + + @WithName("double") + double doublePrimitive(); + + @WithName("double") + Double doubleWrapper(); + + @WithName("char") + char charPrimitive(); + + @WithName("char") + Character charWrapper(); + + @WithName("boolean") + boolean booleanPrimitive(); + + @WithName("boolean") + Boolean booleanWrapper(); +} +---- + +[source,properties] +---- +int=9 +long=9999999999 +float=99.9 +double=99.99 +char=c +boolean=true +---- + +This is also valid for `Optional` and friends. + +[source,java] +---- +@ConfigMapping +public interface Optionals { + Optional server(); + + Optional optional(); + + @WithName("optional.int") + OptionalInt optionalInt(); + + interface Server { + String host(); + + int port(); + } +} +---- + +In this case, the mapping won’t fail if the configuraton properties values are missing. + +=== `@WithConverter` + +The `io.smallrye.config.WithConverter` annotation provides a way to set a specific `Converter` in a mapping. + +[source,java] +---- +@ConfigMapping +public interface Converters { + @WithConverter(FooBarConverter.class) + String foo(); +} + +public static class FooBarConverter implements Converter { + @Override + public String convert(final String value) { + return "bar"; + } +} +---- + +[source,properties] +---- +foo=foo +---- + +A call to `Converters.foo()` results in the value `bar`. + +== Collections + +A config mapping is also able to map the collections types `List` and `Set`. + +* A member with a `Collection` type requires the configuration name to be in its indexed format +* Each configuration name, plus its index maps the configuration value to the corresponding `Collection` element in the +object type +* The index must be part of the configuration path, by appending the index between square brackets to the`Collection` +member +* The index specified in the configuration name is used to order the element in the `Collection` +* Missing elements or gaps are removed + +[source,java] +---- +@ConfigMapping(prefix = "server") +public interface ServerCollections { + Set environments(); + + interface Environment { + String name(); + + List apps(); + + interface App { + String name(); + + List services(); + + Optional> databases(); + } + } +} +---- + +[source,properties] +---- +server.environments[0].name=dev +server.environments[0].apps[0].name=rest +server.environments[0].apps[0].services=bookstore,registration +server.environments[0].apps[0].databases=pg,h2 +server.environments[0].apps[1].name=batch +server.environments[0].apps[1].services=stock,warehouse +---- + +The `List` and `Set` mappings can use xref:config/indexed-properties.adoc[Indexed Properties] to map configuration values in +mapping groups. + +[NOTE] +==== + +A `List` mapping is backed by an `ArrayList`, and a `Set` mapping is backed by a `HashSet`. + +==== + +== Maps + +A config mapping is also able to map a `Map`. + +* A member with a `Map` type requires an additional configuration name added to the configuration path of the `Map` +member to act as a map key +* The additional configuration name maps a Map entry with the configuration name as the `Map` entry key and +the configuration value as the Map entry value + +[source,java] +---- +@ConfigMapping(prefix = "server") +public interface Server { + String host(); + + int port(); + + Map form(); + + Map> aliases(); + + interface Alias { + String name(); + } +} +---- + +[source,properties] +---- +server.host=localhost +server.port=8080 +server.form.index=index.html +server.form.login.page=login.html +server.form.error.page=error.html +server.aliases.localhost[0].name=prod +server.aliases.localhost[1].name=127.0.0.1 +server.aliases.\"io.smallrye\"[0].name=smallrye +---- + +The configuration property name needs to specify an additional segment to act as the map key. The `server.form` matches +the `Server#form` `Map` and the segments `index`, `login.page` and `error.page` represent the `Map` +keys. + +[NOTE] +==== + +A `Map` mapping is backed by an `HashMap`. + +==== + +When populating a `Map`, `SmallRyeConfig` requires the configuration names listed in +`SmallRyeConfig#getPropertyNames` to find the `Map` keys. If a `ConfigSource` does not support +`getPropertyNames` (empty), the names must be provided by another `ConfigSource` that can do so. After retrieving the +map keys, `SmallRyeConfig` performs the lookup of the values with the regular `ConfigSource` ordinal ordering. Even if +a `ConfigSource` does not provide `getPropertyNames` it can provide the value by having the name listed in another +capable `ConfigSource`. + +For collection types, the key requires the indexed format. The configuration name `server.aliases.localhost++[++0++]++.name` +maps to the `Map++<++String, List++<++Alias++>>++ aliases()` member, where `localhost` is the `Map` key, `++[++0++]++` is the index of the +`List++<++Alias++>++` collection where the `Alias` element will be stored, containing the name `prod`. + +[TIP] +.Map quoted keys requirement +==== + +The `Map` key part in the configuration property name may require quotes to delimit the key if the key contains +a dot, otherwise they are optional. If the key name is delimited by quotes, all configuration property names related +with the same name must use the quoted format across all sources, including Environment Variables. + +==== + +[WARNING] +.Do not mix quoted and unquoted keys +==== + +For a key that spans a single segment, the quoted and unquoted forms name the same `Map` key. Both +`server.aliases.localhost.name` and `server.aliases."localhost".name` refer to the key `localhost`, so writing both +makes the key ambiguous. + +When `SmallRyeConfig` finds both forms, it resolves the entry with the quoted form and logs a warning: + +.... +SRCFG01009: The Map path keys "server.aliases."localhost"" and "server.aliases.localhost" are ambiguous. These can +cause issues in the mapping. Please, do not mix quoted and unquoted in the same Map key. Using +server.aliases."localhost". +.... + +Because only the quoted form is used to look up the entry values, the property names written in the unquoted form +are left unmapped, and the mapping fails validation: + +.... +SRCFG00050: server.aliases.localhost.name does not map to any root +.... + +If the two forms also split the members of a nested group between them, the members that only exist in the +unquoted form are not found at all, and the mapping additionally fails with: + +.... +SRCFG00014: The config property server.aliases."localhost".name is required but it could not be found in any config source +.... + +Either form works on its own, so pick one and use it consistently for a given key across all sources. + +==== + +=== `@WithUnnamedKey` + +The `io.smallrye.config.WithUnnamedKey` annotation allows to omit a single map key in the configuration path: + +[source,java] +---- +@ConfigMapping(prefix = "server") +public interface Server { + @WithUnnamedKey("localhost") + Map aliases(); + + interface Alias { + String name(); + } +} +---- + +[source,properties] +---- +server.aliases.name=localhost +server.aliases.prod.name=prod +---- + +The `sever.aliases.name` is an unnamed `Map` property, because it does not contain the `Map` key to populate the `Map` +entry. Due to `@WithUnnamedKey("localhost")` the `Map` key is not required in the configuration path. The key used to +look up the Map entry is given by `io.smallrye.config.WithUnnamedKey#value`: + +[source,java] +---- +Server server = config.getConfigMapping(Server.class); +Map localhost = server.aliases.get("localhost"); +---- + +[WARNING] +==== + +If the unnamed key (in this case `localhost`) is explicitly set in a property name, the explicit value takes precedence + +==== + +The `eager` attribute (default `true`) controls whether the unnamed key entry is included in the `Map` when its values +come only from defaults. When `eager = false`, the unnamed key entry is excluded from the `Map` unless at least one +of its values is explicitly set in a configuration source: + +[source,java] +---- +@ConfigMapping(prefix = "server") +public interface Server { + @WithUnnamedKey(value = "localhost", eager = false) + Map aliases(); + + interface Alias { + @WithDefault("localhost") + String name(); + } +} +---- + +With `eager = false` and no properties set, `server.aliases` returns an empty `Map`. With `eager = true` (the +default), it returns a `Map` with the key `localhost` populated from the `@WithDefault`. + +=== `@WithKeys` + +The `io.smallrye.config.WithKeys` annotation allows to define which `Map` keys must be loaded by +the configuration: + +[source,java] +---- +@ConfigMapping(prefix = "server") +public interface Server { + @WithKeys(KeysProvider.class) + Map aliases(); + + interface Alias { + String name(); + } + + class KeysProvider implements Supplier> { + @Override + public Iterable get() { + return List.of("dev", "test", "prod"); + } + } +} +---- + +In this case, `SmallRyeConfig` will look for the map keys `dev`, `test` and `prod` instead of discovering the keys +with `SmallRyeConfig#getPropertyNames`: + +[source,properties] +---- +servers.alias.dev.name=dev +servers.alias.test.name=test +servers.alias.prod.name=prod +---- + +The provided list will effectively substitute the lookup in `SmallRyeConfig#getPropertyNames`, thus enabling a +`ConfigSource` that does not list its properties, to contribute configuration to the `Map`. Each key must exist in +the final configuration (relative to the `Map` path segment), or the mapping will fail with a +`ConfigValidationException`. + +=== `@WithDefaults` + +The `io.smallrye.config.WithDefaults` is a marker annotation to use only in a `Map` to return the default value for +the value element on any key lookup: + +[source,java] +---- +@ConfigMapping(prefix = "server") +public interface Server { + @WithDefaults + Map aliases(); + + interface Alias { + @WithDefault("localhost") + String name(); + } +} +---- + +[source,properties] +---- +server.aliases.prod.name=prod +---- + +A look-up to the `aliases` `Map` with the key `localhost`, `any` or any other key, returns a `Alias` instance, where +`Alias.name` is `localhost`, which is the default value. A look-up to `prod` returns a `Alias` instance, where +`Alias.name` is `prod` because the property is defined in the configuration as `server.aliases.prod.name=prod`. + +The `Map` can only iterate and size the defined keys. In this case, the `aliases` `Map` only iterates the `prod` key, +and the size is `1`. + +[source,java] +---- +Server server = config.getConfigMapping(Server.class); +Map localhost = server.aliases().get("localhost"); +Map any = server.aliases().get("any"); +Map prod = server.aliases().get("prod"); +---- + +== Optionals + +* A mapping can wrap any complex type with an `Optional` +* `Optional` mappings do not require the configuration path and value to be present + +== Secrets + +* A mapping can mark a member type as a secret with `Secret++<++T++>++`: + +[source,java] +---- +@ConfigMapping(prefix = "credentials") +public interface Credentials { + String username(); + + Secret password(); +} +---- + +A `Secret` value modifies the behaviour of the config system by: + +* Omitting the name of the secret in `SmallRyeConfig#getPropertyNames()` +* Omitting the name and value of the secret in the mapping `toString` method +* Throwing a `SecurityException` when trying to retrieve the value via `SmallRyeConfig` programmatic API + +A Secret can be of any type that can be converted by a registered `org.eclipse.microprofile.config.spi.Converter` of +the same type. + +== toString, equals, hashcode + +If the config mapping contains a `toString` method declaration, the config mapping instance will include a proper +implementation of the `toString` method. The `equals` and `hashcode` methods are included automatically. + +[CAUTION] +==== + +Do not include a `toString` declaration in a config mapping with sensitive information. + +==== + +== Validation + +A config mapping may combine annotations from https://beanvalidation.org/[Bean Validation] to validate configuration +properties values. + +[source,java] +---- +@ConfigMapping(prefix = "server") +interface Server { + @Size(min = 2, max = 20) + String host(); + + @Max(10000) + int port(); +} +---- + +The application startup fails with a `io.smallrye.config.ConfigValidationException` if the configuration properties +values do not follow the contraints defined in `Server`. + +[NOTE] +==== + +For validation to work, the `smallrye-config-validator` dependency is required in the classpath. + +==== diff --git a/content/docs/smallrye-config/latest/config/profiles.adoc b/content/docs/smallrye-config/latest/config/profiles.adoc new file mode 100644 index 000000000..ca2dd361e --- /dev/null +++ b/content/docs/smallrye-config/latest/config/profiles.adoc @@ -0,0 +1,192 @@ += Profiles +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Applications often require different configurations depending on the target environment. For example, the local +development environment may be different from the production environment. Profiles allow for multiple configurations +in the same file or separate files and select between them via a profile name. + +== Profile aware properties + +To be able to set properties with the same name, each property needs to be prefixed with a percentage sign `%` followed +by the profile name and a dot `.` in the syntax `%++{++profile-name}.config.name`: + +.META-INF/microprofile-config.properties +[source,properties] +---- +http.port=8080 +%dev.http.port=8181 +---- + +To activate the profile `dev`, the configuration `smallrye.config.profile=dev` has to be set into any valid +`ConfigSource`. + +Any lookup to the `http.port` property name will first search by the active profile name `%dev.http.port` and then +fallback to `http.port` if no value is present. In this case a lookup to the property `http.port` with the `dev` profile +active, yields the value `8181`. + +[WARNING] +==== + +The profile must be set in one of the primary sources (system properties, environment variables, +`application.properties`, or any other source that does not require configuration) to determine the proper +configuration. + +==== + +== Profile aware files + +Properties for a specific profile may reside in a `microprofile-config-++{++profile}.properties` named file. The previous +example can be expressed as: + +.META-INF/microprofile-config.properties +[source,properties] +---- +http.port=8080 +---- + +.META-INF/microprofile-config-dev.properties +[source,properties] +---- +http.port=8181 +---- + +In this style, the property names in the profile aware file do not need to be prefixed with the profile name. + +[NOTE] +==== + +Properties in the profile aware file have priority over profile aware properties defined in the main file. + +==== + +[WARNING] +==== + +Do not use Profile aware files to set `smallrye.config.profile`. This will not work because the +the profile is required in advance to load the profile aware files. + +==== + +== Priority + +Profile lookups are only valid if the `ConfigSource` has a higher ordinal than a lookup to the regular configuration +name. Consider: + +.main.properties +[source,properties] +---- +config_ordinal=1000 +http.port=8080 +---- + +.profile.properties +[source,properties] +---- +config_ordinal=100 +%dev.http.port=8181 +---- + +Even with the profile `dev` active, the lookup value for `my.prop` is `1234`. This prevents lower ordinal sources to +set a profile property value that cannot be overridden unless the profile property is also overridden. + +== Multiple Profiles + +Multiple Profiles may be active at the same time. The configuration `smallrye.config.profile` accepts a comma-separated +list of profile names: `smallrye.config.profile=common,dev`. Both `common` and `dev` are separate profiles. + +When multiple profiles are active, the rules for profile configuration are the same. If two profiles define the same +configuration, then the last listed profile has priority. Consider: + +[source,properties] +---- +smallrye.config.profile=common,dev + +my.prop=1234 +%common.my.prop=1234 +%dev.my.prop=5678 + +%common.commom.prop=common +%dev.dev.prop=dev +%test.test.prop=test +---- + +Then + +* `common.prop` value is `common` +* `dev.prop` value is `dev` +* `my.prop` value is `5678` +* `test.prop` does not have a value + +It is also possible to define multiple profile properties, with a comma-separated list of profile names: + +[source,properties] +---- +%prod,dev.my.prop=1234 +---- + +The property name `common.prop` is active in both `dev` and `prod` profile. If the same property name exists in +multiple profile properties then, the property name with the most specific profile wins: + +[source,properties] +---- +smallrye.config.profile=dev + +%prod,dev.my.prop=1234 + +%dev.my.prop=5678 +---- + +Then `my.prop` value is `5678`. + +== Parent Profile + +A Parent Profile adds multiple levels of hierarchy to the current profile. The configuration +`smallrye.config.profile.parent` also acccepts a comma-separated list of profile names. + +When the Parent Profile is active, if a property cannot be found in the current active Profile, the config lookup +fallbacks to the Parent Profile. Consider: + +[source,properties] +---- +smallrye.config.profile=dev +smallrye.config.profile.parent=common + +my.prop=1234 +%common.my.prop=0 +%dev.my.prop=5678 + +%common.commom.prop=common +%dev.dev.prop=dev +%test.test.prop=test +---- + +Then + +* `common.prop` value is `common` +* `dev.prop` value is `dev` +* `my.prop` value is `0` +* `test.prop` does not have a value + +[WARNING] +==== + +Do not use Profile aware files to set smallrye.config.profile.parent++`++. This will not work because the +the profile is required in advance to load the profile aware files. + +==== + +=== Multi-level Hierarchy + +The Parent Profile also supports multiple levels of hierarchies: + +[source,properties] +---- +smallrye.config.profile=child +%child.smallrye.config.profile.parent=parent +%parent.smallrye.config.profile.parent=grandparent +%grandparent.smallrye.config.profile.parent=greatgrandparent +%greatgrandparent.smallrye.config.profile.parent=end +---- + +Will load the following profiles in order: `child`, `parent`, `grandparent`, `greatgrandparent`, `end` diff --git a/content/docs/smallrye-config/latest/config/secret-keys.adoc b/content/docs/smallrye-config/latest/config/secret-keys.adoc new file mode 100644 index 000000000..df27211bf --- /dev/null +++ b/content/docs/smallrye-config/latest/config/secret-keys.adoc @@ -0,0 +1,232 @@ += Secret Keys +:version-root: ../ +include::{version-root}_attributes.adoc[] + +== Secret Keys Expressions + +In SmallRye Config, a secret configuration may be expressed as `$++{++handler::value}`, where the `handler` is the name of +a `io.smallrye.config.SecretKeysHandler` to decode or decrypt the `value` separated by a double colon `::`. + +It is possible to create a custom `SecretKeysHandler` and provide different ways to decode or decrypt configuration +values. + +A custom `SecretKeysHandler` requires an implementation of `io.smallrye.config.SecretKeysHandler` or +`io.smallrye.config.SecretKeysHandlerFactory`. Each implementation requires registration via the `ServiceLoader` +mechanism, either in `META-INF/services/io.smallrye.config.SecretKeysHandler` or +`META-INF/services/io.smallrye.config.SecretKeysHandlerFactory` files. + +=== Custom `SecretKeysHandler` + +A direct `SecretKeysHandler` implementation is suitable when the handler needs no configuration of its own: + +[source,java] +---- +public class Base64SecretKeysHandler implements SecretKeysHandler { + @Override + public String decode(final String secret) { + return new String(Base64.getDecoder().decode(secret)); + } + + @Override + public String getName() { + return "base64"; + } +} +---- + +.META-INF/services/io.smallrye.config.SecretKeysHandler + +.... +org.acme.config.Base64SecretKeysHandler +.... + +A secret value encoded with the `base64` handler can then be expressed as: + +[source,properties] +---- +my.secret=${base64::SGVsbG8gV29ybGQ=} +---- + +=== `SecretKeysHandlerFactory` + +When a handler requires configuration from other config sources (for example, a key or a credential read from +the config), use `SecretKeysHandlerFactory` instead. The factory receives a `ConfigSourceContext` that provides +access to all config sources initialized before the factory runs: + +[source,java] +---- +public class VaultSecretKeysHandlerFactory implements SecretKeysHandlerFactory { + @Override + public SecretKeysHandler getSecretKeysHandler(final ConfigSourceContext context) { + ConfigValue token = context.getValue("vault.token"); + return new VaultSecretKeysHandler(token.getValue()); + } + + @Override + public String getName() { + return "vault"; + } +} +---- + +.META-INF/services/io.smallrye.config.SecretKeysHandlerFactory +[source,properties] +---- +org.acme.config.VaultSecretKeysHandlerFactory +---- + +=== `LazySecretKeysHandler` + +`SecretKeysHandlerFactory` initializes during the first phase of `SmallRyeConfig` bootstrap, alongside regular +`ConfigSource` and `ConfigSourceProvider` registrations. This means that config values produced by a +`ConfigSourceFactory` are *not* yet available when the factory's `getSecretKeysHandler` is called. + +For handlers that depend on sources provided by a `ConfigSourceFactory`, wrap an inner `SecretKeysHandlerFactory` +in a `SecretKeysHandlerFactory.LazySecretKeysHandler`. The inner factory's `getSecretKeysHandler` is only invoked +the first time a value actually needs to be decoded, by which point all sources — including those from +`ConfigSourceFactory` — are fully initialized: + +[source,java] +---- +public class VaultSecretKeysHandlerFactory implements SecretKeysHandlerFactory { + @Override + public SecretKeysHandler getSecretKeysHandler(final ConfigSourceContext context) { + return new LazySecretKeysHandler(new SecretKeysHandlerFactory() { + @Override + public SecretKeysHandler getSecretKeysHandler(final ConfigSourceContext context) { + // This runs lazily, after all sources are ready. + ConfigValue token = context.getValue("vault.token"); + return new VaultSecretKeysHandler(token.getValue()); + } + + @Override + public String getName() { + return "vault"; + } + }); + } + + @Override + public String getName() { + return "vault"; + } +} +---- + +[WARNING] +==== + +The inner factory wrapped by `LazySecretKeysHandler` is what defers initialization. Do not call +`context.getValue` in the outer `getSecretKeysHandler`; only the inner factory's `getSecretKeysHandler` (invoked lazily) may resolve configuration values. + +==== + +[CAUTION] +==== + +It is not possible to mix Secret Keys Expressions with Property Expressions. + +==== + +=== Crypto + +The `smallrye-config-crypto` artifact contains a few out-of-the-box `SecretKeysHandler`s ready for use. It requires +the following dependency: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.config + smallrye-config-crypto + {version} + +---- + +==== AES/GCM/NoPadding `$++{++aes-gcm-nopadding::...}` + +* The encoding length is 128. +* The secret and the encryption key (without padding) must be base 64 encoded. + +[NOTE] +==== + +.application.properties +[source,properties] +---- +smallrye.config.secret-handler.aes-gcm-nopadding.encryption-key=DDne5obnfH1RSeTg71xSZg + +my.secret=${aes-gcm-nopadding::DLTb_9zxThxeT5iAQqswEl5Dn1ju4FdM9hIyVip35t5V} +---- + +The `$++{++aes-gcm-nopadding::...}` `SecretKeyHandler` requires +`smallrye.config.secret-handler.aes-gcm-nopadding.encryption-key` configuration to state the encryption key to be +used by the `aes-gcm-nopaddin` handler. + +A lookup to `my.secret` will use the `SecretKeysHandler` name `aes-gcm-nopadding` to decode the value +`DJNrZ6LfpupFv6QbXyXhvzD8eVDnDa++_++kTliQBpuzTobDZxlg`. + +==== + +[NOTE] +==== + +It is possible to generate the encrypted secret with the following http://jbang.dev/[JBang] script: + +[source,shell] +---- +jbang https://raw.githubusercontent.com/smallrye/smallrye-config/main/documentation/src/main/docs/config/secret-handlers/encryptor.java -s= -k=` +---- + +==== + +===== Configuration + +[width="100%",cols="90%,5%,5%",options="header",] +|=== +|Configuration Property |Type |Default +|`smallrye.config.secret-handler.aes-gcm-nopadding.encryption-key` + +The encryption key to use to decode secrets encoded by the `AES/GCM/NoPadding` algorithm. |String | +|`"smallrye.config.secret-handler.aes-gcm-nopadding.encryption-key-decode"` + +Decode the encryption key in Base64, if the plain text key was used to encrypt the secret. |boolean |false +|=== + +== Secret Keys Names + +When configuration properties contain passwords or other kinds of secrets, Smallrye Config can hide them to prevent +accidental exposure of such values. + +*This is no way a replacement for securing secrets.* Proper security mechanisms must still be used to secure +secrets. However, there is still the fundamental problem that passwords and secrets are generally encoded simply as +strings. Secret Keys provides a way to "lock" the configuration so that secrets do not appear unless explicitly enabled. + +To mark specific keys as secrets, register an instance of `io.smallrye.config.SecretKeysConfigSourceInterceptor` by +using the interceptor factory as follows: + +[source,java] +---- +public class SecretKeysConfigInterceptorFactory implements ConfigSourceInterceptorFactory { + @Override + public ConfigSourceInterceptor getInterceptor(ConfigSourceInterceptorContext context) { + return new SecretKeysConfigSourceInterceptor(Set.of("secret")); + } +} +---- + +Register the factory so that it can be found at runtime by creating a +`META-INF/services/io.smallrye.config.ConfigSourceInterceptorFactory` file that contains the fully qualified name of +this factory class. + +From this point forward, every lookup to the configuration name `secret` will throw a `SecurityException`. + +Access the Secret Keys using the APIs `io.smallrye.config.SecretKeys#doUnlocked(java.lang.Runnable)` +and `io.smallrye.config.SecretKeys#doUnlocked(java.util.function.Supplier++<++T++>++)`. + +[source,java] +---- +String secretValue = SecretKeys.doUnlocked(() -> { + config.getValue("secret", String.class); +}); +---- + +Secret Keys are only unlocked in the context of `doUnlocked`. Once the execution completes, the secrets become locked +again. diff --git a/content/docs/smallrye-config/latest/converters/custom.adoc b/content/docs/smallrye-config/latest/converters/custom.adoc new file mode 100644 index 000000000..e95f13ad9 --- /dev/null +++ b/content/docs/smallrye-config/latest/converters/custom.adoc @@ -0,0 +1,101 @@ += Custom `Converter` +:version-root: ../ +include::{version-root}_attributes.adoc[] + +It is possible to create a custom `Converter` type as specified in +https://github.com/eclipse/microprofile-config/[MicroProfile Config]. + +A custom `Converter` requires an implementation of `org.eclipse.microprofile.config.spi.Converter`. Each implementation +requires registration via the `ServiceLoader` mechanism in the +`META-INF/services/org.eclipse.microprofile.config.spi.Converter` file. Consider: + +[source,java] +---- +package org.acme.config; + +public class CustomValue { + + private final int number; + + public CustomValue(int number) { + this.number = number; + } + + public int getNumber() { + return number; + } +} +---- + +The corresponding converter can look like: + +[source,java] +---- +package org.acme.config; + +import org.eclipse.microprofile.config.spi.Converter; + +public class CustomValueConverter implements Converter { + + @Override + public CustomValue convert(String value) { + return new CustomValue(Integer.parseInt(value)); + } +} +---- + +And registration in: + +.META-INF/services/org.eclipse.microprofile.config.spi.Converter +[source,properties] +---- +org.acme.config.CustomValueConverter +---- + +[WARNING] +==== + +The custom `Converter` class must be `public`, must have a `public` constructor with no arguments, and must not be +abstract. + +==== + +The `CustomValueConverter` converts the configuration value to the `CustomValue` type automatically. + +[source,java] +---- +Config config = Config.getOrCreate(); +CustomValue value = config.getValue("custom.value", CustomValue.class); +---- + +The `jakarta.annotation.Priority` annotation overrides the `Converter` priority and change converters precedence to fine +tune the execution order. By default, if no `@Priority` is specified by the `Converter`, the converter is registered +with a priority of `100`. Consider: + +[source,java] +---- +package org.acme.config; + +import jakarta.annotation.Priority; +import org.eclipse.microprofile.config.spi.Converter; + +@Priority(150) +public class SecretConverter implements Converter { + + @Override + public CustomValue convert(String value) { + final int secretNumber; + if (value.startsFrom("OBF:")) { + secretNumber = Integer.parseInt(SecretDecoder.decode(value)); + } else { + secretNumber = Integer.parseInt(value); + } + + return new CustomValue(secretNumber); + } +} +---- + +Two `Converter`s, (`CustomValueConverter` and `SecretConverter`) can convert the same type `CustomValue`. Since +`SecretConverter` has a priority of `150`, it will be used instead of a `CustomValueConverter` which has a default +priority of `100` (no annotation). diff --git a/content/docs/smallrye-config/latest/extensions/fallback.adoc b/content/docs/smallrye-config/latest/extensions/fallback.adoc new file mode 100644 index 000000000..05af7251d --- /dev/null +++ b/content/docs/smallrye-config/latest/extensions/fallback.adoc @@ -0,0 +1,59 @@ += Fallback +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The `io.smallrye.config.FallbackConfigSourceInterceptor` allows to fall back to another configuration name, by +providing a transformation function or just a simple key value map. + +When a configuration name does not exist, there might be another configuration name that the config can +fall back to provide the same expected behavior. The fallback function is only applied if the original resolved +configuration name is not found and resolved to the fallback name. + +[source,java] +---- +package org.acme.config; + +import io.smallrye.config.FallbackConfigSourceInterceptor; + +public class MicroProfileConfigFallbackInterceptor extends FallbackConfigSourceInterceptor { + public MicroProfileConfigFallbackInterceptor() { + super(name -> name.startsWith("mp.config") ? + name.replaceAll("mp\\.config", "smallrye.config") : + name); + } +} +---- + +And registration in: + +.META-INF/services/io.smallrye.config.ConfigSourceInterceptor +[source,properties] +---- +org.acme.config.MicroProfileConfigFallbackInterceptor +---- + +The `MicroProfileConfigFallbackInterceptor` can fallback configuration names in the `mp.config` namespace +to the `smallrye.config` namespace. + +[NOTE] +==== + +.application.properties +[source,properties] +---- +mp.config.profile=test +smallrye.config.profile=prod +---- + +A lookup to `mp.config.profile` returns the value `test`. + +.application.properties +[source,properties] +---- +smallrye.config.profile=prod +---- + +A lookup to `mp.config.profile` returns the value `prod`. The config is not able to find a value for +`mp.config.profile`, so the interceptor fallbacks and lookups the value of `smallrye.config.profile`. + +==== diff --git a/content/docs/smallrye-config/latest/extensions/interceptors.adoc b/content/docs/smallrye-config/latest/extensions/interceptors.adoc new file mode 100644 index 000000000..8811bf0dc --- /dev/null +++ b/content/docs/smallrye-config/latest/extensions/interceptors.adoc @@ -0,0 +1,179 @@ += Interceptors +:version-root: ../ +include::{version-root}_attributes.adoc[] + +SmallRye Config provides an interceptor chain that hooks into the configuration values resolution. This is useful to +implement features like xref:config/profiles.adoc[Profiles] , xref:config/expressions.adoc[Property Expressions], or just +logging to find out where the config value was loaded from. + +An interceptor can be created by implementing the +https://github.com/smallrye/smallrye-config/blob/main/implementation/src/main/java/io/smallrye/config/ConfigSourceInterceptor.java[ConfigSourceInterceptor] +interface. + +An interceptor requires an implementation of `io.smallrye.config.ConfigSourceInterceptor`. Each implementation requires +registration via the `ServiceLoader` mechanism in the `META-INF/services/io.smallrye.config.ConfigSourceInterceptor` +file. Alternatively, interceptors may be registered via the Programmatic API in +`SmallRyeConfigBuilder#withInterceptors`. + +`io.smallrye.config.ConfigSourceInterceptor` exposes two interception points: + +* `ConfigValue getValue(ConfigSourceInterceptorContext context, String name)` — intercepts the resolution of a +configuration value by name. +* `Iterator++<++String++>++ iterateNames(ConfigSourceInterceptorContext context)` — intercepts the iteration of all known +configuration property names. + +The `ConfigSourceInterceptorContext` is used to proceed with the interceptor chain. Calling `context.proceed(name)` +continues to the next interceptor in the chain. The chain can be short-circuited by returning a custom instance of +`io.smallrye.config.ConfigValue` from `getValue`, or a custom `Iterator++<++String++>++` from `iterateNames`. The `ConfigValue` +objects hold information about the key name, value, config source origin and ordinal. + +[NOTE] +==== + +The interceptor chain is applied before any conversion is performed on the configuration value. + +==== + +[source,java] +---- +package org.acme.config; + +import jakarta.annotation.Priority; + +import io.smallrye.config.ConfigSourceInterceptor; +import io.smallrye.config.Priorities; + +@Priority(Priorities.APPLICATION) +public class LoggingConfigSourceInterceptor implements ConfigSourceInterceptor { + @Override + public ConfigValue getValue(final ConfigSourceInterceptorContext context, final String name) { + ConfigValue configValue = context.proceed(name); + if (configValue != null) { + System.out.println("Looked up: " + configValue.getName() + "=" + configValue.getValue()); + } else { + System.out.println("Not found: " + name); + } + return configValue; + } +} +---- + +And registration in: + +.META-INF/services/io.smallrye.config.ConfigSourceInterceptor +[source,properties] +---- +org.acme.config.LoggingConfigSourceInterceptor +---- + +The `LoggingConfigSourceInterceptor` logs each configuration name lookup. The logged information includes the config +name and value, and (from the resolved `ConfigValue`) the config source origin and location if they exist. + +== `iterateNames` + +The `iterateNames` method intercepts the set of configuration property names visible to the rest of the chain. The +default implementation simply delegates to `context.iterateNames()`. Override it to add, remove, or transform names. + +A common use case is hiding sensitive property names from enumeration while still allowing their values to be +retrieved: + +[source,java] +---- +package org.acme.config; + +import java.util.Iterator; + +import jakarta.annotation.Priority; + +import io.smallrye.config.ConfigSourceInterceptor; +import io.smallrye.config.ConfigSourceInterceptorContext; +import io.smallrye.config.Priorities; + +@Priority(Priorities.APPLICATION) +public class HidingConfigSourceInterceptor implements ConfigSourceInterceptor { + @Override + public ConfigValue getValue(final ConfigSourceInterceptorContext context, final String name) { + return context.proceed(name); + } + + @Override + public Iterator iterateNames(final ConfigSourceInterceptorContext context) { + Iterator names = context.iterateNames(); + return new Iterator<>() { + @Override + public boolean hasNext() { + return names.hasNext(); + } + + @Override + public String next() { + String name = names.next(); + // filter out any name starting with "secret." + return name.startsWith("secret.") ? next() : name; + } + }; + } +} +---- + +[NOTE] +==== + +`iterateNames` affects the results of `Config#getPropertyNames()` and is used by Config Mappings to discover +`Map` keys. Interceptors that hide names will also prevent those names from being mapped into `Map` entries. + +==== + +== `ConfigSourceInterceptorContext` + +`ConfigSourceInterceptorContext` provides two ways to continue the chain: + +* `proceed(String name)` — passes the lookup to the *next* interceptor in the chain. This is the standard method +to use in most interceptors. +* `restart(String name)` — re-invokes the *first* interceptor in the chain from the beginning. This is intended for +relocating or compatibility interceptors that resolve a new name and need the full chain to re-evaluate it, including +profile expansion and expression resolution. Passing the original name to `restart` can cause an infinite loop, so +care must be taken. + +Example — an interceptor that resolves an alias name and restarts the full chain for the resolved name: + +[source,java] +---- +package org.acme.config; + +import jakarta.annotation.Priority; + +import io.smallrye.config.ConfigSourceInterceptor; +import io.smallrye.config.ConfigSourceInterceptorContext; +import io.smallrye.config.ConfigValue; +import io.smallrye.config.Priorities; + +@Priority(Priorities.APPLICATION) +public class AliasConfigSourceInterceptor implements ConfigSourceInterceptor { + @Override + public ConfigValue getValue(final ConfigSourceInterceptorContext context, final String name) { + if (name.equals("app.host")) { + // Restart the chain with the canonical name so profiles and + // expressions are re-applied to the resolved name. + return context.restart("server.host"); + } + return context.proceed(name); + } +} +---- + +Interceptors may also be created with an implementation of `io.smallrye.config.ConfigSourceInterceptorFactory`. Each +implementation requires registration via the `ServiceLoader` mechanism in the +`META-INF/services/io.smallrye.config.ConfigSourceInterceptorFactory` file. Alternatively, interceptors factories may +be registered via the Programmatic API in `SmallRyeConfigBuilder#withInterceptorFactories`. + +The `ConfigSourceInterceptorFactory` can initialize an interceptor with access to the current chain +(so it can be used to configure the interceptor and retrieve configuration values) and set the priority. + +A `ConfigSourceInterceptor` implementation class can specify a priority by way of the standard +`jakarta.annotation.Priority` annotation. If no priority is explicitly assigned, the default priority value of +`io.smallrye.config.Priorities.APPLICATION` is assumed. If multiple interceptors are registered with the same priority, +then their execution order may be non-deterministic. + +A collection of built-in priority constants can be found in `io.smallrye.config.Priorities`. It is recommended to +use `io.smallrye.config.Priorities.APPLICATION` as a baseline for user defined interceptors. diff --git a/content/docs/smallrye-config/latest/extensions/logging.adoc b/content/docs/smallrye-config/latest/extensions/logging.adoc new file mode 100644 index 000000000..3a42e6573 --- /dev/null +++ b/content/docs/smallrye-config/latest/extensions/logging.adoc @@ -0,0 +1,15 @@ += Logging +:version-root: ../ +include::{version-root}_attributes.adoc[] + +== LoggingConfigSourceInterceptor + +The `io.smallrye.config.LoggingConfigSourceInterceptor` logs lookups of configuration names in the provided logging +platform. The log information includes config name and value, the config source origin and location if it exists. + +The log is done as `debug`, so the debug threshold must be set to `debug` for the `io.smallrye.config` appender to +display the logs. + +This requires registration via the `ServiceLoader` mechanism in the +`META-INF/services/io.smallrye.config.ConfigSourceInterceptorFactory` file of the +`io.smallrye.config.LoggingConfigSourceInterceptor` interceptor. diff --git a/content/docs/smallrye-config/latest/extensions/relocate.adoc b/content/docs/smallrye-config/latest/extensions/relocate.adoc new file mode 100644 index 000000000..e33ee4f6a --- /dev/null +++ b/content/docs/smallrye-config/latest/extensions/relocate.adoc @@ -0,0 +1,51 @@ += Relocate +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The `io.smallrye.config.RelocateConfigSourceInterceptor` allows to relocate a configuration name to another name, by +providing a transformation function or just a simple key value map. + +When a configuration key is renamed, lookup needs to happen on the new name, but also on the old name if the +config sources are not updated yet. The relocation function gives priority to the new resolved configuration name or +resolves to the old name if no value is found under the new relocation name. + +[source,java] +---- +package org.acme.config; + +import io.smallrye.config.RelocateConfigSourceInterceptor; + +public class MicroProfileConfigRelocateInterceptor extends RelocateConfigSourceInterceptor { + public MicroProfileConfigRelocateInterceptor() { + super(name -> name.startsWith("mp.config") ? + name.replaceAll("mp\\.config", "smallrye.config") : + name); + } +} +---- + +And registration in: + +.META-INF/services/io.smallrye.config.ConfigSourceInterceptor +[source,properties] +---- +org.acme.config.MicroProfileConfigRelocateInterceptor +---- + +The `MicroProfileConfigRelocateInterceptor` can relocate configuration names in the `mp.config` namespace +to the `smallrye.config` namespace. + +[NOTE] +==== + +.application.properties +[source,properties] +---- +mp.config.profile=test +smallrye.config.profile=prod +---- + +A lookup to `mp.config.profile` returns the value `prod`. The config finds a valid value in the relocated name +`smallrye.config.profile`, so the interceptor will use this value instead of the one in `mp.config.profile`. + +==== diff --git a/content/docs/smallrye-config/latest/index.adoc b/content/docs/smallrye-config/latest/index.adoc new file mode 100644 index 000000000..dca56db0f --- /dev/null +++ b/content/docs/smallrye-config/latest/index.adoc @@ -0,0 +1,97 @@ += SmallRye Config +include::_attributes.adoc[] + +*SmallRye Config* is a library that provides a way to configure applications, frameworks and containers. It is used +in applications servers like https://wildfly.org/[WildFly], https://openliberty.io[Open Liberty] and +https://tomee.apache.org[TomEE] or frameworks like https://quarkus.io[Quarkus]. It can also be used completely +standalone in any Java application, which makes it a very flexible library. + +It follows the https://github.com/eclipse/microprofile-config/[MicroProfile Config] specification to provide +the initial config foundations and expands with it own concepts to cover a wide range of use cases observed in the +configuration space. + +== Use SmallRye Config in a Java application + +Add the _dependency_ to your project using your preferred build tool: + +[.tabs] +==== + +Maven:: ++ +-- + +[source,xml,subs="attributes+"] +---- + + io.smallrye.config + smallrye-config + {version} + +---- + +-- + +Gradle (Groovy):: ++ +-- + +[source,groovy,subs="attributes+"] +---- +implementation 'io.smallrye.config:smallrye-config:{version}' +---- + +-- + +Gradle (Kotlin):: ++ +-- + +[source,kotlin,subs="attributes+"] +---- +implementation("io.smallrye.config:smallrye-config:{version}") +---- + +-- + +JBang:: ++ +-- + +[source,java,subs="attributes+"] +---- +//DEPS io.smallrye.config:smallrye-config:{version} +---- + +-- + +==== + +And retrieve a `SmallRyeConfig` instance with: + +[source,java] +---- +Config config = io.smallrye.config.Config.getOrCreate(); +---- + +[NOTE] +==== + +The `SmallRyeConfig` instance will be created and registered to the context class loader if no such configuration +is already created and registered. + +==== + +Or build your own: + +[source,java] +---- +SmallRyeConfig config = new SmallRyeConfigBuilder().build(); +---- + +[NOTE] +==== + +`io.smallrye.config.Config` is the entry point to all the config capabilities provided by SmallRye Config. + +==== diff --git a/content/docs/smallrye-config/main/_attributes.adoc b/content/docs/smallrye-config/main/_attributes.adoc new file mode 100644 index 000000000..0aefa5869 --- /dev/null +++ b/content/docs/smallrye-config/main/_attributes.adoc @@ -0,0 +1,21 @@ +:project-version: 4.0.0 +// Attributes shared by every page. smallrye.io overrides the ones it needs when it publishes. +// Pages in subdirectories set :version-root: (../ per level) before including this file. +ifndef::version-root[:version-root:] +:idprefix: +:idseparator: - +:sectanchors: +:examples-dir: {version-root}_examples +:partialsdir: {version-root}_partials +:project-name: SmallRye Config +:javadoc-version: latest +:version: {project-version} +:generated-dir: {version-root}_generated + +// Added by smallrye.io (tools/docs/Docs.java sync) +:javadoc-version: 4.0.0 +:relfileprefix: /docs/smallrye-config/main/ +:imagesdir: /docs/smallrye-config/main +:docs-project: smallrye-config +:docs-version: main +:docs-ref: main diff --git a/content/docs/smallrye-config/main/_nav.yml b/content/docs/smallrye-config/main/_nav.yml new file mode 100644 index 000000000..6a14b50be --- /dev/null +++ b/content/docs/smallrye-config/main/_nav.yml @@ -0,0 +1,66 @@ +title: SmallRye Config +nav: +- title: Home + page: index +- title: Config + items: + - title: Getting Started + page: config/getting-started + - title: Expressions + page: config/expressions + - title: Profiles + page: config/profiles + - title: Environment Variables + page: config/environment-variables + - title: Indexed Properties + page: config/indexed-properties + - title: ConfigValue + page: config/config-value + - title: Mappings + page: config/mappings + - title: Instance Builder + page: config/instance-builder + - title: Map Support + page: config/map-support + - title: Secret Keys + page: config/secret-keys + - title: Customizer + page: config/customizer + - title: Configuration Reference + page: config/configuration +- title: Config Sources + items: + - title: Custom + page: config-sources/custom + - title: Factories + page: config-sources/factories + - title: Locations + page: config-sources/locations + - title: YAML + page: config-sources/yaml + - title: TOML + page: config-sources/toml + - title: JSON + page: config-sources/json + - title: FileSystem + page: config-sources/filesystem + - title: ZooKeeper + page: config-sources/zookeeper + - title: HOCON + page: config-sources/hocon + - title: KeyStore + page: config-sources/keystore +- title: Converters + items: + - title: Custom + page: converters/custom +- title: Extensions + items: + - title: Interceptors + page: extensions/interceptors + - title: Fallback + page: extensions/fallback + - title: Relocate + page: extensions/relocate + - title: Logging + page: extensions/logging diff --git a/content/docs/smallrye-config/main/assets/smallrye-icon.svg b/content/docs/smallrye-config/main/assets/smallrye-icon.svg new file mode 100644 index 000000000..c10b011e0 --- /dev/null +++ b/content/docs/smallrye-config/main/assets/smallrye-icon.svg @@ -0,0 +1,21 @@ + + + + + SmallRye Icon + + + + + + + + + diff --git a/content/docs/smallrye-config/main/config-sources/custom.adoc b/content/docs/smallrye-config/main/config-sources/custom.adoc new file mode 100644 index 000000000..bd9cc1444 --- /dev/null +++ b/content/docs/smallrye-config/main/config-sources/custom.adoc @@ -0,0 +1,166 @@ += Config Sources +:version-root: ../ +include::{version-root}_attributes.adoc[] + +== Custom `ConfigSource` + +It’s possible to create a custom ConfigSource as specified in +https://github.com/eclipse/microprofile-config/[MicroProfile Config]. + +With a Custom `ConfigSource` it is possible to read additional configuration values and add them to the `Config` +instance in a defined ordinal. This allows overriding values from other sources or falling back to other values. + +A custom `ConfigSource` requires an implementation of `org.eclipse.microprofile.config.spi.ConfigSource` or +`org.eclipse.microprofile.config.spi.ConfigSourceProvider`. Each implementation requires registration via the +`ServiceLoader` mechanism, either in `META-INF/services/org.eclipse.microprofile.config.spi.ConfigSource` or +`META-INF/services/org.eclipse.microprofile.config.spi.ConfigSourceProvider` files. + +Consider a simple in-memory ConfigSource: + +[source,java] +---- +package org.acme.config; + +import org.eclipse.microprofile.config.spi.ConfigSource; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +public class InMemoryConfigSource implements ConfigSource { + private static final Map configuration = new HashMap<>(); + + static { + configuration.put("my.prop", "1234"); + } + + @Override + public int getOrdinal() { + return 350; + } + + @Override + public Set getPropertyNames() { + return configuration.keySet(); + } + + @Override + public String getValue(final String propertyName) { + return configuration.get(propertyName); + } + + @Override + public String getName() { + return InMemoryConfigSource.class.getSimpleName(); + } +} +---- + +And registration in: + +.META-INF/services/org.eclipse.microprofile.config.spi.ConfigSource +[source,properties] +---- +org.acme.config.InMemoryConfigSource +---- + +The `InMemoryConfigSource` will be ordered between the `System properties` config source, and the +`Environment variables` config source due to the `350` ordinal. + +In this case, `my.prop` from `InMemoryConfigSource` will only be used if the `Config` instance is unable to find a +value in `System Properties`, ignoring all the other lower ordinal config sources. + +== `ConfigSourceFactory` + +Another way to create a `ConfigSource` is via the SmallRye Config `io.smallrye.config.ConfigSourceFactory` API. The +difference between the SmallRye Config factory and the standard way to create a `ConfigSource` as specified in +https://github.com/eclipse/microprofile-config/[MicroProfile Config], is the factory ability to provide a context +with access to the current configuration. + +Each implementation of `io.smallrye.config.ConfigSourceFactory` requires registration via the `ServiceLoader` mechanism +in the `META-INF/services/io.smallrye.config.ConfigSourceFactory` file. + +[source,java] +---- +package org.acme.config; + +import java.util.Collections; +import java.util.OptionalInt; + +import org.eclipse.microprofile.config.spi.ConfigSource; + +import io.smallrye.config.ConfigSourceContext; +import io.smallrye.config.ConfigSourceFactory; +import io.smallrye.config.ConfigValue; +import io.smallrye.config.PropertiesConfigSource; + +public class URLConfigSourceFactory implements ConfigSourceFactory { + @Override + public Iterable getConfigSources(final ConfigSourceContext context) { + final ConfigValue value = context.getValue("config.url"); + if (value == null || value.getValue() == null) { + return Collections.emptyList(); + } + + try { + return Collections.singletonList(new PropertiesConfigSource(new URL(value.getValue()))); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public OptionalInt getPriority() { + return OptionalInt.of(290); + } +} +---- + +And registration in: + +.META-INF/services/io.smallrye.config.ConfigSourceFactory +[source,properties] +---- +org.acme.config.URLConfigSourceFactory +---- + +By implementing `io.smallrye.config.ConfigSourceFactory`, a list of `ConfigSource` may be provided via the +`Iterable++<++ConfigSource++>++ getConfigSources(ConfigSourceContext context)` method. The `ConfigSourceFactory` may also +assign a priority by overriding the method `OptionalInt getPriority()`. The priority value is used to sort multiple +`io.smallrye.config.ConfigSourceFactory` (if found). Higher the value, higher the priority. + +[WARNING] +==== + +The `io.smallrye.config.ConfigSourceFactory` priority does not affect the `ConfigSource` ordinal. These are sorted +independently. + +==== + +When the Factory is initializing, the provided `ConfigSourceContext` may call the method +`ConfigValue getValue(String name)`. This method looks up configuration names in all `ConfigSource`s that were already +initialized by the `Config` instance, including sources with lower ordinals than the ones defined in the +`ConfigSourceFactory`. This means that a `Config` instance is initialized in two steps: first all `ConfigSource` and +`ConfigSourceProvider` and then the `ConfigSourceFactory`. Only configuation values found in the first step are avaible +to the ++`++ConfigSourceFactory. + +The `ConfigSource` list provided by a `ConfigSourceFactory` is not taken into consideration to +configure other sources produced by a lower priority `ConfigSourceFactory`. + +== Override `ConfigSource` ordinal + +The special configuration property name `config++_++ordinal` can be set in any `ConfigSource` to override its default +ordinal. + +For instance, setting the system property `-Dconfig++_++ordinal=200` will override the ordinal for the `System properties` +config source and move it to be looked up after the `Environment Variables` config source. + +== Properties + +The https://github.com/smallrye/smallrye-config/blob/main/implementation/src/main/java/io/smallrye/config/PropertiesConfigSource.java[PropertiesConfigSource] +creates a `ConfigSource` from Java `Properties`, `Map++<++String, String++>++` objects or a `.properties` file (referenced by +its URL). + +== `.env` + +The https://github.com/smallrye/smallrye-config/blob/main/implementation/src/main/java/io/smallrye/config/DotEnvConfigSourceProvider.java[DotEnvConfigSourceProvider] create a `ConfigSource` from a `.env` file. diff --git a/content/docs/smallrye-config/main/config-sources/factories.adoc b/content/docs/smallrye-config/main/config-sources/factories.adoc new file mode 100644 index 000000000..a3c26281c --- /dev/null +++ b/content/docs/smallrye-config/main/config-sources/factories.adoc @@ -0,0 +1,130 @@ += Config Source Factory +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Another way to create a `ConfigSource` is via the `ConfigSourceFactory`. The difference between the SmallRye Config +Factory and the standard way to make a `ConfigSource` as specified in MicroProfile Config is the Factory's ability to +provide a context with access to the available configuration. With the `ConfigSourceFactory` it is possible to +bootstrap a `ConfigSource` that configures itself with previously initialized `ConfigSource's`. + +By implementing +https://github.com/smallrye/smallrye-config/blob/main/implementation/src/main/java/io/smallrye/config/ConfigSourceFactory.java[ConfigSourceFactory], +a list of `ConfigSource's` may be provided via the `Iterable++<++ConfigSource++>++ getConfigSources(ConfigSourceContext context)` method. The `ConfigSourceFactory` may also +assign a priority by overriding the default method `OptionalInt getPriority()`. The priority only sorts the factories +during initialization. After initialization, the provided `ConfigSources` will use their own ordinal and sorted with +all `ConfigSources` available in the `Config` instance. + +When the Factory initializes, the provided `ConfigSourceContext` may call the method +`ConfigValue getValue(String name)`. This method looks up configuration names in all `ConfigSource's` already +initialized by the `Config` instance, including sources with lower ordinals than the ones defined in the +`ConfigSourceFactory`. The `ConfigSourceFactory` does not consider `ConfigSources's` provided by other +`ConfigSourceFactory's` (the priority does not matter). + +Registration of a `ConfigSourceFactory` is done via the `ServiceLoader` mechanism by providing the +implementation classes in a `META-INF/services/io.smallrye.config.ConfigSourceFactory` file. Alternatively, factories +may be registered via the Programmatic API in `SmallRyeConfigBuilder#withSources`. + +A `ConfigSourceFactory` requires an implementation of `io.smallrye.config.ConfigSourceFactory`. Each implementation +requires registration via the `ServiceLoader` mechanism in the +`META-INF/services/io.smallrye.config.ConfigSourceFactory` file. Alternatively, interceptors may be registered via the +Programmatic API in `SmallRyeConfigBuilder#withSources`. + +[source,java] +---- +package org.acme.config + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Collections; +import java.util.List; + +import org.eclipse.microprofile.config.spi.ConfigSource; + +import io.smallrye.config._private.ConfigMessages; +import io.smallrye.config.PropertiesConfigSource; +import io.smallrye.config.ConfigSourceContext; +import io.smallrye.config.ConfigSourceFactory; +import io.smallrye.config.ConfigValue; + +public class FileSystemConfigSourceFactory implements ConfigSourceFactory { + @Override + public Iterable getConfigSources(final ConfigSourceContext context) { + final ConfigValue value = context.getValue("org.acme.config.file.locations"); + if (value == null || value.getValue() == null) { + return Collections.emptyList(); + } + + try { + return List.of(new PropertiesConfigSource(toURL(value.getValue()), 250)); + } catch (IOException e) { + return Collections.emptyList(); + } + } + + private URL toURL(final String value) { + try { + return new URI(value).toURL(); + } catch (URISyntaxException | MalformedURLException e) { + throw ConfigMessages.msg.uriSyntaxInvalid(e, value); + } + } +} +---- + +And registration in: + +.META-INF/services/io.smallrye.config.ConfigSourceFactory +[source,properties] +---- +org.acme.config.FileSystemConfigSourceFactory +---- + +The `FileSystemConfigSourceFactory` look ups the configuration value for `org.acme.config.file.locations`, and uses it +to set up an additional `ConfigSource`. + +== `ConfigurableConfigSourceFactory` + +A `ConfigurableConfigSourceFactory` accepts a `@ConfigMapping` interface to configure the `ConfigSource`, removing the +need to look up configuration values manually via `ConfigSourceContext`: + +[source,java] +---- +@ConfigMapping(prefix = "org.acme.config.file") +interface FileSystemConfig { + List locations(); +} +---- + +[source,java] +---- +public class FileSystemConfigurableConfigSourceFactory implements ConfigurableConfigSourceFactory { + @Override + public Iterable getConfigSources(ConfigSourceContext context, FileSystemConfig config) { + List sources = new ArrayList<>(); + for (URL url : config.locations()) { + try { + sources.add(new PropertiesConfigSource(url, 250)); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + return sources; + } +} +---- + +And registration in: + +.META-INF/services/io.smallrye.config.ConfigSourceFactory +[source,properties] +---- +org.acme.config.FileSystemConfigurableConfigSourceFactory +---- + +The `ConfigMapping` interface is automatically populated from all `ConfigSource`s already initialized at the time the +factory runs. Values are resolved using the same two-step initialization as a regular `ConfigSourceFactory`: sources +from `ConfigSource` / `ConfigSourceProvider` registrations are available, but sources from other +`ConfigSourceFactory` implementations are not. diff --git a/content/docs/smallrye-config/main/config-sources/filesystem.adoc b/content/docs/smallrye-config/main/config-sources/filesystem.adoc new file mode 100644 index 000000000..66e16db05 --- /dev/null +++ b/content/docs/smallrye-config/main/config-sources/filesystem.adoc @@ -0,0 +1,51 @@ += FileSystem Config Source +:version-root: ../ +include::{version-root}_attributes.adoc[] + +This Config Source loads configuration values for each file found in a directory. Each file corresponds to a single +property, where the file name is the configuration property name and the file content the configuration value. + +For instance, if a directory structure looks like: + +.... +foo/ +|__num.max +|__num.size +.... + +The `FileSystem` Config Source will provide 2 properties: + +* num.max +* num.size + +[WARNING] +==== + +Nested directories are not supported. + +==== + +This Config Source can be used to read configuration from +https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap[Kubernetes ConfigMap]. Check the +https://github.com/smallrye/smallrye-config/blob/main/examples/configmap/README.adoc[Kubernetes ConfigMap ConfigSource Example]. + +The same mapping rules as defined for environment variables are applied, so the `FileSystem` Config Source will search +for a given property name `num.max`: + +* Exact match (`num.max`) +* Replace each character that is neither alphanumeric nor ++_++ with ++_++ (`num++_++max`) +* Replace each character that is neither alphanumeric nor ++_++ with ++_++; then convert the name to upper case (`NUM++_++MAX`) + +The following dependency is required in the classpath to use the `FileSystem` Config Source: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.config + smallrye-config-source-file-system + {version} + +---- + +The configuration property `smallrye.config.source.file.locations` sets the directory paths to look up the files. It +accepts multiple locations separated by a comma and each must represent a valid URI to a directory. diff --git a/content/docs/smallrye-config/main/config-sources/hocon.adoc b/content/docs/smallrye-config/main/config-sources/hocon.adoc new file mode 100644 index 000000000..23b29b437 --- /dev/null +++ b/content/docs/smallrye-config/main/config-sources/hocon.adoc @@ -0,0 +1,46 @@ += HOCON Config Source +:version-root: ../ +include::{version-root}_attributes.adoc[] + +This Config Source allows to use the https://github.com/lightbend/config/blob/main/HOCON.md[HOCON] file format to +load configuration values. The HOCON Config Source loads the +configuration from the file `META-INF/microprofile-config.conf`. It has a lower ordinal (`50`) than the +`microprofile-config.properties`. + +The following dependency is required in the classpath to use the HOCON Config Source: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.config + smallrye-config-source-hocon + {version} + +---- + +Expressions defined as `$++{++value}` (unquoted) are resolved internally by the HOCON Config Source as described in the +https://github.com/lightbend/config/blob/main/HOCON.md#substitutions[HOCON Substitutions] documentation. Quoted +Expressions defined as `"$++{++value}"` are resolved by xref:config/expressions.adoc[SmallRye Config Property Expressions]. + +Consider: + +*hocon.conf* + +[source,conf] +---- +{ + foo: "bar", + hocon: ${foo}, + config: "${foo}" +} +---- + +*application.properties* + +[source,properties] +---- +config_ordinal=1000 +foo=baz +---- + +The value of `hocon` is `bar` and the value of `config` is `baz` (if the properties source has a higher ordinal). diff --git a/content/docs/smallrye-config/main/config-sources/json.adoc b/content/docs/smallrye-config/main/config-sources/json.adoc new file mode 100644 index 000000000..54fe1a808 --- /dev/null +++ b/content/docs/smallrye-config/main/config-sources/json.adoc @@ -0,0 +1,6 @@ += JSON Config Source +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The xref:config-sources/yaml.adoc[YAML Config Source] also accepts the JSON format as its contents. The configuration file +`META-INF/microprofile-config.yaml`, still requires to use the `yaml` extension. diff --git a/content/docs/smallrye-config/main/config-sources/keystore.adoc b/content/docs/smallrye-config/main/config-sources/keystore.adoc new file mode 100644 index 000000000..19c2dac49 --- /dev/null +++ b/content/docs/smallrye-config/main/config-sources/keystore.adoc @@ -0,0 +1,67 @@ += KeyStore Config Source +:version-root: ../ +include::{version-root}_attributes.adoc[] + +This Config Source allows to use a Java `KeyStore` to load configuration values. It uses an ordinal of `100`. + +The following dependency is required in the classpath to use the KeyStore Config Source: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.config + smallrye-config-source-keystore + {version} + +---- + +== Create a KeyStore + +The following command creates a simple KeyStore + +[source,bash] +---- +keytool -importpass -alias my.secret -keystore keystore -storepass secret -storetype PKCS12 -v +---- + +The `-alias my.secret` stores the configuration property name `my.secret` in the KeyStore. The command will +interactively ask for the value to be stored in the KeyStore. + +== Read the KeyStore + +The KeyStore Config Source supports reading multiple keystore files: + +[source,properties] +---- +smallrye.config.source.keystore.one.path=keystore-one +smallrye.config.source.keystore.one.password=password + +smallrye.config.source.keystore.two.path=keystore-two +smallrye.config.source.keystore.two.password=password +---- + +The names are arbitrary and can be any name. The name `one` and `two` are used to distinguish both KeyStores. + +If a stored configuration property requires a xref:config/secret-keys.adoc[Secret Handler] to decode a value, set +the handler name with `smallrye.config.source.keystore."name".handler`. + +== Configuration + +[width="100%",cols="95%,2%,3%",options="header",] +|=== +|Configuration Property |Type |Default +|`smallrye.config.source.keystore."name".path` + +The KeyStore path. |String | +|`smallrye.config.source.keystore."name".password` + +The KeyStore password. |String | +|`smallrye.config.source.keystore."name".type` + +The KeyStore type. |String |`PKCS12` +|`smallrye.config.source.keystore."name".handler` + +An Optional secret keys handler. |String | +|`smallrye.config.source.keystore."name".aliases."key".name` + +An Optional aliases key name. |String | +|`smallrye.config.source.keystore."name".aliases."key".password` + +An Optional aliases key password. |String | +|`smallrye.config.source.keystore."name".aliases."key".handler` + +An Optional aliases key secret keys handler. |String | +|=== diff --git a/content/docs/smallrye-config/main/config-sources/locations.adoc b/content/docs/smallrye-config/main/config-sources/locations.adoc new file mode 100644 index 000000000..84bdc6c76 --- /dev/null +++ b/content/docs/smallrye-config/main/config-sources/locations.adoc @@ -0,0 +1,82 @@ += Locations +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Additionally, to the default config locations specified by the +https://github.com/eclipse/microprofile-config/[MicroProfile Config] specification, SmallRye Config provides a way to +scan additional locations for configuration properties files. + +The `smallrye.config.locations` configuration property accepts multiple locations separated by a comma `,` and each +ust represent a valid `URI`. The supported `URI` schemes are: + +* file or directory (`file:`) +* classpath resource +* jar resource (`jar:`) +* http resource (`http:`) + +Each `URI` scheme loads all discovered resources in a `ConfigSource`. All loaded sources use the same ordinal of the +source that found the `smallrye.config.locations` configuration property. For instance, if `smallrye.config.locations` +is set as a system property, then all loaded sources have their ordinals set to `400` (system properties use `400` as +their ordinal). The ordinal may be overridden directly in the resource by setting the `config++_++ordinal` property. +Sources are sorted first by their ordinal, then by location order, and finally by loading order. + +If a profile is active, and the `URI` represents a single resource (for instance a file), then resources that match +the active profile are also loaded. The profile resource name must follow the pattern: `++{++location}-++{++profile}`. A +profile resource is only loaded if the unprofiled resource is also available in the same location. This is to keep a +consistent loading order and pair all the resources together. + +Profile resources are not taken into account if the location is a directory since there is no reliable way to discover +which file is the main resource. Properties that use the profile prefix syntax `%profile.` will work as expected. + +_All properties files from a directory_ + +[source,properties] +---- +# loads all files from a relative path +smallrye.config.locations=./src/main/resources/ + +# loads all files from an absolute path +smallrye.config.locations=/user/local/config +---- + +For relative paths, the JVM `user.dir` property defines the current directory. + +_A specific file_ + +../src/main/resources/additional.properties + +[source,properties] +---- +smallrye.config.locations=./src/main/resources/additional.properties +---- + +If a profile `dev` is active, and an `additional-dev.properties` file exists, this will also be loaded. + +_All `additional.properties` files from the classpath_ + +.additional.properties +[source,properties] +---- +smallrye.config.locations=additional.properties +---- + +If a profile `prod` is active, and an `additional-prod.properties` resources exists next to the `additional.properties` +resource, this will also be loaded. + +_The `resources.properties` file from a specific jar_ + +.jar:file:///user/local/app/lib/resources-.jar!/resources.properties +[source,properties] +---- +smallrye.config.locations=jar:file:///user/local/app/lib/resources-.jar!/resources.properties +---- + +If a profile `test` is active, and an `additional-test.properties` resource exists, this will also be loaded. + +_The `config.properties` file from a web server_ + +.http://localhost:8080/config/config.properties +[source,properties] +---- +smallrye.config.locations=http://localhost:8080/config/config.properties +---- diff --git a/content/docs/smallrye-config/main/config-sources/toml.adoc b/content/docs/smallrye-config/main/config-sources/toml.adoc new file mode 100644 index 000000000..84319a738 --- /dev/null +++ b/content/docs/smallrye-config/main/config-sources/toml.adoc @@ -0,0 +1,22 @@ += TOML Config Source +:version-root: ../ +include::{version-root}_attributes.adoc[] + +This Config Source allows to use a https://toml.io[TOML] file to load configuration values. The TOML Config Source +loads the configuration from the following files: + +[arabic] +. (`265`) `application.toml` in `config` folder, located in the current working directory +. (`255`) `application.toml` in the classpath +. (`110`) MicroProfile Config configuration file `META-INF/microprofile-config.toml` in the classpath + +The following dependency is required in the classpath to use the TOML Config Source: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.config + smallrye-config-source-toml + {version} + +---- diff --git a/content/docs/smallrye-config/main/config-sources/yaml.adoc b/content/docs/smallrye-config/main/config-sources/yaml.adoc new file mode 100644 index 000000000..0c8bb5e3f --- /dev/null +++ b/content/docs/smallrye-config/main/config-sources/yaml.adoc @@ -0,0 +1,21 @@ += YAML Config Source +:version-root: ../ +include::{version-root}_attributes.adoc[] + +This Config Source allows to use a `yaml` file to load configuration values. The YAML Config Source loads the configuration from the following files: + +[arabic] +. (`265`) `application.yaml++|++yml` in `config` folder, located in the current working directory +. (`255`) `application.yaml++|++yml` in the classpath +. (`110`) MicroProfile Config configuration file `META-INF/microprofile-config.yaml++|++yml` in the classpath + +The following dependency is required in the classpath to use the YAML Config Source: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.config + smallrye-config-source-yaml + {version} + +---- diff --git a/content/docs/smallrye-config/main/config-sources/zookeeper.adoc b/content/docs/smallrye-config/main/config-sources/zookeeper.adoc new file mode 100644 index 000000000..37f27785d --- /dev/null +++ b/content/docs/smallrye-config/main/config-sources/zookeeper.adoc @@ -0,0 +1,30 @@ += ZooKeeper Config Source +:version-root: ../ +include::{version-root}_attributes.adoc[] + +This Config Source allows using https://zookeeper.apache.org/index.html[Apache ZooKeeper] to load configuration +values. + +The following dependency is required in the classpath to use the ZooKeeper Config Source: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.config + smallrye-config-source-zookeeper + {version} + +---- + +It also requires to set up additional configuration properties to identify the ZooKeeper instance: + +[source,properties] +---- +io.smallrye.configsource.zookeeper.url=localhost:2181 +io.smallrye.configsource.zookeeper.applicationId=applicationId +---- + +The ZooKeeper Config Source will look for configuration values in a ZooKeeper instance running in the url set in +`io.smallrye.configsource.zookeeper.url` and in the znodes available in `/applicationId/`. + +This Config Source has an ordinal of `150`. diff --git a/content/docs/smallrye-config/main/config/config-value.adoc b/content/docs/smallrye-config/main/config/config-value.adoc new file mode 100644 index 000000000..63d58d715 --- /dev/null +++ b/content/docs/smallrye-config/main/config/config-value.adoc @@ -0,0 +1,8 @@ += Config Value +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The `io.smallrye.config.ConfigValue` is a metadata object that holds additional information after the lookup of a +configuration property. It is able to hold information of configuration property name, value, profile, the +`ConfigSource` from where the configuration was loaded, the ordinal of the `ConfigSource` and a line number from where +the configuration was loaded if exists. diff --git a/content/docs/smallrye-config/main/config/configuration.adoc b/content/docs/smallrye-config/main/config/configuration.adoc new file mode 100644 index 000000000..cb7a1b582 --- /dev/null +++ b/content/docs/smallrye-config/main/config/configuration.adoc @@ -0,0 +1,20 @@ += Configuration Reference +:version-root: ../ +include::{version-root}_attributes.adoc[] + +[width="100%",cols="92%,4%,4%",options="header",] +|=== +|Configuration Property |Type |Default +|`smallrye.config.profile` + +The main xref:config/profiles.adoc[Profile] to activate. |String++[]++ | +|`smallrye.config.profile.parent` + +The parent xref:config/profiles.adoc#parent-profile[Profile] to activate. |String | +|`smallrye.config.locations` + +xref:config-sources/locations.adoc[Additional config locations] to be loaded with the Config. The configuration supports multiple locations separated by a comma and each must represent a valid `java.net.URI`. |URI++[]++ | +|`smallrye.config.mapping.validate-unknown` + +xref:config/mappings.adoc#retrieval[Validates] that a `@ConfigMapping` maps every available configuration name contained in the mapping prefix. |boolean |true +|`smallrye.config.secret-handlers` + +The names of the secret handlers to be loaded. A value of `all` loads all available secret handlers and a value of `none` skips the load. |String++[]++ |all +|`smallrye.config.log.values` + +Enable logging of configuration values lookup in DEBUG log level. |boolean |false +|=== diff --git a/content/docs/smallrye-config/main/config/customizer.adoc b/content/docs/smallrye-config/main/config/customizer.adoc new file mode 100644 index 000000000..e841a645b --- /dev/null +++ b/content/docs/smallrye-config/main/config/customizer.adoc @@ -0,0 +1,49 @@ += Customizer +:version-root: ../ +include::{version-root}_attributes.adoc[] + +A `SmallRyeConfigBuilderCustomizer` allows to customize a `SmallRyeConfigBuilder`, used to create the `SmallRyeConfig` +instance. + +Registration of a `SmallRyeConfigBuilderCustomizer` is done via the `ServiceLoader` mechanism by providing the +implementation classes in a `META-INF/services/io.smallrye.config.SmallRyeConfigBuilderCustomizer` file. Alternatively, +customizers may be registered via the Programmatic API in `SmallRyeConfigBuilder#withCustomizers`. + +The `SmallRyeConfigBuilderCustomizer` may also +assign a priority by overriding the default method `int priority()`. Customizers are sorted by ascending priority and +executed in that order, meaning that higher numeric priorities will execute last, possible overriding values set by +previous customizers. + +.CustomConfigBuilder +[source,java] +---- +package org.acme.config; + +import io.smallrye.config.SmallRyeConfigBuilder; +import io.smallrye.config.SmallRyeConfigBuilderCustomizer; + +public class CustomConfigBuilder implements SmallRyeConfigBuilderCustomizer { + @Override + public void configBuilder(final SmallRyeConfigBuilder builder) { + builder.withDefaultValue("my.default", "1234"); + } +} +---- + +And registration in: + +.META-INF/services/io.smallrye.config.SmallRyeConfigBuilderCustomizer +[source,properties] +---- +org.acme.config.CustomConfigBuilder +---- + +The `CustomConfigBuilder` will be executed when creating a new `SmallRyeConfig` from a `SmallRyeConfigBuilder`: + +[source,java] +---- +SmallRyeConfig config = new SmallRyeConfigBuilder().build(); +config.getValue("my.default", int.class); +---- + +A look up to `my.default` returns the value `1234`, registered by the `CustomConfigBuilder`. diff --git a/content/docs/smallrye-config/main/config/environment-variables.adoc b/content/docs/smallrye-config/main/config/environment-variables.adoc new file mode 100644 index 000000000..15579d8e6 --- /dev/null +++ b/content/docs/smallrye-config/main/config/environment-variables.adoc @@ -0,0 +1,80 @@ += Environment Variables +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Environment Variable names follow the conversion rules specified by https://github.com/eclipse/microprofile-config/blob/master/spec/src/main/asciidoc/configsources.asciidoc#default-configsources[MicroProfile Config]. + +SmallRye Config specifies additional conversion rules: + +* A property with double quotes `foo."bar".baz` or `foo.bar."baz"`, replace each character that is neither alphanumeric +nor `++_++` with `++_++`: `FOO++__++BAR++__++BAZ` or `FOO++_++BAR++__++BAZ++_++` ; +* A property with dashes `foo.bar-baz`, replace each character that is neither alphanumeric nor `++_++` +with `++_++`: `FOO++_++BAR++_++BAZ` +* An indexed property `foo.bar++[++0++]++` or `foo.bar++[++0++]++.baz`, replace each character that is neither alphanumeric nor `++_++` +with `++_++`: `FOO++_++BAR++_++0++_++` or `FOO++_++BAR++_++0++__++BAZ`. + +[CAUTION] +==== + +Environment Variables format cannot represent the entire spectrum of common property names. + +==== + +The lookup of configuration values from Environment Variables will always use the dotted format name. For +instance, the lookup of the Environment Variable `FOO++_++BAR` value, requires the property name `foo.bar`: + +[source,java] +---- +Config config = Config.getOrCreate(); +String value = config.getValue("foo.bar", String.class); +---- + +When SmallRyeConfig performs the lookup on the Environment Variables Config Source, it applies the conversion rules to +find the matching property name and retrieve the value. + +In some situations, looking up the exact property name is impossible. For instance, when SmallRye Config has to look up +a configuration that is part of a `Map`, and the property name contains a dynamic segment (a `Map` key). In this case, +SmallRye Config relies upon each source's list of property names. These must be converted back to their most likely +dotted format for Environment Variables. + +By default, the underscore `++_++` of an Environment Variable name always maps to a dot `.`. If the property name +contains a dash or some other special character, that property name can be specified in another Config +Source, with the expected dotted format. It will provide additional information to SmallRye Config to perform a +two-way conversion and match the property names. + +Consider: + +..env + +[source,console] +---- +FOO_BAR_BAZ=VALUE +---- + +Will map to `foo.bar.baz` and value `value`. + +If `foo.bar-baz` is available in any source: + +..env + +[source,console] +---- +FOO_BAR_BAZ=VALUE +---- + +.application.properties +[source,properties] +---- +foo.bar-baz=default +---- + +Will map to `foo.bar-baz` and value `value`. + +[NOTE] +==== + +The property name in dotted format needs to exist somewhere to provide this additional information. It can be set in a +low ordinal source, even without value. The Environment Variables source will override the value and map the correct +configuration name. + +==== diff --git a/content/docs/smallrye-config/main/config/expressions.adoc b/content/docs/smallrye-config/main/config/expressions.adoc new file mode 100644 index 000000000..f76d0df5c --- /dev/null +++ b/content/docs/smallrye-config/main/config/expressions.adoc @@ -0,0 +1,75 @@ += Property Expressions +:version-root: ../ +include::{version-root}_attributes.adoc[] + +SmallRye Config provides property expressions expansion on configuration values. An expression string is a mix of plain +strings and expression segments, which are wrapped by the sequence $++{++ ... }. + +For instance, the following configuration properties file: + +[source,properties] +---- +remote.host=smallrye.io +callable.url=https://${remote.host}/ +---- + +The resolved value of the `callable.url` property is `https://smallrye.io/`. + +Additionally, the Expression Expansion engine supports the following segments: + +* `$++{++expression:value}` - Provides a default value after the `:` if the expansion doesn’t find a value. +* `$++{++my.prop$++{++compose}}` - Composed expressions. Inner expressions are resolved first. +* `$++{++my.prop}$++{++my.prop}` - Multiple expressions. + +If an expression cannot be expanded and no default is supplied a `NoSuchElementException` is thrown. + +== Escaping + +A `$` that begins an expression segment can be escaped with a backslash to treat it as a literal character: + +[source,properties] +---- +# value is: ${my.prop} +expression=\${my.prop} +---- + +A backslash that immediately precedes a `$` can itself be escaped with another backslash: + +[source,properties] +---- +# my.prop=value +# value is: \value +expression=\\${my.prop} +---- + +Escaping only applies to values that contain an expression segment. In a value that has no `$++{++ ... }` segment, +every backslash is treated as a literal character and nothing needs escaping (for example, a Windows path such as +`C:++\++some++\++path` is used as-is). + +As soon as a value contains an expression segment, the whole value is processed for escapes and backslash becomes an +escape character everywhere in it, not just before a `$`. Standard escape sequences are interpreted (`++\++n`, `++\++r`, `++\++t`, +`++\++b`, `++\++f`) and a backslash before any other character is dropped. To keep a literal backslash in such a value, double +it: + +[source,properties] +---- +# value is: C:\some\path\value (my.prop=value) +expression=C:\\some\\path\\${my.prop} +---- + +== Disabling Expansion + +Expression expansion may be selectively disabled for a block of code using `io.smallrye.config.Expressions.withoutExpansion()`. +The raw (unexpanded) value is returned for any property looked up within the block: + +[source,properties] +---- +callable.url=https://${remote.host}/ +---- + +[source,java] +---- +Config config = Config.getOrCreate(); +// returns "https://${remote.host}/" +String url = Expressions.withoutExpansion(() -> config.getValue("callable.url", String.class)); +---- diff --git a/content/docs/smallrye-config/main/config/getting-started.adoc b/content/docs/smallrye-config/main/config/getting-started.adoc new file mode 100644 index 000000000..d2eb46c6d --- /dev/null +++ b/content/docs/smallrye-config/main/config/getting-started.adoc @@ -0,0 +1,182 @@ += Getting Started +:version-root: ../ +include::{version-root}_attributes.adoc[] + +== Config Sources + +By default, SmallRye Config reads configuration properties from multiple configuration sources (by descending ordinal): + +[arabic] +. (`400`) System properties +. (`300`) Environment variables +. (`295`) `.env` file in the current working directory +. (`260`) `application.properties` in `config` folder, located in the current working directory +. (`250`) `application.properties` in the classpath +. (`100`) MicroProfile Config configuration file `META-INF/microprofile-config.properties` in the classpath + +A configuration source is handled by a `ConfigSource`. A `ConfigSource` provides configuration values from a specific +place. + +The final configuration is the aggregation of the properties defined by all these sources. A configuration property +lookup starts by the highest ordinal configuration source available and works it way down to other sources until a +match is found. This means that any configuration property may override a value just by setting a different value in a +higher ordinal config source. For example, a property configured using an Environment Variable overrides the value +provided using the `microprofile-config.properties` file. + +=== System Properties + +System properties can be handed to the application through the `-D` flag during startup. For instance, +`java -Dmy.prop -jar my.jar`. + +=== Environment Variables + +Environment variables are set directly in the host operating system. Environment variables names follow the conversion +rules detailed by xref:config/environment-variables.adoc[Environment Variables]. + +=== MicroProfile Config configuration file + +The MicroProfile Config configuration file `META-INF/microprofile-config.properties` in the classpath. It follows the +standard convention for `properties` files. + +.META-INF/microprofile-config.properties +[source,properties] +---- +greeting.message=hello +goodbye.message=bye +---- + +=== Additional Config Sources + +SmallRye Config provides additional extensions which cover other configuration formats and stores: + +* xref:config-sources/yaml.adoc[YAML] +* xref:config-sources/filesystem.adoc[File System] +* xref:config-sources/zookeeper.adoc[ZooKeeper] +* xref:config-sources/hocon.adoc[HOCON] + +It is also possible to create a xref:config-sources/custom.adoc[Custom ConfigSource]. + +== Retrieving the Configuration + +=== Programmatically + +The `io.smallrye.config.Config#getOrCreate()` API allows to access the `io.smallrye.config.Config` API +programmatically: + +[source,java] +---- +import io.smallrye.config.Config; + +Config config = Config.getOrCreate(); +String message = config.getValue("greeting.message", String.class); +---- + +The `Config` instance will be created and registered to the current context class loader if no such configuration is +already created and registered. This means that subsequent calls to `Config.getOrCreate()` or +`Config.get()` will return the same `Config` instance if the context class loader is the same. + +To obtain a detached instanced, use the `io.smallrye.config.SmallRyeConfigBuilder`: + +[source,java] +---- +SmallRyeConfig config = new SmallRyeConfigBuilder() + .addDefaultInterceptors() + .addDefaultSources() + .build(); + +String message = config.getValue("greeting.message", String.class); +---- + +=== With CDI + +In a CDI environment, configuration can be injected in CDI aware beans with `@Inject` and +the `org.eclipse.microprofile.config.inject.ConfigProperty` qualifier. + +[source,java] +---- +@Inject +@ConfigProperty(name = "greeting.message") +String message; + +@Inject +@ConfigProperty(name = "greeting.suffix", defaultValue="!") +String suffix; + +@Inject +@ConfigProperty(name = "greeting.name") +Optional name; + +@Inject +SmallRyeConfig config; +---- + +* If a value if not provided for this `greeting.message`, the application startup fails with a +`jakarta.enterprise.inject.spi.DeploymentException: No config value of type ++[++class java.lang.String++]++ exists for: greeting.message`. +* The default value `!` is injected if the configuration does not provide a value for `greeting.suffix`. +* The property `greeting.name` is optional - an empty Optional is injected if the configuration does not provide a +value for it. + +== Override Config + +It is possible to override `Config` default initialization `Config.getOrCreate()`, by extending +`io.smallrye.config.SmallRyeConfigFactory` and registering the implementation with the `ServiceLoader` mechanism. + +== Config vs SmallRyeConfig + +The `io.smallrye.config.Config` is an extension of `org.eclipse.microprofile.config.Config` and provides +additional APIs and helper methods not available in `org.eclipse.microprofile.config.Config`. To obtain an instance of +`io.smallrye.config.Config`, the original `org.eclipse.microprofile.config.Config` can be unwrapped: + +[source,java] +---- +import io.smallrye.config.SmallRyeConfig; +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.ConfigProvider; + +Config config = ConfigProvider.getConfig(); +SmallRyeConfig smallRyeConfig = config.unwrap(SmallRyeConfig.class); +---- + +Or obtained directly: + +[source,java] +---- +Config config = Config.getOrCreate(); +---- + +A few notable APIs provided by `io.smallrye.config.Config` allow to: + +* Retrieve multiple values into a specified `Collection` +* Retrieve xref:config/indexed-properties.adoc[Indexed Values] +* Retrieve xref:config/mappings.adoc[Config Mappings] instances +* Retrieve the raw value of a configuration +* Check if a property is present +* Retrieve a `Converter` +* Convert values + +== Converters + +The `ConfigSource` retrieves a configuration value as a `String`. Other data types require a conversion using the +`org.eclipse.microprofile.config.spi.Converter` API. + +Most of the common `Converter` types are provided by default: + +* `boolean` and `java.lang.Boolean`; the values "true", "1", "YES", "Y" "ON" represent `true`. Any other value will be +interpreted as `false` +* `byte` and `java.lang.Byte` +* `short` and `java.lang.Short` +* `int`, `java.lang.Integer`, and `java.util.OptionalInt` +* `long`, `java.lang.Long`, and `java.util.OptionalLong` +* `float` and `java.lang.Float`; a dot '.' is used to separate the fractional digits +* `double`, `java.lang.Double`, and `java.util.OptionalDouble`; a dot '.' is used to separate the fractional digits +* `char` and `java.lang.Character` +* `java.lang.Class` based on the result of `Class.forName` +* `java.net.InetAddress` +* `java.util.UUID` +* `java.util.Currency` +* `java.util.regex.Pattern` +* `java.nio.file.Path` +* Any class with declared static methods `of`, `valueOf` or `parse` that take a `String` or a `CharSequence` +* Any class with declared constructors that takes a `String` or a `CharSequence` + +All default converters have a priority of `1`. diff --git a/content/docs/smallrye-config/main/config/indexed-properties.adoc b/content/docs/smallrye-config/main/config/indexed-properties.adoc new file mode 100644 index 000000000..78b32cc10 --- /dev/null +++ b/content/docs/smallrye-config/main/config/indexed-properties.adoc @@ -0,0 +1,32 @@ += Indexed Properties +:version-root: ../ +include::{version-root}_attributes.adoc[] + +In https://github.com/eclipse/microprofile-config/[MicroProfile Config], a config value with unescaped commas may be +converted to `Collection`. It works for simple cases, but it becomes cumbersome and limited for more advanced use cases. + +Indexed Properties provide a way to use indexes in config property names to map specific elements in a `Collection` +type. Since the indexed element is part of the property name, it can also map complex object types. Consider: + +[source,properties] +---- +# MicroProfile Config - Collection Values +my.collection=dog,cat,turtle + +# SmallRye Config - Indexed Property +my.indexed.collection[0]=dog +my.indexed.collection[1]=cat +my.indexed.collection[2]=turtle +---- + +The indexed property syntax uses the property name and square brackets with an index in between. + +A call to `Config#getValues("my.collection", String.class)`, will automatically create and convert a `List++<++String++>++` +that contains the values `dog`, `cat` and `turtle`. A call to `Config#getValues("my.indexed.collection", String.class)` +returns the exact same result. The indexed property format is prioritized when both styles are found in the same +configuration source. When available in multiple sources, the higher ordinal source wins, like any other configuration +lookup. + +The indexed property is sorted by its index before being added to the target `Collection`. Any gaps in the indexes do +not resolve to the target `Collection`, which means that the `Collection` result will store all values without empty +elements. diff --git a/content/docs/smallrye-config/main/config/instance-builder.adoc b/content/docs/smallrye-config/main/config/instance-builder.adoc new file mode 100644 index 000000000..1bd05231f --- /dev/null +++ b/content/docs/smallrye-config/main/config/instance-builder.adoc @@ -0,0 +1,234 @@ += Instance Builder +:version-root: ../ +include::{version-root}_attributes.adoc[] + +With the `io.smallrye.config.ConfigInstanceBuilder` API, it is possible to create instances of a configuration interface +programmatically, without requiring a `SmallRyeConfig` or any configuration source. This is useful for testing, default +configurations, or any scenario where configuration values are known at build time. + +== Usage + +A configuration interface instance is created with `ConfigInstanceBuilder.forInterface()`: + +[source,java] +---- +interface Server { + String host(); + + int port(); +} + +Server server = ConfigInstanceBuilder.forInterface(Server.class) + .with(Server::host, "localhost") + .with(Server::port, 8080) + .build(); +---- + +The builder uses method references to identify which property to set, providing compile-time type safety without +string-based property names. The `with` method accepts a method reference to the configuration interface member and the +value to set. + +[NOTE] +==== + +The configuration interface does not require the `@ConfigMapping` annotation to work with the builder. Any valid +configuration interface is accepted. + +==== + +== Primitive Types + +The builder provides dedicated `with()` overloads for `int`, `long`, `double`, and `boolean` primitive types. Other +primitive types (`byte`, `short`, `float`, `char`) are set via the generic `with()` method using their boxed types: + +[source,java] +---- +@ConfigMapping +interface Primitives { + int intValue(); + + boolean booleanValue(); + + byte byteValue(); +} + +Primitives primitives = ConfigInstanceBuilder.forInterface(Primitives.class) + .with(Primitives::intValue, 42) + .with(Primitives::booleanValue, true) + .with(Primitives::byteValue, Byte.valueOf((byte) 1)) + .build(); +---- + +== Optional Properties + +The `withOptional()` method sets optional properties. If an optional property is not set, it defaults to empty: + +[source,java] +---- +interface AppConfig { + Optional name(); + + OptionalInt timeout(); +} + +AppConfig config = ConfigInstanceBuilder.forInterface(AppConfig.class) + .withOptional(AppConfig::name, "MyApp") + .withOptional(AppConfig::timeout, 30) + .build(); +---- + +The `withOptional` method wraps the value in `Optional.of()`, `OptionalInt.of()`, `OptionalLong.of()`, or +`OptionalDouble.of()` depending on the property type. + +== Defaults + +Properties annotated with `io.smallrye.config.WithDefault` are automatically applied when no value is explicitly set +in the builder: + +[source,java] +---- +interface ServerDefaults { + @WithDefault("localhost") + String host(); + + @WithDefault("8080") + int port(); +} + +ServerDefaults server = ConfigInstanceBuilder.forInterface(ServerDefaults.class).build(); +---- + +The `ServerDefaults#host()` will return the value `localhost` and `ServerDefaults#port()` will return the value `8080`. + +Explicitly set values override the `@WithDefault` annotation: + +[source,java] +---- +ServerDefaults server = ConfigInstanceBuilder.forInterface(ServerDefaults.class) + .with(ServerDefaults::host, "0.0.0.0") + .build(); +---- + +The `ServerDefaults#host()` will return the value `0.0.0.0` and `ServerDefaults#port()` will return the value `8080`. + +== Nested Groups + +Nested configuration groups are built separately and composed into the parent builder: + +[source,java] +---- +interface AppConfig { + String name(); + + DatabaseConfig database(); +} + +interface DatabaseConfig { + String url(); + + int poolSize(); +} + +AppConfig config = ConfigInstanceBuilder.forInterface(AppConfig.class) + .with(AppConfig::name, "MyApp") + .with(AppConfig::database, ConfigInstanceBuilder.forInterface(DatabaseConfig.class) + .with(DatabaseConfig::url, "jdbc:h2:mem:test") + .with(DatabaseConfig::poolSize, 10) + .build()) + .build(); +---- + +If a nested group has `@WithDefault` values for all its members, the nested group instance is automatically built with +those defaults when not explicitly set in the builder. + +== Collections and Maps + +`List`, `Set`, and `Map` types are set directly with their values: + +[source,java] +---- +interface AppConfig { + List hosts(); + + Map labels(); +} + +AppConfig config = ConfigInstanceBuilder.forInterface(AppConfig.class) + .with(AppConfig::hosts, List.of("host1", "host2")) + .with(AppConfig::labels, Map.of("env", "prod", "region", "us-east")) + .build(); +---- + +Collections and Maps annotated with `@WithDefault` are automatically populated when not explicitly set. + +== Converters + +The builder uses the same converter mechanism as `SmallRyeConfig`. Converters are discovered via the +`java.util.ServiceLoader` mechanism, or can be registered explicitly with `ConfigInstanceBuilder.registerConverter()`: + +[source,java] +---- +ConfigInstanceBuilder.registerConverter(Duration.class, new MyDurationConverter()); +---- + +Properties annotated with `io.smallrye.config.WithConverter` use the specified converter for both `@WithDefault` value +conversion and any value set via the builder: + +[source,java] +---- +interface AppConfig { + @WithDefault("30s") + @WithConverter(DurationConverter.class) + Duration timeout(); + + @WithConverter(MemorySizeConverter.class) + MemorySize maxBodySize(); +} +---- + +[NOTE] +==== + +Converters registered with `registerConverter` are global and shared across all `ConfigInstanceBuilder` instances. + +==== + +== Required Properties + +Properties without a `@WithDefault` are considered required. Calling `build()` throws a `NoSuchElementException` if +any required property is missing: + +[source,java] +---- +interface Server { + String host(); + + @WithDefault("8080") + int port(); +} + +// Throws NoSuchElementException because host is required +ConfigInstanceBuilder.forInterface(Server.class).build(); + +// Works because host is provided +ConfigInstanceBuilder.forInterface(Server.class) + .with(Server::host, "localhost") + .build(); +---- + +== Builder Reuse + +A builder can be used to produce multiple instances. Each `build()` call creates a new independent instance: + +[source,java] +---- +ConfigInstanceBuilder builder = ConfigInstanceBuilder.forInterface(Server.class) + .with(Server::host, "localhost") + .with(Server::port, 8080); + +Server first = builder.build(); +Server second = builder.build(); +---- + +The `first` and `second` instances are equal but not the same object. Values set after a `build()` call affect only +subsequent builds. diff --git a/content/docs/smallrye-config/main/config/map-support.adoc b/content/docs/smallrye-config/main/config/map-support.adoc new file mode 100644 index 000000000..72b42740c --- /dev/null +++ b/content/docs/smallrye-config/main/config/map-support.adoc @@ -0,0 +1,60 @@ += Map Support +:version-root: ../ +include::{version-root}_attributes.adoc[] + +SmallRye Config allows injecting multiple configuration parameters as a `Map`. The configuration value syntax is +represented by `property.name.map-key=value` Consider: + +[source,properties] +---- +server.reasons.200=OK +server.reasons.201=Created +---- + +The previous configuration could be injected directly in a CDI Bean: + +With `@ConfigProperty` + +[source,java] +---- +@ApplicationScoped +public class ConfigBean { + @Inject + @ConfigProperty(name = "server.reasons") + Map reasons; +} +---- + +With `@ConfigProperties` + +[source,java] +---- +@ConfigProperties(prefix = "server") +public class Config { + Map reasons; +} +---- + +The `Map` will contains the keys `200` and `201`, which map to the values `OK` and `Created`. + +[NOTE] +==== + +Only the direct sub properties will be converted into a `Map` and +injected into the target bean, the rest will be ignored. In other words, +in the previous example, a property whose name is `reasons.200.a` would +be ignored as not considered as a direct sub property. + +==== + +[NOTE] +==== + +The property will be considered as missing if no direct sub properties +could be found. + +==== + +It is also possible to retrieve the `Map` programmatically by calling the methods +`SmallRyeConfig#getValues("server.reasons", Integer.class, String.class)` or +`SmallRyeConfig#getOptionalValues("server.reasons", Integer.class, String.class)`. diff --git a/content/docs/smallrye-config/main/config/mappings.adoc b/content/docs/smallrye-config/main/config/mappings.adoc new file mode 100644 index 000000000..b7454239f --- /dev/null +++ b/content/docs/smallrye-config/main/config/mappings.adoc @@ -0,0 +1,860 @@ += Mappings +:version-root: ../ +include::{version-root}_attributes.adoc[] + +With SmallRye Config Mappings, it is possible to group multiple configuration properties in a single interface that +share the same prefix (or namespace). It supports the following set of features: + +* Automatic conversion of the configuration type, including `List`, `Set`, `Map`, `Optional` and primitive types. +* Nested Config Mapping groups. +* Configuration Properties Naming Strategies +* Integration with Bean Validation + +== Mapping Rules + +A complex object type uses the following rules to map configuration values to their member values: + +* A configuration path is built by taking the object type prefix (or namespace) and the mapping member name +* The member name is converted to its kebab-case format +* If the member name is represented as a getter, the member name is taken from its property name equivalent, and then +converted to its kebab-case format +* The configuration value is automatically converted to the member type +* The configuration path is required to exist with a valid configuration value or the mapping will fail + +[NOTE] +==== + +Kebab-case - the method name is derived by replacing case changes with a dash to map the configuration property. + +==== + +A Config Mapping requires an interface with minimal metadata configuration annotated with +`io.smallrye.config.ConfigMapping`: + +[source,java] +---- +@ConfigMapping(prefix = "server") +interface Server { + String host(); + + int port(); +} +---- + +The `Server` interface is able to map configurations with the name `server.host` into the `Server#host()` method and +`server.port` into `Server#port()` method. The configuration property name to lookup is built from the prefix, and the +method name with `.` (dot) as the separator. + +[WARNING] +==== + +If a mapping fails to match a configuration property the config system throws a `NoSuchElementException`, unless +the mapped element is an `Optional`. + +==== + +The `@ConfigMapping` interface must obey the following rules: + +* A mapping method cannot accept parameters +* A mapping method return type cannot be void +* A mapping cannot use self-reference types +* `default` methods are allowed + +== Registration + +Registration of Config Mappings is automatic in CDI aware environments with the `@ConfigMapping` annotation. + +In non-CDI environments, the Config Mapping can be registered via `SmallRyeConfigBuilder#withMapping`. In this case, +the `@ConfigMapping` is completely optional (but recommendeded to set the prefix). + +[source,java] +---- +SmallRyeConfig config = new SmallRyeConfigBuilder() + .withMapping(Server.class) + .build(); +---- + +== Retrieval + +A config mapping interface can be injected into any CDI aware bean: + +[source,java] +---- +@ApplicationScoped +class BusinessBean { + @Inject + Server server; + + public void businessMethod() { + String host = server.host(); + } +} +---- + +In non-CDI environments, use the API `io.smallrye.config.SmallRyeConfig#getConfigMapping` to retrieve the config +mapping instance: + +[source,java] +---- +Config config = Config.getOrCreate(); +Server server = config.getConfigMapping(Server.class); +---- + +[NOTE] +==== + +Config Mapping instances are cached. They are populated when the `SmallRyeConfig` instance is initialized and +their values are not updated on `ConfigSource` changes. + +==== + +For a Config Mapping to be valid, it needs to match every configuration property name contained in the `Config` under +the specified prefix set in `@ConfigMapping`. This prevents unknown configuration properties in the `Config`. + +=== Disabling validation + +Validation can be disabled entirely in two ways: + +Via configuration property: + +[source,properties] +---- +smallrye.config.mapping.validate-unknown=false +---- + +Or programmatically via the builder: + +[source,java] +---- +SmallRyeConfig config = new SmallRyeConfigBuilder() + .withMapping(Server.class) + .withValidateUnknown(false) + .build(); +---- + +=== Ignoring specific paths + +Rather than disabling validation entirely, specific paths can be ignored with +`SmallRyeConfigBuilder#withMappingIgnore`. The path argument supports three patterns: + +[cols=",",options="header",] +|=== +|Pattern |Meaning +|`foo.bar` |Ignores the exact configuration name `foo.bar` +|`foo.++*++` |Ignores all direct child names under `foo` +|`foo.++**++` |Ignores all names under `foo` at any level (recursive) +|=== + +[source,java] +---- +SmallRyeConfig config = new SmallRyeConfigBuilder() + .withMapping(Server.class) + .withMappingIgnore("server.foo") // ignore the exact name + .withMappingIgnore("server.extras.*") // ignore direct children of server.extras + .withMappingIgnore("server.legacy.**") // ignore everything under server.legacy + .build(); +---- + +Multiple calls to `withMappingIgnore` accumulate — each call adds to the set of ignored paths. + +== Defaults + +The `io.smallrye.config.WithDefault` annotation allows to set a default property value into a mapping (and prevent +errors if the configuration value is not available in any `ConfigSource`). + +[source,java] +---- +public interface Defaults { + @WithDefault("foo") + String foo(); + + @WithDefault("bar") + String bar(); +} +---- + +No configuration properties are required. The `Defaults#foo()` will return the value `foo` and `Defaults#bar()` will +return the value `bar`. + +== Nested Groups + +A nested mapping provides a way to map sub-groups of configuration properties. + +* A nested type contributes with its name (converted to its kebab-case format) +* The configuration path is built by taking the root object type prefix (or namespace), the nested type name and the +member name of the nested type + +[source,java] +---- +@ConfigMapping(prefix = "server") +public interface Server { + String host(); + + int port(); + + Log log(); + + interface Log { + boolean enabled(); + + String suffix(); + + boolean rotate(); + } +} +---- + +[source,properties] +---- +server.host=localhost +server.port=8080 +server.log.enabled=true +server.log.suffix=.log +server.log.rotate=false +---- + +The method name of a mapping group acts as a sub-prefix in the property name. In this case the matching property to +`Server.Log#enabled` is `server.log.enabled`. + +== Hierarchy + +A config mapping can extend another mapping and inherit all its super members: + +[source,java] +---- +public interface Parent { + String name(); +} + +@ConfigMapping(prefix = "child") +public interface Child extends Parent { + +} +---- + +And override members: + +[source,java] +---- +public interface Parent { + String name(); +} + +@ConfigMapping(prefix = "child") +public interface Child extends Parent { + @WithName("child-name") + String name(); +} +---- + +== Overriding property names + +=== `@WithName` + +If a method name and a property name do not match, the `io.smallrye.config.WithName` annotation can +override the method name mapping and use the name supplied in the annotation. + +[source,java] +---- +@ConfigMapping(prefix = "server") +interface Server { + @WithName("name") + String host(); + + int port(); +} +---- + +[source,properties] +---- +server.name=localhost +server.port=8080 +---- + +=== `@WithParentName` + +The `io.smallrye.config.WithParentName` annotation allows configurations mappings to inherit its parent container name, +simplifying the configuration property name required to match the mapping. + +[source,java] +---- +@ConfigMapping(prefix = "server") +interface Server { + @WithParentName + ServerHostAndPort hostAndPort(); + + @WithParentName + ServerInfo info(); +} + +interface ServerHostAndPort { + String host(); + + int port(); +} + +interface ServerInfo { + String name(); +} +---- + +[source,properties] +---- +server.host=localhost +server.port=8080 +server.name=konoha +---- + +Without the `@WithParentName` the method `ServerInfo#name` maps the configuration property `server.info.name`. With +`@WithParentName`, the `Server#info` mapping will inherit the parent name from `Server` and `ServerInfo#name` maps to +the property `server.name` instead. + +=== NamingStrategy + +Method names in camelCase map to kebab-case configuration property names by default. + +[source,java] +---- +@ConfigMapping(prefix = "server") +interface Server { + String theHost(); + + int thePort(); +} +---- + +[source,properties] +---- +server.the-host=localhost +server.the-port=8080 +---- + +The mapping strategy can be adjusted by setting `namingStrategy` value in the `@ConfigMapping` annotation. + +[source,java] +---- +@ConfigMapping(prefix = "server", namingStrategy = ConfigMapping.NamingStrategy.VERBATIM) +public interface ServerVerbatimNamingStrategy { + String theHost(); + + int thePort(); +} +---- + +[source,properties] +---- +server.theHost=localhost +server.thePort=8080 +---- + +The `@ConfigMapping` annotation support the following naming stategies: + +* KEBAB++_++CASE - The method name is derived by replacing case changes with a dash to map the configuration property. +* VERBATIM - The method name is used as is to map the configuration property. +* SNAKE++_++CASE - The method name is derived by replacing case changes with an underscore to map the configuration property. + +=== `beanStyleGetters` + +The `beanStyleGetters` attribute (default `false`) enables matching bean-style getter names (`get`/`is` prefixed) to +their property name equivalent. For example, `getHost()` and `isEnabled()` map to the properties `host` and `enabled` +respectively: + +[source,java] +---- +@ConfigMapping(prefix = "server", beanStyleGetters = true) +public interface Server { + String getHost(); + + int getPort(); + + boolean isEnabled(); +} +---- + +[source,properties] +---- +server.host=localhost +server.port=8080 +server.enabled=true +---- + +[WARNING] +==== + +Bean-style getter matching allows multiple method names to match the same configuration name. For instance, +`getFoo` and `isFoo` both match `foo`, which may not be intended. Prefer simple method names that match +one-to-one with their configuration names. + +==== + +== Conversion + +A config mapping interface support automatic conversions of all types available for conversion in `Config`. + +[source,java] +---- +@ConfigMapping +public interface SomeTypes { + @WithName("int") + int intPrimitive(); + + @WithName("int") + Integer intWrapper(); + + @WithName("long") + long longPrimitive(); + + @WithName("long") + Long longWrapper(); + + @WithName("float") + float floatPrimitive(); + + @WithName("float") + Float floatWrapper(); + + @WithName("double") + double doublePrimitive(); + + @WithName("double") + Double doubleWrapper(); + + @WithName("char") + char charPrimitive(); + + @WithName("char") + Character charWrapper(); + + @WithName("boolean") + boolean booleanPrimitive(); + + @WithName("boolean") + Boolean booleanWrapper(); +} +---- + +[source,properties] +---- +int=9 +long=9999999999 +float=99.9 +double=99.99 +char=c +boolean=true +---- + +This is also valid for `Optional` and friends. + +[source,java] +---- +@ConfigMapping +public interface Optionals { + Optional server(); + + Optional optional(); + + @WithName("optional.int") + OptionalInt optionalInt(); + + interface Server { + String host(); + + int port(); + } +} +---- + +In this case, the mapping won’t fail if the configuraton properties values are missing. + +=== `@WithConverter` + +The `io.smallrye.config.WithConverter` annotation provides a way to set a specific `Converter` in a mapping. + +[source,java] +---- +@ConfigMapping +public interface Converters { + @WithConverter(FooBarConverter.class) + String foo(); +} + +public static class FooBarConverter implements Converter { + @Override + public String convert(final String value) { + return "bar"; + } +} +---- + +[source,properties] +---- +foo=foo +---- + +A call to `Converters.foo()` results in the value `bar`. + +== Collections + +A config mapping is also able to map the collections types `List` and `Set`. + +* A member with a `Collection` type requires the configuration name to be in its indexed format +* Each configuration name, plus its index maps the configuration value to the corresponding `Collection` element in the +object type +* The index must be part of the configuration path, by appending the index between square brackets to the`Collection` +member +* The index specified in the configuration name is used to order the element in the `Collection` +* Missing elements or gaps are removed + +[source,java] +---- +@ConfigMapping(prefix = "server") +public interface ServerCollections { + Set environments(); + + interface Environment { + String name(); + + List apps(); + + interface App { + String name(); + + List services(); + + Optional> databases(); + } + } +} +---- + +[source,properties] +---- +server.environments[0].name=dev +server.environments[0].apps[0].name=rest +server.environments[0].apps[0].services=bookstore,registration +server.environments[0].apps[0].databases=pg,h2 +server.environments[0].apps[1].name=batch +server.environments[0].apps[1].services=stock,warehouse +---- + +The `List` and `Set` mappings can use xref:config/indexed-properties.adoc[Indexed Properties] to map configuration values in +mapping groups. + +[NOTE] +==== + +A `List` mapping is backed by an `ArrayList`, and a `Set` mapping is backed by a `HashSet`. + +==== + +== Maps + +A config mapping is also able to map a `Map`. + +* A member with a `Map` type requires an additional configuration name added to the configuration path of the `Map` +member to act as a map key +* The additional configuration name maps a Map entry with the configuration name as the `Map` entry key and +the configuration value as the Map entry value + +[source,java] +---- +@ConfigMapping(prefix = "server") +public interface Server { + String host(); + + int port(); + + Map form(); + + Map> aliases(); + + interface Alias { + String name(); + } +} +---- + +[source,properties] +---- +server.host=localhost +server.port=8080 +server.form.index=index.html +server.form.login.page=login.html +server.form.error.page=error.html +server.aliases.localhost[0].name=prod +server.aliases.localhost[1].name=127.0.0.1 +server.aliases.\"io.smallrye\"[0].name=smallrye +---- + +The configuration property name needs to specify an additional segment to act as the map key. The `server.form` matches +the `Server#form` `Map` and the segments `index`, `login.page` and `error.page` represent the `Map` +keys. + +[NOTE] +==== + +A `Map` mapping is backed by an `HashMap`. + +==== + +When populating a `Map`, `SmallRyeConfig` requires the configuration names listed in +`SmallRyeConfig#getPropertyNames` to find the `Map` keys. If a `ConfigSource` does not support +`getPropertyNames` (empty), the names must be provided by another `ConfigSource` that can do so. After retrieving the +map keys, `SmallRyeConfig` performs the lookup of the values with the regular `ConfigSource` ordinal ordering. Even if +a `ConfigSource` does not provide `getPropertyNames` it can provide the value by having the name listed in another +capable `ConfigSource`. + +For collection types, the key requires the indexed format. The configuration name `server.aliases.localhost++[++0++]++.name` +maps to the `Map++<++String, List++<++Alias++>>++ aliases()` member, where `localhost` is the `Map` key, `++[++0++]++` is the index of the +`List++<++Alias++>++` collection where the `Alias` element will be stored, containing the name `prod`. + +[TIP] +.Map quoted keys requirement +==== + +The `Map` key part in the configuration property name may require quotes to delimit the key if the key contains +a dot, otherwise they are optional. If the key name is delimited by quotes, all configuration property names related +with the same name must use the quoted format across all sources, including Environment Variables. + +==== + +[WARNING] +.Do not mix quoted and unquoted keys +==== + +For a key that spans a single segment, the quoted and unquoted forms name the same `Map` key. Both +`server.aliases.localhost.name` and `server.aliases."localhost".name` refer to the key `localhost`, so writing both +makes the key ambiguous. + +When `SmallRyeConfig` finds both forms, it resolves the entry with the quoted form and logs a warning: + +.... +SRCFG01009: The Map path keys "server.aliases."localhost"" and "server.aliases.localhost" are ambiguous. These can +cause issues in the mapping. Please, do not mix quoted and unquoted in the same Map key. Using +server.aliases."localhost". +.... + +Because only the quoted form is used to look up the entry values, the property names written in the unquoted form +are left unmapped, and the mapping fails validation: + +.... +SRCFG00050: server.aliases.localhost.name does not map to any root +.... + +If the two forms also split the members of a nested group between them, the members that only exist in the +unquoted form are not found at all, and the mapping additionally fails with: + +.... +SRCFG00014: The config property server.aliases."localhost".name is required but it could not be found in any config source +.... + +Either form works on its own, so pick one and use it consistently for a given key across all sources. + +==== + +=== `@WithUnnamedKey` + +The `io.smallrye.config.WithUnnamedKey` annotation allows to omit a single map key in the configuration path: + +[source,java] +---- +@ConfigMapping(prefix = "server") +public interface Server { + @WithUnnamedKey("localhost") + Map aliases(); + + interface Alias { + String name(); + } +} +---- + +[source,properties] +---- +server.aliases.name=localhost +server.aliases.prod.name=prod +---- + +The `sever.aliases.name` is an unnamed `Map` property, because it does not contain the `Map` key to populate the `Map` +entry. Due to `@WithUnnamedKey("localhost")` the `Map` key is not required in the configuration path. The key used to +look up the Map entry is given by `io.smallrye.config.WithUnnamedKey#value`: + +[source,java] +---- +Server server = config.getConfigMapping(Server.class); +Map localhost = server.aliases.get("localhost"); +---- + +[WARNING] +==== + +If the unnamed key (in this case `localhost`) is explicitly set in a property name, the explicit value takes precedence + +==== + +The `eager` attribute (default `true`) controls whether the unnamed key entry is included in the `Map` when its values +come only from defaults. When `eager = false`, the unnamed key entry is excluded from the `Map` unless at least one +of its values is explicitly set in a configuration source: + +[source,java] +---- +@ConfigMapping(prefix = "server") +public interface Server { + @WithUnnamedKey(value = "localhost", eager = false) + Map aliases(); + + interface Alias { + @WithDefault("localhost") + String name(); + } +} +---- + +With `eager = false` and no properties set, `server.aliases` returns an empty `Map`. With `eager = true` (the +default), it returns a `Map` with the key `localhost` populated from the `@WithDefault`. + +=== `@WithKeys` + +The `io.smallrye.config.WithKeys` annotation allows to define which `Map` keys must be loaded by +the configuration: + +[source,java] +---- +@ConfigMapping(prefix = "server") +public interface Server { + @WithKeys(KeysProvider.class) + Map aliases(); + + interface Alias { + String name(); + } + + class KeysProvider implements Supplier> { + @Override + public Iterable get() { + return List.of("dev", "test", "prod"); + } + } +} +---- + +In this case, `SmallRyeConfig` will look for the map keys `dev`, `test` and `prod` instead of discovering the keys +with `SmallRyeConfig#getPropertyNames`: + +[source,properties] +---- +servers.alias.dev.name=dev +servers.alias.test.name=test +servers.alias.prod.name=prod +---- + +The provided list will effectively substitute the lookup in `SmallRyeConfig#getPropertyNames`, thus enabling a +`ConfigSource` that does not list its properties, to contribute configuration to the `Map`. Each key must exist in +the final configuration (relative to the `Map` path segment), or the mapping will fail with a +`ConfigValidationException`. + +=== `@WithDefaults` + +The `io.smallrye.config.WithDefaults` is a marker annotation to use only in a `Map` to return the default value for +the value element on any key lookup: + +[source,java] +---- +@ConfigMapping(prefix = "server") +public interface Server { + @WithDefaults + Map aliases(); + + interface Alias { + @WithDefault("localhost") + String name(); + } +} +---- + +[source,properties] +---- +server.aliases.prod.name=prod +---- + +A look-up to the `aliases` `Map` with the key `localhost`, `any` or any other key, returns a `Alias` instance, where +`Alias.name` is `localhost`, which is the default value. A look-up to `prod` returns a `Alias` instance, where +`Alias.name` is `prod` because the property is defined in the configuration as `server.aliases.prod.name=prod`. + +The `Map` can only iterate and size the defined keys. In this case, the `aliases` `Map` only iterates the `prod` key, +and the size is `1`. + +[source,java] +---- +Server server = config.getConfigMapping(Server.class); +Map localhost = server.aliases().get("localhost"); +Map any = server.aliases().get("any"); +Map prod = server.aliases().get("prod"); +---- + +== Optionals + +* A mapping can wrap any complex type with an `Optional` +* `Optional` mappings do not require the configuration path and value to be present + +== Secrets + +* A mapping can mark a member type as a secret with `Secret++<++T++>++`: + +[source,java] +---- +@ConfigMapping(prefix = "credentials") +public interface Credentials { + String username(); + + Secret password(); +} +---- + +A `Secret` value modifies the behaviour of the config system by: + +* Omitting the name of the secret in `SmallRyeConfig#getPropertyNames()` +* Omitting the name and value of the secret in the mapping `toString` method +* Throwing a `SecurityException` when trying to retrieve the value via `SmallRyeConfig` programmatic API + +A Secret can be of any type that can be converted by a registered `org.eclipse.microprofile.config.spi.Converter` of +the same type. + +== toString, equals, hashcode + +If the config mapping contains a `toString` method declaration, the config mapping instance will include a proper +implementation of the `toString` method. The `equals` and `hashcode` methods are included automatically. + +[CAUTION] +==== + +Do not include a `toString` declaration in a config mapping with sensitive information. + +==== + +== Validation + +A config mapping may combine annotations from https://beanvalidation.org/[Bean Validation] to validate configuration +properties values. + +[source,java] +---- +@ConfigMapping(prefix = "server") +interface Server { + @Size(min = 2, max = 20) + String host(); + + @Max(10000) + int port(); +} +---- + +The application startup fails with a `io.smallrye.config.ConfigValidationException` if the configuration properties +values do not follow the contraints defined in `Server`. + +[NOTE] +==== + +For validation to work, the `smallrye-config-validator` dependency is required in the classpath. + +==== diff --git a/content/docs/smallrye-config/main/config/profiles.adoc b/content/docs/smallrye-config/main/config/profiles.adoc new file mode 100644 index 000000000..ca2dd361e --- /dev/null +++ b/content/docs/smallrye-config/main/config/profiles.adoc @@ -0,0 +1,192 @@ += Profiles +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Applications often require different configurations depending on the target environment. For example, the local +development environment may be different from the production environment. Profiles allow for multiple configurations +in the same file or separate files and select between them via a profile name. + +== Profile aware properties + +To be able to set properties with the same name, each property needs to be prefixed with a percentage sign `%` followed +by the profile name and a dot `.` in the syntax `%++{++profile-name}.config.name`: + +.META-INF/microprofile-config.properties +[source,properties] +---- +http.port=8080 +%dev.http.port=8181 +---- + +To activate the profile `dev`, the configuration `smallrye.config.profile=dev` has to be set into any valid +`ConfigSource`. + +Any lookup to the `http.port` property name will first search by the active profile name `%dev.http.port` and then +fallback to `http.port` if no value is present. In this case a lookup to the property `http.port` with the `dev` profile +active, yields the value `8181`. + +[WARNING] +==== + +The profile must be set in one of the primary sources (system properties, environment variables, +`application.properties`, or any other source that does not require configuration) to determine the proper +configuration. + +==== + +== Profile aware files + +Properties for a specific profile may reside in a `microprofile-config-++{++profile}.properties` named file. The previous +example can be expressed as: + +.META-INF/microprofile-config.properties +[source,properties] +---- +http.port=8080 +---- + +.META-INF/microprofile-config-dev.properties +[source,properties] +---- +http.port=8181 +---- + +In this style, the property names in the profile aware file do not need to be prefixed with the profile name. + +[NOTE] +==== + +Properties in the profile aware file have priority over profile aware properties defined in the main file. + +==== + +[WARNING] +==== + +Do not use Profile aware files to set `smallrye.config.profile`. This will not work because the +the profile is required in advance to load the profile aware files. + +==== + +== Priority + +Profile lookups are only valid if the `ConfigSource` has a higher ordinal than a lookup to the regular configuration +name. Consider: + +.main.properties +[source,properties] +---- +config_ordinal=1000 +http.port=8080 +---- + +.profile.properties +[source,properties] +---- +config_ordinal=100 +%dev.http.port=8181 +---- + +Even with the profile `dev` active, the lookup value for `my.prop` is `1234`. This prevents lower ordinal sources to +set a profile property value that cannot be overridden unless the profile property is also overridden. + +== Multiple Profiles + +Multiple Profiles may be active at the same time. The configuration `smallrye.config.profile` accepts a comma-separated +list of profile names: `smallrye.config.profile=common,dev`. Both `common` and `dev` are separate profiles. + +When multiple profiles are active, the rules for profile configuration are the same. If two profiles define the same +configuration, then the last listed profile has priority. Consider: + +[source,properties] +---- +smallrye.config.profile=common,dev + +my.prop=1234 +%common.my.prop=1234 +%dev.my.prop=5678 + +%common.commom.prop=common +%dev.dev.prop=dev +%test.test.prop=test +---- + +Then + +* `common.prop` value is `common` +* `dev.prop` value is `dev` +* `my.prop` value is `5678` +* `test.prop` does not have a value + +It is also possible to define multiple profile properties, with a comma-separated list of profile names: + +[source,properties] +---- +%prod,dev.my.prop=1234 +---- + +The property name `common.prop` is active in both `dev` and `prod` profile. If the same property name exists in +multiple profile properties then, the property name with the most specific profile wins: + +[source,properties] +---- +smallrye.config.profile=dev + +%prod,dev.my.prop=1234 + +%dev.my.prop=5678 +---- + +Then `my.prop` value is `5678`. + +== Parent Profile + +A Parent Profile adds multiple levels of hierarchy to the current profile. The configuration +`smallrye.config.profile.parent` also acccepts a comma-separated list of profile names. + +When the Parent Profile is active, if a property cannot be found in the current active Profile, the config lookup +fallbacks to the Parent Profile. Consider: + +[source,properties] +---- +smallrye.config.profile=dev +smallrye.config.profile.parent=common + +my.prop=1234 +%common.my.prop=0 +%dev.my.prop=5678 + +%common.commom.prop=common +%dev.dev.prop=dev +%test.test.prop=test +---- + +Then + +* `common.prop` value is `common` +* `dev.prop` value is `dev` +* `my.prop` value is `0` +* `test.prop` does not have a value + +[WARNING] +==== + +Do not use Profile aware files to set smallrye.config.profile.parent++`++. This will not work because the +the profile is required in advance to load the profile aware files. + +==== + +=== Multi-level Hierarchy + +The Parent Profile also supports multiple levels of hierarchies: + +[source,properties] +---- +smallrye.config.profile=child +%child.smallrye.config.profile.parent=parent +%parent.smallrye.config.profile.parent=grandparent +%grandparent.smallrye.config.profile.parent=greatgrandparent +%greatgrandparent.smallrye.config.profile.parent=end +---- + +Will load the following profiles in order: `child`, `parent`, `grandparent`, `greatgrandparent`, `end` diff --git a/content/docs/smallrye-config/main/config/secret-keys.adoc b/content/docs/smallrye-config/main/config/secret-keys.adoc new file mode 100644 index 000000000..df27211bf --- /dev/null +++ b/content/docs/smallrye-config/main/config/secret-keys.adoc @@ -0,0 +1,232 @@ += Secret Keys +:version-root: ../ +include::{version-root}_attributes.adoc[] + +== Secret Keys Expressions + +In SmallRye Config, a secret configuration may be expressed as `$++{++handler::value}`, where the `handler` is the name of +a `io.smallrye.config.SecretKeysHandler` to decode or decrypt the `value` separated by a double colon `::`. + +It is possible to create a custom `SecretKeysHandler` and provide different ways to decode or decrypt configuration +values. + +A custom `SecretKeysHandler` requires an implementation of `io.smallrye.config.SecretKeysHandler` or +`io.smallrye.config.SecretKeysHandlerFactory`. Each implementation requires registration via the `ServiceLoader` +mechanism, either in `META-INF/services/io.smallrye.config.SecretKeysHandler` or +`META-INF/services/io.smallrye.config.SecretKeysHandlerFactory` files. + +=== Custom `SecretKeysHandler` + +A direct `SecretKeysHandler` implementation is suitable when the handler needs no configuration of its own: + +[source,java] +---- +public class Base64SecretKeysHandler implements SecretKeysHandler { + @Override + public String decode(final String secret) { + return new String(Base64.getDecoder().decode(secret)); + } + + @Override + public String getName() { + return "base64"; + } +} +---- + +.META-INF/services/io.smallrye.config.SecretKeysHandler + +.... +org.acme.config.Base64SecretKeysHandler +.... + +A secret value encoded with the `base64` handler can then be expressed as: + +[source,properties] +---- +my.secret=${base64::SGVsbG8gV29ybGQ=} +---- + +=== `SecretKeysHandlerFactory` + +When a handler requires configuration from other config sources (for example, a key or a credential read from +the config), use `SecretKeysHandlerFactory` instead. The factory receives a `ConfigSourceContext` that provides +access to all config sources initialized before the factory runs: + +[source,java] +---- +public class VaultSecretKeysHandlerFactory implements SecretKeysHandlerFactory { + @Override + public SecretKeysHandler getSecretKeysHandler(final ConfigSourceContext context) { + ConfigValue token = context.getValue("vault.token"); + return new VaultSecretKeysHandler(token.getValue()); + } + + @Override + public String getName() { + return "vault"; + } +} +---- + +.META-INF/services/io.smallrye.config.SecretKeysHandlerFactory +[source,properties] +---- +org.acme.config.VaultSecretKeysHandlerFactory +---- + +=== `LazySecretKeysHandler` + +`SecretKeysHandlerFactory` initializes during the first phase of `SmallRyeConfig` bootstrap, alongside regular +`ConfigSource` and `ConfigSourceProvider` registrations. This means that config values produced by a +`ConfigSourceFactory` are *not* yet available when the factory's `getSecretKeysHandler` is called. + +For handlers that depend on sources provided by a `ConfigSourceFactory`, wrap an inner `SecretKeysHandlerFactory` +in a `SecretKeysHandlerFactory.LazySecretKeysHandler`. The inner factory's `getSecretKeysHandler` is only invoked +the first time a value actually needs to be decoded, by which point all sources — including those from +`ConfigSourceFactory` — are fully initialized: + +[source,java] +---- +public class VaultSecretKeysHandlerFactory implements SecretKeysHandlerFactory { + @Override + public SecretKeysHandler getSecretKeysHandler(final ConfigSourceContext context) { + return new LazySecretKeysHandler(new SecretKeysHandlerFactory() { + @Override + public SecretKeysHandler getSecretKeysHandler(final ConfigSourceContext context) { + // This runs lazily, after all sources are ready. + ConfigValue token = context.getValue("vault.token"); + return new VaultSecretKeysHandler(token.getValue()); + } + + @Override + public String getName() { + return "vault"; + } + }); + } + + @Override + public String getName() { + return "vault"; + } +} +---- + +[WARNING] +==== + +The inner factory wrapped by `LazySecretKeysHandler` is what defers initialization. Do not call +`context.getValue` in the outer `getSecretKeysHandler`; only the inner factory's `getSecretKeysHandler` (invoked lazily) may resolve configuration values. + +==== + +[CAUTION] +==== + +It is not possible to mix Secret Keys Expressions with Property Expressions. + +==== + +=== Crypto + +The `smallrye-config-crypto` artifact contains a few out-of-the-box `SecretKeysHandler`s ready for use. It requires +the following dependency: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.config + smallrye-config-crypto + {version} + +---- + +==== AES/GCM/NoPadding `$++{++aes-gcm-nopadding::...}` + +* The encoding length is 128. +* The secret and the encryption key (without padding) must be base 64 encoded. + +[NOTE] +==== + +.application.properties +[source,properties] +---- +smallrye.config.secret-handler.aes-gcm-nopadding.encryption-key=DDne5obnfH1RSeTg71xSZg + +my.secret=${aes-gcm-nopadding::DLTb_9zxThxeT5iAQqswEl5Dn1ju4FdM9hIyVip35t5V} +---- + +The `$++{++aes-gcm-nopadding::...}` `SecretKeyHandler` requires +`smallrye.config.secret-handler.aes-gcm-nopadding.encryption-key` configuration to state the encryption key to be +used by the `aes-gcm-nopaddin` handler. + +A lookup to `my.secret` will use the `SecretKeysHandler` name `aes-gcm-nopadding` to decode the value +`DJNrZ6LfpupFv6QbXyXhvzD8eVDnDa++_++kTliQBpuzTobDZxlg`. + +==== + +[NOTE] +==== + +It is possible to generate the encrypted secret with the following http://jbang.dev/[JBang] script: + +[source,shell] +---- +jbang https://raw.githubusercontent.com/smallrye/smallrye-config/main/documentation/src/main/docs/config/secret-handlers/encryptor.java -s= -k=` +---- + +==== + +===== Configuration + +[width="100%",cols="90%,5%,5%",options="header",] +|=== +|Configuration Property |Type |Default +|`smallrye.config.secret-handler.aes-gcm-nopadding.encryption-key` + +The encryption key to use to decode secrets encoded by the `AES/GCM/NoPadding` algorithm. |String | +|`"smallrye.config.secret-handler.aes-gcm-nopadding.encryption-key-decode"` + +Decode the encryption key in Base64, if the plain text key was used to encrypt the secret. |boolean |false +|=== + +== Secret Keys Names + +When configuration properties contain passwords or other kinds of secrets, Smallrye Config can hide them to prevent +accidental exposure of such values. + +*This is no way a replacement for securing secrets.* Proper security mechanisms must still be used to secure +secrets. However, there is still the fundamental problem that passwords and secrets are generally encoded simply as +strings. Secret Keys provides a way to "lock" the configuration so that secrets do not appear unless explicitly enabled. + +To mark specific keys as secrets, register an instance of `io.smallrye.config.SecretKeysConfigSourceInterceptor` by +using the interceptor factory as follows: + +[source,java] +---- +public class SecretKeysConfigInterceptorFactory implements ConfigSourceInterceptorFactory { + @Override + public ConfigSourceInterceptor getInterceptor(ConfigSourceInterceptorContext context) { + return new SecretKeysConfigSourceInterceptor(Set.of("secret")); + } +} +---- + +Register the factory so that it can be found at runtime by creating a +`META-INF/services/io.smallrye.config.ConfigSourceInterceptorFactory` file that contains the fully qualified name of +this factory class. + +From this point forward, every lookup to the configuration name `secret` will throw a `SecurityException`. + +Access the Secret Keys using the APIs `io.smallrye.config.SecretKeys#doUnlocked(java.lang.Runnable)` +and `io.smallrye.config.SecretKeys#doUnlocked(java.util.function.Supplier++<++T++>++)`. + +[source,java] +---- +String secretValue = SecretKeys.doUnlocked(() -> { + config.getValue("secret", String.class); +}); +---- + +Secret Keys are only unlocked in the context of `doUnlocked`. Once the execution completes, the secrets become locked +again. diff --git a/content/docs/smallrye-config/main/converters/custom.adoc b/content/docs/smallrye-config/main/converters/custom.adoc new file mode 100644 index 000000000..e95f13ad9 --- /dev/null +++ b/content/docs/smallrye-config/main/converters/custom.adoc @@ -0,0 +1,101 @@ += Custom `Converter` +:version-root: ../ +include::{version-root}_attributes.adoc[] + +It is possible to create a custom `Converter` type as specified in +https://github.com/eclipse/microprofile-config/[MicroProfile Config]. + +A custom `Converter` requires an implementation of `org.eclipse.microprofile.config.spi.Converter`. Each implementation +requires registration via the `ServiceLoader` mechanism in the +`META-INF/services/org.eclipse.microprofile.config.spi.Converter` file. Consider: + +[source,java] +---- +package org.acme.config; + +public class CustomValue { + + private final int number; + + public CustomValue(int number) { + this.number = number; + } + + public int getNumber() { + return number; + } +} +---- + +The corresponding converter can look like: + +[source,java] +---- +package org.acme.config; + +import org.eclipse.microprofile.config.spi.Converter; + +public class CustomValueConverter implements Converter { + + @Override + public CustomValue convert(String value) { + return new CustomValue(Integer.parseInt(value)); + } +} +---- + +And registration in: + +.META-INF/services/org.eclipse.microprofile.config.spi.Converter +[source,properties] +---- +org.acme.config.CustomValueConverter +---- + +[WARNING] +==== + +The custom `Converter` class must be `public`, must have a `public` constructor with no arguments, and must not be +abstract. + +==== + +The `CustomValueConverter` converts the configuration value to the `CustomValue` type automatically. + +[source,java] +---- +Config config = Config.getOrCreate(); +CustomValue value = config.getValue("custom.value", CustomValue.class); +---- + +The `jakarta.annotation.Priority` annotation overrides the `Converter` priority and change converters precedence to fine +tune the execution order. By default, if no `@Priority` is specified by the `Converter`, the converter is registered +with a priority of `100`. Consider: + +[source,java] +---- +package org.acme.config; + +import jakarta.annotation.Priority; +import org.eclipse.microprofile.config.spi.Converter; + +@Priority(150) +public class SecretConverter implements Converter { + + @Override + public CustomValue convert(String value) { + final int secretNumber; + if (value.startsFrom("OBF:")) { + secretNumber = Integer.parseInt(SecretDecoder.decode(value)); + } else { + secretNumber = Integer.parseInt(value); + } + + return new CustomValue(secretNumber); + } +} +---- + +Two `Converter`s, (`CustomValueConverter` and `SecretConverter`) can convert the same type `CustomValue`. Since +`SecretConverter` has a priority of `150`, it will be used instead of a `CustomValueConverter` which has a default +priority of `100` (no annotation). diff --git a/content/docs/smallrye-config/main/extensions/fallback.adoc b/content/docs/smallrye-config/main/extensions/fallback.adoc new file mode 100644 index 000000000..05af7251d --- /dev/null +++ b/content/docs/smallrye-config/main/extensions/fallback.adoc @@ -0,0 +1,59 @@ += Fallback +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The `io.smallrye.config.FallbackConfigSourceInterceptor` allows to fall back to another configuration name, by +providing a transformation function or just a simple key value map. + +When a configuration name does not exist, there might be another configuration name that the config can +fall back to provide the same expected behavior. The fallback function is only applied if the original resolved +configuration name is not found and resolved to the fallback name. + +[source,java] +---- +package org.acme.config; + +import io.smallrye.config.FallbackConfigSourceInterceptor; + +public class MicroProfileConfigFallbackInterceptor extends FallbackConfigSourceInterceptor { + public MicroProfileConfigFallbackInterceptor() { + super(name -> name.startsWith("mp.config") ? + name.replaceAll("mp\\.config", "smallrye.config") : + name); + } +} +---- + +And registration in: + +.META-INF/services/io.smallrye.config.ConfigSourceInterceptor +[source,properties] +---- +org.acme.config.MicroProfileConfigFallbackInterceptor +---- + +The `MicroProfileConfigFallbackInterceptor` can fallback configuration names in the `mp.config` namespace +to the `smallrye.config` namespace. + +[NOTE] +==== + +.application.properties +[source,properties] +---- +mp.config.profile=test +smallrye.config.profile=prod +---- + +A lookup to `mp.config.profile` returns the value `test`. + +.application.properties +[source,properties] +---- +smallrye.config.profile=prod +---- + +A lookup to `mp.config.profile` returns the value `prod`. The config is not able to find a value for +`mp.config.profile`, so the interceptor fallbacks and lookups the value of `smallrye.config.profile`. + +==== diff --git a/content/docs/smallrye-config/main/extensions/interceptors.adoc b/content/docs/smallrye-config/main/extensions/interceptors.adoc new file mode 100644 index 000000000..8811bf0dc --- /dev/null +++ b/content/docs/smallrye-config/main/extensions/interceptors.adoc @@ -0,0 +1,179 @@ += Interceptors +:version-root: ../ +include::{version-root}_attributes.adoc[] + +SmallRye Config provides an interceptor chain that hooks into the configuration values resolution. This is useful to +implement features like xref:config/profiles.adoc[Profiles] , xref:config/expressions.adoc[Property Expressions], or just +logging to find out where the config value was loaded from. + +An interceptor can be created by implementing the +https://github.com/smallrye/smallrye-config/blob/main/implementation/src/main/java/io/smallrye/config/ConfigSourceInterceptor.java[ConfigSourceInterceptor] +interface. + +An interceptor requires an implementation of `io.smallrye.config.ConfigSourceInterceptor`. Each implementation requires +registration via the `ServiceLoader` mechanism in the `META-INF/services/io.smallrye.config.ConfigSourceInterceptor` +file. Alternatively, interceptors may be registered via the Programmatic API in +`SmallRyeConfigBuilder#withInterceptors`. + +`io.smallrye.config.ConfigSourceInterceptor` exposes two interception points: + +* `ConfigValue getValue(ConfigSourceInterceptorContext context, String name)` — intercepts the resolution of a +configuration value by name. +* `Iterator++<++String++>++ iterateNames(ConfigSourceInterceptorContext context)` — intercepts the iteration of all known +configuration property names. + +The `ConfigSourceInterceptorContext` is used to proceed with the interceptor chain. Calling `context.proceed(name)` +continues to the next interceptor in the chain. The chain can be short-circuited by returning a custom instance of +`io.smallrye.config.ConfigValue` from `getValue`, or a custom `Iterator++<++String++>++` from `iterateNames`. The `ConfigValue` +objects hold information about the key name, value, config source origin and ordinal. + +[NOTE] +==== + +The interceptor chain is applied before any conversion is performed on the configuration value. + +==== + +[source,java] +---- +package org.acme.config; + +import jakarta.annotation.Priority; + +import io.smallrye.config.ConfigSourceInterceptor; +import io.smallrye.config.Priorities; + +@Priority(Priorities.APPLICATION) +public class LoggingConfigSourceInterceptor implements ConfigSourceInterceptor { + @Override + public ConfigValue getValue(final ConfigSourceInterceptorContext context, final String name) { + ConfigValue configValue = context.proceed(name); + if (configValue != null) { + System.out.println("Looked up: " + configValue.getName() + "=" + configValue.getValue()); + } else { + System.out.println("Not found: " + name); + } + return configValue; + } +} +---- + +And registration in: + +.META-INF/services/io.smallrye.config.ConfigSourceInterceptor +[source,properties] +---- +org.acme.config.LoggingConfigSourceInterceptor +---- + +The `LoggingConfigSourceInterceptor` logs each configuration name lookup. The logged information includes the config +name and value, and (from the resolved `ConfigValue`) the config source origin and location if they exist. + +== `iterateNames` + +The `iterateNames` method intercepts the set of configuration property names visible to the rest of the chain. The +default implementation simply delegates to `context.iterateNames()`. Override it to add, remove, or transform names. + +A common use case is hiding sensitive property names from enumeration while still allowing their values to be +retrieved: + +[source,java] +---- +package org.acme.config; + +import java.util.Iterator; + +import jakarta.annotation.Priority; + +import io.smallrye.config.ConfigSourceInterceptor; +import io.smallrye.config.ConfigSourceInterceptorContext; +import io.smallrye.config.Priorities; + +@Priority(Priorities.APPLICATION) +public class HidingConfigSourceInterceptor implements ConfigSourceInterceptor { + @Override + public ConfigValue getValue(final ConfigSourceInterceptorContext context, final String name) { + return context.proceed(name); + } + + @Override + public Iterator iterateNames(final ConfigSourceInterceptorContext context) { + Iterator names = context.iterateNames(); + return new Iterator<>() { + @Override + public boolean hasNext() { + return names.hasNext(); + } + + @Override + public String next() { + String name = names.next(); + // filter out any name starting with "secret." + return name.startsWith("secret.") ? next() : name; + } + }; + } +} +---- + +[NOTE] +==== + +`iterateNames` affects the results of `Config#getPropertyNames()` and is used by Config Mappings to discover +`Map` keys. Interceptors that hide names will also prevent those names from being mapped into `Map` entries. + +==== + +== `ConfigSourceInterceptorContext` + +`ConfigSourceInterceptorContext` provides two ways to continue the chain: + +* `proceed(String name)` — passes the lookup to the *next* interceptor in the chain. This is the standard method +to use in most interceptors. +* `restart(String name)` — re-invokes the *first* interceptor in the chain from the beginning. This is intended for +relocating or compatibility interceptors that resolve a new name and need the full chain to re-evaluate it, including +profile expansion and expression resolution. Passing the original name to `restart` can cause an infinite loop, so +care must be taken. + +Example — an interceptor that resolves an alias name and restarts the full chain for the resolved name: + +[source,java] +---- +package org.acme.config; + +import jakarta.annotation.Priority; + +import io.smallrye.config.ConfigSourceInterceptor; +import io.smallrye.config.ConfigSourceInterceptorContext; +import io.smallrye.config.ConfigValue; +import io.smallrye.config.Priorities; + +@Priority(Priorities.APPLICATION) +public class AliasConfigSourceInterceptor implements ConfigSourceInterceptor { + @Override + public ConfigValue getValue(final ConfigSourceInterceptorContext context, final String name) { + if (name.equals("app.host")) { + // Restart the chain with the canonical name so profiles and + // expressions are re-applied to the resolved name. + return context.restart("server.host"); + } + return context.proceed(name); + } +} +---- + +Interceptors may also be created with an implementation of `io.smallrye.config.ConfigSourceInterceptorFactory`. Each +implementation requires registration via the `ServiceLoader` mechanism in the +`META-INF/services/io.smallrye.config.ConfigSourceInterceptorFactory` file. Alternatively, interceptors factories may +be registered via the Programmatic API in `SmallRyeConfigBuilder#withInterceptorFactories`. + +The `ConfigSourceInterceptorFactory` can initialize an interceptor with access to the current chain +(so it can be used to configure the interceptor and retrieve configuration values) and set the priority. + +A `ConfigSourceInterceptor` implementation class can specify a priority by way of the standard +`jakarta.annotation.Priority` annotation. If no priority is explicitly assigned, the default priority value of +`io.smallrye.config.Priorities.APPLICATION` is assumed. If multiple interceptors are registered with the same priority, +then their execution order may be non-deterministic. + +A collection of built-in priority constants can be found in `io.smallrye.config.Priorities`. It is recommended to +use `io.smallrye.config.Priorities.APPLICATION` as a baseline for user defined interceptors. diff --git a/content/docs/smallrye-config/main/extensions/logging.adoc b/content/docs/smallrye-config/main/extensions/logging.adoc new file mode 100644 index 000000000..3a42e6573 --- /dev/null +++ b/content/docs/smallrye-config/main/extensions/logging.adoc @@ -0,0 +1,15 @@ += Logging +:version-root: ../ +include::{version-root}_attributes.adoc[] + +== LoggingConfigSourceInterceptor + +The `io.smallrye.config.LoggingConfigSourceInterceptor` logs lookups of configuration names in the provided logging +platform. The log information includes config name and value, the config source origin and location if it exists. + +The log is done as `debug`, so the debug threshold must be set to `debug` for the `io.smallrye.config` appender to +display the logs. + +This requires registration via the `ServiceLoader` mechanism in the +`META-INF/services/io.smallrye.config.ConfigSourceInterceptorFactory` file of the +`io.smallrye.config.LoggingConfigSourceInterceptor` interceptor. diff --git a/content/docs/smallrye-config/main/extensions/relocate.adoc b/content/docs/smallrye-config/main/extensions/relocate.adoc new file mode 100644 index 000000000..e33ee4f6a --- /dev/null +++ b/content/docs/smallrye-config/main/extensions/relocate.adoc @@ -0,0 +1,51 @@ += Relocate +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The `io.smallrye.config.RelocateConfigSourceInterceptor` allows to relocate a configuration name to another name, by +providing a transformation function or just a simple key value map. + +When a configuration key is renamed, lookup needs to happen on the new name, but also on the old name if the +config sources are not updated yet. The relocation function gives priority to the new resolved configuration name or +resolves to the old name if no value is found under the new relocation name. + +[source,java] +---- +package org.acme.config; + +import io.smallrye.config.RelocateConfigSourceInterceptor; + +public class MicroProfileConfigRelocateInterceptor extends RelocateConfigSourceInterceptor { + public MicroProfileConfigRelocateInterceptor() { + super(name -> name.startsWith("mp.config") ? + name.replaceAll("mp\\.config", "smallrye.config") : + name); + } +} +---- + +And registration in: + +.META-INF/services/io.smallrye.config.ConfigSourceInterceptor +[source,properties] +---- +org.acme.config.MicroProfileConfigRelocateInterceptor +---- + +The `MicroProfileConfigRelocateInterceptor` can relocate configuration names in the `mp.config` namespace +to the `smallrye.config` namespace. + +[NOTE] +==== + +.application.properties +[source,properties] +---- +mp.config.profile=test +smallrye.config.profile=prod +---- + +A lookup to `mp.config.profile` returns the value `prod`. The config finds a valid value in the relocated name +`smallrye.config.profile`, so the interceptor will use this value instead of the one in `mp.config.profile`. + +==== diff --git a/content/docs/smallrye-config/main/index.adoc b/content/docs/smallrye-config/main/index.adoc new file mode 100644 index 000000000..dca56db0f --- /dev/null +++ b/content/docs/smallrye-config/main/index.adoc @@ -0,0 +1,97 @@ += SmallRye Config +include::_attributes.adoc[] + +*SmallRye Config* is a library that provides a way to configure applications, frameworks and containers. It is used +in applications servers like https://wildfly.org/[WildFly], https://openliberty.io[Open Liberty] and +https://tomee.apache.org[TomEE] or frameworks like https://quarkus.io[Quarkus]. It can also be used completely +standalone in any Java application, which makes it a very flexible library. + +It follows the https://github.com/eclipse/microprofile-config/[MicroProfile Config] specification to provide +the initial config foundations and expands with it own concepts to cover a wide range of use cases observed in the +configuration space. + +== Use SmallRye Config in a Java application + +Add the _dependency_ to your project using your preferred build tool: + +[.tabs] +==== + +Maven:: ++ +-- + +[source,xml,subs="attributes+"] +---- + + io.smallrye.config + smallrye-config + {version} + +---- + +-- + +Gradle (Groovy):: ++ +-- + +[source,groovy,subs="attributes+"] +---- +implementation 'io.smallrye.config:smallrye-config:{version}' +---- + +-- + +Gradle (Kotlin):: ++ +-- + +[source,kotlin,subs="attributes+"] +---- +implementation("io.smallrye.config:smallrye-config:{version}") +---- + +-- + +JBang:: ++ +-- + +[source,java,subs="attributes+"] +---- +//DEPS io.smallrye.config:smallrye-config:{version} +---- + +-- + +==== + +And retrieve a `SmallRyeConfig` instance with: + +[source,java] +---- +Config config = io.smallrye.config.Config.getOrCreate(); +---- + +[NOTE] +==== + +The `SmallRyeConfig` instance will be created and registered to the context class loader if no such configuration +is already created and registered. + +==== + +Or build your own: + +[source,java] +---- +SmallRyeConfig config = new SmallRyeConfigBuilder().build(); +---- + +[NOTE] +==== + +`io.smallrye.config.Config` is the entry point to all the config capabilities provided by SmallRye Config. + +==== diff --git a/content/docs/smallrye-fault-tolerance/5/_attributes.adoc b/content/docs/smallrye-fault-tolerance/5/_attributes.adoc new file mode 100644 index 000000000..605282246 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/5/_attributes.adoc @@ -0,0 +1,24 @@ +:project-version: 5.6.0 +// Attributes shared by every page. smallrye.io overrides the ones it needs when it publishes. +// Pages in subdirectories set :version-root: (../ per level) before including this file. +ifndef::version-root[:version-root:] +:idprefix: +:idseparator: - +:sectanchors: +:partialsdir: {version-root}_partials +:examples-dir: {version-root}_examples +:smallrye-fault-tolerance: SmallRye Fault Tolerance +:smallrye-fault-tolerance-version: 5.6.0 +:microprofile-fault-tolerance: MicroProfile Fault Tolerance +:microprofile-fault-tolerance-version: 3.0 +:microprofile-fault-tolerance-url: https://download.eclipse.org/microprofile/microprofile-fault-tolerance-3.0/microprofile-fault-tolerance-spec-3.0.html +:vertx4-version: 4.3.4 +:generated-dir: {version-root}_generated + +// Added by smallrye.io (tools/docs/Docs.java sync) +:javadoc-version: 5.6.0 +:relfileprefix: /docs/smallrye-fault-tolerance/5/ +:imagesdir: /docs/smallrye-fault-tolerance/5 +:docs-project: smallrye-fault-tolerance +:docs-version: 5 +:docs-ref: 5.6.0 diff --git a/content/docs/smallrye-fault-tolerance/5/_nav.yml b/content/docs/smallrye-fault-tolerance/5/_nav.yml new file mode 100644 index 000000000..2f9f56b2a --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/5/_nav.yml @@ -0,0 +1,44 @@ +title: SmallRye Fault Tolerance +nav: +- title: Overview + page: index +- title: Usage + items: + - title: Basic Usage + page: usage/basic + - title: Extra Features + page: usage/extra + - title: Programmatic API + page: usage/programmatic-api +- title: Integration + items: + - title: Introduction + page: integration/intro + - title: Metrics + page: integration/metrics + - title: Thread Pool + page: integration/thread-pool + - title: Context Propagation + page: integration/context-propagation + - title: OpenTracing + page: integration/opentracing + - title: Event Loop + page: integration/event-loop + - title: Additional Asynchronous Types Integration Concerns + page: integration/async-types + - title: Kotlin Integration Concerns + page: integration/kotlin + - title: Programmatic API Integration Concerns + page: integration/programmatic-api +- title: Internals + items: + - title: Project Structure + page: internals/project-structure + - title: Instructions + page: internals/instructions + - title: Fault Tolerance Core + page: internals/core + - title: Configuration System + page: internals/config + - title: Logging + page: internals/logging diff --git a/content/docs/smallrye-fault-tolerance/5/index.adoc b/content/docs/smallrye-fault-tolerance/5/index.adoc new file mode 100644 index 000000000..71bfe345f --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/5/index.adoc @@ -0,0 +1,11 @@ += SmallRye Fault Tolerance Documentation +include::_attributes.adoc[] + +{smallrye-fault-tolerance} {smallrye-fault-tolerance-version} is an implementation of https://github.com/eclipse/microprofile-fault-tolerance/[Eclipse {microprofile-fault-tolerance}] {microprofile-fault-tolerance-version}. + + +Runtimes that currently use {smallrye-fault-tolerance} include: + +* https://quarkus.io/[Quarkus] +* https://wildfly.org/[WildFly] +* https://thorntail.io/[Thorntail] diff --git a/content/docs/smallrye-fault-tolerance/5/integration/async-types.adoc b/content/docs/smallrye-fault-tolerance/5/integration/async-types.adoc new file mode 100644 index 000000000..f37491fd9 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/5/integration/async-types.adoc @@ -0,0 +1,14 @@ += Additional Asynchronous Types Integration Concerns +:version-root: ../ +include::{version-root}_attributes.adoc[] + +This page describes integration concerns for xref:usage/extra.adoc#async-types[Additional Asynchronous Types]. + +To enable support of additional asynchronous types, it is required that the support library is present. +That is: + +* https://smallrye.io/smallrye-mutiny/[Mutiny]: `io.smallrye:smallrye-fault-tolerance-mutiny` +* https://github.com/ReactiveX/RxJava/tree/3.x[RxJava 3]: `io.smallrye:smallrye-fault-tolerance-rxjava3` + +These libraries include some service providers that {smallrye-fault-tolerance} will automatically load and use. +Therefore, no more integration is necessary. diff --git a/content/docs/smallrye-fault-tolerance/5/integration/context-propagation.adoc b/content/docs/smallrye-fault-tolerance/5/integration/context-propagation.adoc new file mode 100644 index 000000000..a39b7816d --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/5/integration/context-propagation.adoc @@ -0,0 +1,8 @@ += Context Propagation +:version-root: ../ +include::{version-root}_attributes.adoc[] + +{smallrye-fault-tolerance} supports MicroProfile Context Propagation for `@Asynchronous` method invocations, if 2 conditions are satisfied: + +- the integrator provides a MicroProfile Context Propagation enabled thread pool (see xref:integration/thread-pool.adoc[Thread Pool]); +- the `io.smallrye:smallrye-fault-tolerance-context-propagation` artifact is present (it provides alternative implementations of some internal services, loaded using `ServiceLoader`). diff --git a/content/docs/smallrye-fault-tolerance/5/integration/event-loop.adoc b/content/docs/smallrye-fault-tolerance/5/integration/event-loop.adoc new file mode 100644 index 000000000..c86a28679 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/5/integration/event-loop.adoc @@ -0,0 +1,32 @@ += Event Loop +:version-root: ../ +include::{version-root}_attributes.adoc[] + +{smallrye-fault-tolerance} provides an API to integrate with event loops used in non-blocking runtimes. + +WARNING: This integration API comes from the `core` module and so is treated as private. +If you intend to implement it, please let us know in the {smallrye-fault-tolerance} issue tracker. +We'd prefer all the implementations to live in the {smallrye-fault-tolerance} project. + +The integrator must implement the `EventLoop` interface, which lets {smallrye-fault-tolerance} submit ``Runnable``s to the event loop for execution. +Implementations are discovered through `ServiceLoader`, so the corresponding `META-INF/services/\...` file must also exist. + +At most one implementation of `EventLoop` may be present at runtime. +{smallrye-fault-tolerance} will use it to execute non-blocking guarded methods when the caller was originally on an event loop thread. +This is important in case of: + +- retries, when there's a delay between retry attempts (the delay must not block); +- bulkheads, when a task is queued (such task is executed when some other task finishes, which happens on an unknown thread). + +If no event loop integration is present, the tasks described above will be offloaded to the xref:integration/thread-pool.adoc[thread pool]. + +== Vert.x + +{smallrye-fault-tolerance} includes an implementation of the event loop integration API for Vert.x. +This implementation is compiled and tested against Vert.x version {vertx4-version}, but should also work fine with Vert.x 3.x. + +This implementation is present in the `io.smallrye:smallrye-fault-tolerance-vertx` artifact. +Including this artifact is enough to enable the Vert.x integration. + +NOTE: The Vert.x implementation is treated as public, but only as described here. +That is, the artifact should be treated as opaque, no guarantees about its internals are made. diff --git a/content/docs/smallrye-fault-tolerance/5/integration/intro.adoc b/content/docs/smallrye-fault-tolerance/5/integration/intro.adoc new file mode 100644 index 000000000..71bcf3074 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/5/integration/intro.adoc @@ -0,0 +1,40 @@ += Introduction +:version-root: ../ +include::{version-root}_attributes.adoc[] + +{smallrye-fault-tolerance} comprises the following main artifacts: + +* `io.smallrye:smallrye-fault-tolerance-api`: public API for the features provided on top of {microprofile-fault-tolerance}, including a programmatic API; +* `io.smallrye:smallrye-fault-tolerance-core`: core implementations of fault tolerance strategies; +* `io.smallrye:smallrye-fault-tolerance`: CDI-based implementation of {microprofile-fault-tolerance} with {smallrye-fault-tolerance} extensions, including a programmatic API; +* `io.smallrye:smallrye-fault-tolerance-autoconfig-core`: runtime part of the {smallrye-fault-tolerance} configuration system. +** (There's also `io.smallrye:smallrye-fault-tolerance-autoconfig-processor`, but that is only used during {smallrye-fault-tolerance} build and isn't required at runtime.) + +These artifacts must be always present, together with the following dependencies: + +* Jakarta CDI, Annotations and Interceptors API; +* `org.eclipse.microprofile.fault-tolerance:microprofile-fault-tolerance-api`; +* `org.eclipse.microprofile.config:microprofile-config-api` and some implementation; +* `org.jboss.logging:jboss-logging`. + +Some other artifacts are provided to facilitate additional integrations. +These are described in the subsequent sections. + +Most of the integration is performed automatically by the CDI Portable Extension present in {smallrye-fault-tolerance}: `io.smallrye.faulttolerance.FaultToleranceExtension`. +Make sure the extension is active. +The remaining integration concerns are described in the following sections: + +* xref:integration/metrics.adoc[emitting metrics] for fault tolerance strategies; +* xref:integration/thread-pool.adoc[providing a thread pool] for `@Asynchronous` methods and other asynchronous tasks; +* xref:integration/context-propagation.adoc[enabling Context Propagation] for `@Asynchronous` methods; +* xref:integration/opentracing.adoc[enabling OpenTracing integration] with Context Propagation; +* xref:integration/event-loop.adoc[integrating an event loop] for non-blocking method invocations; +* xref:integration/async-types.adoc[integrating additional types] for asynchronous invocations; +* xref:integration/kotlin.adoc[integrating Kotlin support] for `suspend` functions; +* xref:integration/programmatic-api.adoc[integrating the programmatic API] that {smallrye-fault-tolerance} provides. + +== Standalone Implementation of the Programmatic API + +A completely standalone implementation of the programmatic API exists in the `io.smallrye:smallrye-fault-tolerance-standalone` artifact. +This is supposed to be used in a non-CDI environment. +Runtimes that integrate {smallrye-fault-tolerance} typically do so using CDI, and so should ignore this artifact. diff --git a/content/docs/smallrye-fault-tolerance/5/integration/kotlin.adoc b/content/docs/smallrye-fault-tolerance/5/integration/kotlin.adoc new file mode 100644 index 000000000..07a098e38 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/5/integration/kotlin.adoc @@ -0,0 +1,11 @@ += Kotlin Integration Concerns +:version-root: ../ +include::{version-root}_attributes.adoc[] + +This page describes integration concerns for xref:usage/extra.adoc#kotlin-suspend-functions[Kotlin `suspend` functions]. + +To enable support of Kotlin suspending functions, it is required that the support library is present. +Its coordinates are: `io.smallrye:smallrye-fault-tolerance-kotlin`. + +This library includes some service providers that {smallrye-fault-tolerance} will automatically load and use. +Therefore, no more integration is necessary. diff --git a/content/docs/smallrye-fault-tolerance/5/integration/metrics.adoc b/content/docs/smallrye-fault-tolerance/5/integration/metrics.adoc new file mode 100644 index 000000000..61e58b0bc --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/5/integration/metrics.adoc @@ -0,0 +1,45 @@ += Metrics +:version-root: ../ +include::{version-root}_attributes.adoc[] + +{smallrye-fault-tolerance} provides support for MicroProfile Metrics and Micrometer. +Alternatively, metrics may be completely disabled at the integration level. + +As usual, this integration is based on CDI, so an integrator must select one of the following classes and add it to the set of types scanned during CDI bean discovery: + +* `io.smallrye.faulttolerance.metrics.MicroProfileMetricsProvider` +* `io.smallrye.faulttolerance.metrics.MicrometerProvider` +* `io.smallrye.faulttolerance.metrics.NoopProvider` + +All these classes implement an internal interface `MetricsProvider`. +This interface, together with an internal core interface `MetricsRecorder`, is used by the rest of {smallrye-fault-tolerance} to interact with the metrics system. + +NOTE: Only the _names_ of the classes listed above are treated as public. +That is, the classes should be treated as opaque, no guarantees about their internals are made. + +The possibility to choose a metrics implementation is currently mainly provided for Quarkus. +The CDI Portable Extension present in {smallrye-fault-tolerance} always assumes MicroProfile Metrics are required. + +Metrics providers have additional requirements, as described below. + +== MicroProfile Metrics + +If MicroProfile Metrics are used, the integrator must ensure that the following artifacts are present: + +* `org.eclipse.microprofile.metrics:microprofile-metrics-api`; +* some implementation of MicroProfile Metrics. + +NOTE: As mentioned above, when an integrator relies on the CDI Portable Extension provided by {smallrye-fault-tolerance}, this is currently the default and only option. +Therefore, presence of MicroProfile Metrics is mandatory for those integrators. + +== Micrometer + +If Micrometer is used, the integrator must ensure that the following artifact is present: + +* `io.micrometer:micrometer-core`. + +== Noop + +If the "noop" provider is used, metrics are completely disabled and the `MP_Fault_Tolerance_Metrics_Enabled` configuration property is ignored at runtime. + +No metrics API and implementation is required in this case. diff --git a/content/docs/smallrye-fault-tolerance/5/integration/opentracing.adoc b/content/docs/smallrye-fault-tolerance/5/integration/opentracing.adoc new file mode 100644 index 000000000..9d5955ff8 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/5/integration/opentracing.adoc @@ -0,0 +1,13 @@ += OpenTracing +:version-root: ../ +include::{version-root}_attributes.adoc[] + +{smallrye-fault-tolerance} includes an integration between MicroProfile Context Propagation and OpenTracing. +That is, {smallrye-fault-tolerance} includes an implementation of `ThreadContextProvider` which propagates the active OpenTracing span from one thread to another. +This can be used when xref:integration/context-propagation.adoc[Context Propagation] is enabled. + +This integration is present in the `io.smallrye:smallrye-fault-tolerance-tracing-propagation` artifact. +It is used automatically when xref:integration/context-propagation.adoc[Context Propagation] is present. +When Context Propagation is not present, this artifact can still be in place, it just won't do anything. + +Technically, this doesn't even belong to {smallrye-fault-tolerance}, but it's present for historical reasons. diff --git a/content/docs/smallrye-fault-tolerance/5/integration/programmatic-api.adoc b/content/docs/smallrye-fault-tolerance/5/integration/programmatic-api.adoc new file mode 100644 index 000000000..a5ba5c395 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/5/integration/programmatic-api.adoc @@ -0,0 +1,25 @@ += Programmatic API Integration Concerns +:version-root: ../ +include::{version-root}_attributes.adoc[] + +This page describes integration concerns of the xref:usage/programmatic-api.adoc[Programmatic API] of {smallrye-fault-tolerance}. + +== Standalone Implementation + +The standalone implementation provides no integration points. +It is, as the name suggests, completely standalone. + +At the moment, it is not possible to change the thread pool to which actions are offloaded. +A single cached thread pool, obtained using `Executors.newCachedThreadPool()`, is used for all thread offloads. + +Users of the standalone implementation that also use an event loop based library, such as Vert.x, may integrate the event loop support as described in xref:integration/event-loop.adoc[Event Loop]. + +== CDI implementation + +The CDI implementation will use the thread pool an integrator provides (see xref:integration/thread-pool.adoc[Thread Pool]). +This also extends to the context propagation integration (see xref:integration/context-propagation.adoc[Context Propagation]). + +It will also use the event loop support, if integrator provides one (see xref:integration/event-loop.adoc[Event Loop]). + +In other words, runtimes that use the CDI implementation don't have to do any extra integration work. +The existing integration is enough. diff --git a/content/docs/smallrye-fault-tolerance/5/integration/thread-pool.adoc b/content/docs/smallrye-fault-tolerance/5/integration/thread-pool.adoc new file mode 100644 index 000000000..b223fd4fb --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/5/integration/thread-pool.adoc @@ -0,0 +1,30 @@ += Thread Pool +:version-root: ../ +include::{version-root}_attributes.adoc[] + +{smallrye-fault-tolerance} expects integrators to provide a thread pool for running `@Asynchronous` methods and other asynchronous tasks. +This integration is based on CDI. + +The integrator should provide a CDI bean which implements `AsyncExecutorProvider`. +The bean should be `@Singleton`, must be marked as alternative and selected globally for the application. + +There is a default thread pool in case no integration is provided, but it shouldn't be used outside of tests or experiments. +This is provided by the `AsyncExecutorProvider` default implementation, `DefaultAsyncExecutorProvider`. + +Integrators that are themselves Jakarta EE application servers and don't want to expose their own thread pool can subclass `DefaultAsyncExecutorProvider`. +This at least allows customizing the `ThreadFactory`. + +== Configuration + +If the integrator doesn't provide a thread pool, the default thread pool is used as described above. +In such case, the following configuration properties are recognized: + +- `io.smallrye.faulttolerance.mainThreadPoolSize`: maximum number of threads in the thread pool. +Defaults to `100`. + +- `io.smallrye.faulttolerance.mainThreadPoolQueueSize`: size of the queue that the thread pool should use. +Can be `> 0`, in which case the queue will be bounded to given size. +Can be `0`, in which case there will be no queue, and the thread pool will reject excess tasks. +Can be `-1`, in which case the queue will be unbounded. +Values `< -1` are an error. +Defaults to `-1`. diff --git a/content/docs/smallrye-fault-tolerance/5/internals/config.adoc b/content/docs/smallrye-fault-tolerance/5/internals/config.adoc new file mode 100644 index 000000000..26c7a1045 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/5/internals/config.adoc @@ -0,0 +1,103 @@ += Configuration System +:version-root: ../ +include::{version-root}_attributes.adoc[] + +WARNING: Please note that everything explained here is an implementation detail. +API stability for these interfaces and classes is _not_ guaranteed! +This documentation is only present to provide implementation insight for people who have to debug issues or want to contribute. + +The xref:internals/core.adoc[core] fault tolerance strategies are configured simply by constructor parameters. +This is fine, since they are an internal implementation detail. +However, the constructor parameters correspond closely to the members of {microprofile-fault-tolerance} annotations. +The idea is that core fault tolerance strategies have no dependency on the {microprofile-fault-tolerance} API, but given a fault tolerance annotation, creating an instance of the corresponding strategy should be straightforward. + +On top of the core layer, {smallrye-fault-tolerance} implements {microprofile-fault-tolerance}, including a configuration system conforming to the specification. +This document briefly describes how the configuration system works. + +== `FaultToleranceOperation` + +The fault tolerance interceptor, which is the main entrypoint, looks up a `FaultToleranceOperation` instance for each class/method combination it has to process. +The `FaultToleranceOperation` holds a bean class, a descriptor of the guarded method, and configuration for each fault tolerance strategy applied to the method. +It also orchestrates configuration validation. +The interceptor uses a `FaultToleranceOperation` to instantiate all the core fault tolerance strategies for given guarded method. + +The `FaultToleranceOperation` instances are created early on, during application deployment, for all beans that use {microprofile-fault-tolerance}. +It may happen that a `FaultToleranceOperation` is created lazily, for special constructs such as runtime-built proxies, but that is largely irrelevant from the configuration perspective. + +== `FaultToleranceMethod` + +A `FaultToleranceOperation` is created from a `FaultToleranceMethod`. +A `FaultToleranceMethod` holds, again, a bean class, a descriptor of the guarded method, and the annotation instance for each annotation that is present on the guarded method or the class declaring the method. +It also knows what annotations have been declared directly on the method, and what annotations have been declared on the class. +This is because the {microprofile-fault-tolerance} configuration rules distinguish these two cases. + +A `FaultToleranceMethod` must be created in a CDI extension, because it represents the knowledge CDI has about the guarded method. +Note that CDI extensions may add, remove or modify annotations, so `FaultToleranceMethod` doesn't just hold the annotation instances as present in the class bytecode. + +.Quarkus +**** +In environments that bootstrap the CDI container during application build, such as Quarkus, the set of all ``FaultToleranceMethod``s in the application should be collected during build and transferred to runtime. +Then, at application runtime, ``FaultToleranceOperation``s should be created and validated. + +{smallrye-fault-tolerance} doesn't do anything like that, but is designed with Quarkus (and eventual CDI Lite) in mind. +**** + +The main difference between `FaultToleranceMethod` and `FaultToleranceOperation` is that `FaultToleranceMethod` holds annotation instances, while `FaultToleranceOperation` holds configuration. + +== `@AutoConfig` + +As described above, `FaultToleranceMethod` holds annotation instances, while `FaultToleranceOperation` holds configuration. +Yet, `FaultToleranceOperation` exposes the annotation types (such as `@Fallback`) to anyone who needs the configuration. +The configuration consumers simply call the annotation methods (such as `fallbackMethod()`), and configuration is handled behind the scenes, following the {microprofile-fault-tolerance} configuration rules. + +The infrastructure behind this is present in the `autoconfig` module. +This module allows creating simple configuration interfaces like this: + +[source,java] +---- +@AutoConfig +interface FallbackConfig extends Fallback, Config { + default void validate() { + ... + } +} +---- + +The config interface must: + +. be annotated with `@AutoConfig`; +. extend the annotation interface (`Fallback` in this case); +. extend the `Config` interface (from the `autoconfig/core` module); +. provide a `default` implementation of the `validate` method (defined in `Config`). + +For each such interface, an implementation class will be automatically generated by an annotation processor (implemented in the `autoconfig/processor` module). +In this case, it would be called `FallbackConfigImpl`. + +This implementation class has a `static` method called `create` that accepts a `FaultToleranceMethod`. +It follows that for each guarded method, a new instance must be created. +If given `FaultToleranceMethod` doesn't contain an instance of particular annotation, `create` simply returns `null`. + +The implementation class also overrides all methods from the annotation type. +Each annotation method is implemented to first consult MicroProfile Config, and only second to consult the annotation instance (which, as we described above, is present on the `FaultToleranceMethod`). + +The implementation class is supposed to be used just like the annotation type itself. +In other words, to obtain configured values, call the annotation methods on the config interface instance. +For example, to obtain the `fallbackMethod` value, with respect to the {microprofile-fault-tolerance} configuration rules, we obtain an instance of `FallbackConfigImpl` and call `fallbackMethod`. + +The `FaultToleranceOperation` class holds instances of these config interfaces, and exposes them to configuration consumers. +It actually only exposes the annotation types, not the full config interface, but that is enough. + +== Putting it all together + +Let's recap how it all works. + +First, for each guarded method in the application, a `FaultToleranceMethod` instance is created, holding all the annotations that affect the method. + +Second, for each `FaultToleranceMethod`, a `FaultToleranceOperation` is created. +That, in turn, creates an instance of all relevant config interfaces. + +Finally, the fault tolerance interceptor looks up a `FaultToleranceOperation` and creates a chain of fault tolerance strategies. +For each fault tolerance strategy in the chain, the interceptor looks up configuration values from the corresponding config interface. + +The chain of fault tolerance strategies is created on the first invocation to a given guarded method and cached for all subsequent invocations. +That is how we satisfy the requirement that all stateful fault tolerance strategies are singletons, but that's a different story. diff --git a/content/docs/smallrye-fault-tolerance/5/internals/core.adoc b/content/docs/smallrye-fault-tolerance/5/internals/core.adoc new file mode 100644 index 000000000..a98aa743a --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/5/internals/core.adoc @@ -0,0 +1,105 @@ += Fault Tolerance Core +:version-root: ../ +include::{version-root}_attributes.adoc[] + +WARNING: Please note that everything explained here is an implementation detail. +API stability for these interfaces and classes is _not_ guaranteed! +This documentation is only present to provide implementation insight for people who have to debug issues or want to contribute. + +The core idea in {smallrye-fault-tolerance} is a `FaultToleranceStrategy`. +It is an interface that looks like this: + +[source,java] +---- +interface FaultToleranceStrategy { + V apply(InvocationContext ctx) throws Exception; +} +---- + +The `InvocationContext` is a `Callable` that represents the method invocation guarded by this fault tolerance strategy. +The fault tolerance strategy does its work around `ctx.call()`. +It can catch exceptions, invoke `ctx.call()` multiple times, invoke something else, etc. +As an example, let's consider this strategy, applicable to methods that return a `String`: + +[source,java] +---- +public class MyStringFallback implements FaultToleranceStrategy { + @Override + public String apply(InvocationContext ctx) { + try { + return ctx.call(); + } catch (Exception ignored) { + return "my string value"; + } + } +} +---- + +This is a very simple fallback mechanism, which returns a pre-defined value in case of an exception. + +In the {smallrye-fault-tolerance} codebase, you can find implementations of all the strategies required by {microprofile-fault-tolerance}: retry, fallback, timeout, circuit breaker or bulkhead. +Asynchronous invocation, delegated to a thread pool, is of course also supported. + +When multiple fault tolerance strategies are supposed to be used to guard one method, they form a chain. +Continuing with our simple example, adding the ability to chain with another strategy would look like this: + +[source,java] +---- +public class MyStringFallback implements FaultToleranceStrategy { + private final FaultToleranceStrategy delegate; + + public MyStringFallback(FaultToleranceStrategy delegate) { + this.delegate = delegate; + } + + @Override + public String apply(InvocationContext ctx) { + try { + return delegate.apply(ctx); + } catch (Exception ignored) { + return "my string value"; + } + } +} +---- + +We see that one strategy delegates to another, passing the `InvocationContext` along. +In fact, all the implementations in {smallrye-fault-tolerance} are written like this: they expect to be used in a chain, so they take another `FaultToleranceStrategy` to which they delegate. +But if all strategies have this form, when is `ctx.call()` actually invoked? +Good question! +The ultimate `ctx.call()` invocation is done by a special fault tolerance strategy which is called, well, `Invocation`. + +As an example which uses real {microprofile-fault-tolerance} annotations, let's consider this method: + +[source,java] +---- +@Retry(...) +@Timeout(...) +@Fallback(...) +public void doSomething() { + ... +} +---- + +The chain of fault tolerance strategies will look roughly like this: + +[source] +---- +Fallback( + Retry( + Timeout( + Invocation( + // ctx.call() will happen here + // that will, in turn, invoke doSomething() + ) + ) + ) +) +---- + +The order in which the strategies are chained (or, in fact, nested) is specified by {microprofile-fault-tolerance}. + +Other strategies might also be present in more complex cases. +For example, if metrics are enabled, a special strategy is used that collects metrics. +If the method is `@Asynchronous`, a special strategy is used that offloads method execution to another thread. +Etc. diff --git a/content/docs/smallrye-fault-tolerance/5/internals/instructions.adoc b/content/docs/smallrye-fault-tolerance/5/internals/instructions.adoc new file mode 100644 index 000000000..2b275e995 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/5/internals/instructions.adoc @@ -0,0 +1,21 @@ += Instructions +:version-root: ../ +include::{version-root}_attributes.adoc[] + +To compile this project and install the artifacts into local Maven repository, run: + +[source,bash] +---- +mvn clean install +---- + +This will also run all the tests and the TCK. + +You can use + +[source,bash] +---- +mvn clean install -DskipTests +---- + +to skip the tests and make the build a lot faster. diff --git a/content/docs/smallrye-fault-tolerance/5/internals/logging.adoc b/content/docs/smallrye-fault-tolerance/5/internals/logging.adoc new file mode 100644 index 000000000..5e64a9c0c --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/5/internals/logging.adoc @@ -0,0 +1,16 @@ += Logging +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Logging in {smallrye-fault-tolerance} is based on https://jboss-logging.github.io/jboss-logging-tools/[JBoss Logging Tools] (see also https://github.com/smallrye/smallrye/wiki/I18N-Messages-and-Logging[SmallRye instructions]). +It's an annotation processor that takes annotated interfaces and generates logger implementations with out of the box support for message codes and internationalization. +We use a project code of `SRFTL`. +Message codes are divided into ranges that are defined in the `message-ranges.txt` file in the root directory. + +In addition to logging, JBoss Logging Tools can also be used for creating exceptions, again, with out of the box support for message codes and internationalization. +Contrary to both documentation sites mentioned above, it _is_ possible to mix logging and exceptions in a single interface, as demonstrated by `CdiLogger`. +However, {smallrye-fault-tolerance} doesn't use this feature yet, with the exception of `CdiLogger.multipleCircuitBreakersWithTheSameName`. +This should be rectified in the future. + +Note that `DEBUG` and `TRACE` logging doesn't need message codes or internationalization, so we usually don't declare logger methods for them, unless the log message requires parameters. +We still declare the logger interface, let it extend `BasicLogger` and use the singleton instance to log plain ``String``s. diff --git a/content/docs/smallrye-fault-tolerance/5/internals/project-structure.adoc b/content/docs/smallrye-fault-tolerance/5/internals/project-structure.adoc new file mode 100644 index 000000000..a3090d00e --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/5/internals/project-structure.adoc @@ -0,0 +1,28 @@ += Project Structure +:version-root: ../ +include::{version-root}_attributes.adoc[] + +* `api`: {smallrye-fault-tolerance} API, additional to the {microprofile-fault-tolerance} API. + Treated as a public API. +* `implementation/core`: Implementation of core fault tolerance strategies. + Independent of the {microprofile-fault-tolerance} API or the {smallrye-fault-tolerance} API. + Treated as private API, no compatibility guaranteed. +* `implementation/fault-tolerance`: Implementation of the {microprofile-fault-tolerance} API and the {smallrye-fault-tolerance} API. + Based on CDI and the `core` module. + The configuration system is based on the `autoconfig` module. +* `implementation/autoconfig/core`: Core classes of the {smallrye-fault-tolerance} configuration system. + Required at runtime. +* `implementation/autoconfig/processor`: An annotation processor that generates implementations of config interfaces. + Only required during {smallrye-fault-tolerance} build. +* `implementation/context-propagation`: Optional integration with MicroProfile Context Propagation. + See xref:integration/context-propagation.adoc[Context Propagation integration]. +* `implementation/tracing-propagation`: Optional integration between MicroProfile Context Propagation and OpenTracing. + See xref:integration/opentracing.adoc[OpenTracing integration]. +* `implementation/vertx`: Optional integration of the Vert.x event loop. + See xref:integration/event-loop.adoc[event loop integration]. +* `testsuite/basic`: Basic test suite. + Some parts of it overlap with the Eclipse MicroProfile Fault Tolerance TCK. +* `testsuite/integration`: Integration test suite. + Includes several Context Propagation and Vert.x tests. +* `testsuite/tck`: Runs the implementation against the Eclipse MicroProfile Fault Tolerance TCK. +* `doc`: This documentation. diff --git a/content/docs/smallrye-fault-tolerance/5/usage/basic.adoc b/content/docs/smallrye-fault-tolerance/5/usage/basic.adoc new file mode 100644 index 000000000..e123d281e --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/5/usage/basic.adoc @@ -0,0 +1,174 @@ += Basic Usage +:version-root: ../ +include::{version-root}_attributes.adoc[] + +== Installation + +As an application developer, you don't have to "install" {smallrye-fault-tolerance} in any way. +Your chosen runtime, such as Quarkus or WildFly, already integrates {smallrye-fault-tolerance} to provide {microprofile-fault-tolerance}. +Please refer to the documentation of your selected runtime to understand how to enable MicroProfile Fault Tolerance. + +If you are a framework developer, interested in integrating {smallrye-fault-tolerance}, please refer to the xref:integration/intro.adoc[integration] chapter. + +== Annotations + +{microprofile-fault-tolerance} is built on top of CDI and interceptors. +This lets you guard method calls with selected fault tolerance strategies simply by annotating a CDI bean class or method with one of the fault tolerance annotations. +For example: + +[source,java] +---- +@ApplicationScoped // <1> +public class MyService { + @Retry // <2> + public String hello() { + ... + } +} +---- + +<1> `@ApplicationScoped` is a bean defining annotation. + It makes the `MyService` class a CDI bean. +<2> `@Retry` is a fault tolerance annotation. + It could also be placed on the class, in which case it would apply to all methods in the class. + +Here, if the `hello` method throws an exception, the invocation will be retried several times, until the method returns a value. + +TIP: CDI beginners often expect that this will work if you create an instance manually: `new MyService()`. +That is not the case. +You have to let the CDI container construct an instance for you and inject it: `@Inject MyService service`. +What you get injected is actually a proxy that implements the additional behaviors. + +The fault tolerance strategies present in {microprofile-fault-tolerance}, together with the corresponding annotations, are: + +* `@Asynchronous`: offload method execution to another thread +* `@Bulkhead`: limit concurrent invocations +* `@CircuitBreaker`: prevent invocations if previous invocations failed too often +* `@Fallback`: provide alternative result in case of a failure +* `@Retry`: retry several times in case of a failure +* `@Timeout`: fail if the invocation takes too long + +We won't go into detail here. +This is all described in the {microprofile-fault-tolerance-url}[{microprofile-fault-tolerance} specification]. + +== Configuration + +The fault tolerance strategies can be configured using annotation attributes, such as: + +[source,java] +---- +@ApplicationScoped +public class MyService { + @Retry(maxRetries = 10, retryOn = IOException.class) // <1> + public String hello() { + ... + } +} +---- + +<1> Retries will only be attempted if the thrown exception was `IOException`. + Other exceptions will be rethrown directly. + Also the maximum number of retry attempts is changed to 10 (the default is 3). + +This is convenient, but changing such configuration requires recompilation. +For that reason, {microprofile-fault-tolerance} also allows configuration using MicroProfile Config. +For example: + +[source,properties] +---- +com.example.MyService/hello/Retry/maxRetries=5 +---- + +Again, this is all described in the {microprofile-fault-tolerance-url}#configuration[{microprofile-fault-tolerance} specification], so we won't go into detail here. + +[[metrics]] +== Metrics + +The {microprofile-fault-tolerance} specification describes how implementations should expose metrics over MicroProfile Metrics. +As usual, this is described in the {microprofile-fault-tolerance-url}#_integration_with_microprofile_metrics[{microprofile-fault-tolerance} specification]. + +=== Micrometer + +In addition to the MicroProfile Metrics support, demanded by the {microprofile-fault-tolerance} specification, {smallrye-fault-tolerance} also provides support for Micrometer. +The set of metrics emitted to Micrometer is the same as the set of metrics emitted to MicroProfile Metrics, using the same metric names and tags. +Metric types are mapped as closely as possible: + +|=== +| Name | MicroProfile Metrics | Micrometer + +| `ft.invocations.total` +| counter +| counter + +| `ft.retry.calls.total` +| counter +| counter + +| `ft.retry.retries.total` +| counter +| counter + +| `ft.timeout.calls.total` +| counter +| counter + +| `ft.timeout.executionDuration` +| histogram +| timer + +| `ft.circuitbreaker.calls.total` +| counter +| counter + +| `ft.circuitbreaker.state.total` +| gauge +| time gauge + +| `ft.circuitbreaker.opened.total` +| counter +| counter + +| `ft.bulkhead.calls.total` +| counter +| counter + +| `ft.bulkhead.executionsRunning` +| gauge +| gauge + +| `ft.bulkhead.executionsWaiting` +| gauge +| gauge + +| `ft.bulkhead.runningDuration` +| histogram +| timer + +| `ft.bulkhead.waitingDuration` +| histogram +| timer + +| `ft.ratelimit.calls.total` +| counter +| counter +|=== + +Note that distribution summaries in Micrometer, including timers, do not emit quantile buckets by default. +Micrometer recommends that libraries should not configure them out of the box, so if you need them, you should use a `MeterFilter`. + +The following implementation makes sure Micrometer emits the same quantile buckets as MicroProfile Metrics for all fault tolerance metrics: + +```java +static final MeterFilter ENABLE_HISTOGRAMS = new MeterFilter() { + @Override + public DistributionStatisticConfig configure(Meter.Id id, DistributionStatisticConfig config) { + if (id.getName().startsWith("ft.")) { + return DistributionStatisticConfig.builder() + .percentiles(0.5, 0.75, 0.95, 0.98, 0.99, 0.999) + .build() + .merge(config); + } + return config; + } +}; +``` diff --git a/content/docs/smallrye-fault-tolerance/5/usage/extra.adoc b/content/docs/smallrye-fault-tolerance/5/usage/extra.adoc new file mode 100644 index 000000000..940379331 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/5/usage/extra.adoc @@ -0,0 +1,835 @@ += Extra Features +:version-root: ../ +include::{version-root}_attributes.adoc[] + +{smallrye-fault-tolerance} provides several features that are not present in the {microprofile-fault-tolerance} specification. +Their public API is present in the `io.smallrye:smallrye-fault-tolerance-api` artifact. + +Note that these features may have experimental status, marked by the `@Experimental` annotation. + +== Circuit Breaker Maintenance + +It is sometimes useful to see the circuit breaker status from within the application, or reset it to the initial state. +This is possible in two steps: + +. Give the circuit breaker a name by annotating the guarded method with `@CircuitBreakerName`: ++ +[source,java] +---- +@ApplicationScoped +public class MyService { + @CircuitBreaker + @CircuitBreakerName("hello-cb") // <1> + public String hello() { + ... + } +} +---- ++ +<1> The circuit breaker guarding the `MyService.hello` method is given a name `hello-cb`. +. Inject `CircuitBreakerMaintenance` and call its methods: ++ +[source,java] +---- +@ApplicationScoped +public class Example { + @Inject + CircuitBreakerMaintenance maintenance; + + public void test() { + System.out.println("Circuit breaker state: " + + maintenance.currentState("hello-cb")); // <1> + maintenance.resetAll(); // <2> + } +} +---- ++ +<1> Obtains current circuit breaker state. +<2> Resets all circuit breakers to the initial state. + +The `CircuitBreakerMaintenance` interface provides 4 methods: + +. `currentState(name)`: returns current state of given circuit breaker. + The return type `CircuitBreakerState` is an `enum` with 3 values: `CLOSED`, `OPEN`, `HALF_OPEN`. +- `onStateChange(name, callback)`: registers a callback that will be called when given circuit breaker changes state. +. `reset(name)`: resets given circuit breaker to the initial state. +. `resetAll()`: resets all circuit breakers in the application to the initial state. + +See the javadoc of those methods for more information. + +[[async-nonblocking]] +== @AsynchronousNonBlocking + +In addition to the {microprofile-fault-tolerance} `@Asynchronous` annotation, {smallrye-fault-tolerance} offers an annotation for asynchronous non-blocking processing: `@AsynchronousNonBlocking`. + +This annotation may only be placed on methods that return `CompletionStage`, because the `Future` type can't really be used for non-blocking processing. +It may also be placed on a class, in which case all business methods must return `CompletionStage`. + +As opposed to `@Asynchronous`, the `@AsynchronousNonBlocking` annotation does _not_ offload the method execution to another thread. +The method is allowed to proceed on the original thread, assuming that it returns quickly and all processing it does is performed in a non-blocking way. + +|=== +|Annotation |Method return type |Execution + +|`@Asynchronous` +|`Future` or `CompletionStage` +|offloaded to a thread pool + +|`@AsynchronousNonBlocking` +|`CompletionStage` +|proceeds on the original thread +|=== + +=== When to Use @Asynchronous + +Use `@Asynchronous` if the method has blocking logic, but you don’t want to block the caller thread. +For example: + +[source,java] +---- +@ApplicationScoped +public class MyService { + @Retry + @Asynchronous // <1> + CompletionStage hello() { + ... + } +} +---- + +<1> Using the `@Asynchronous` annotation, because the method blocks, and it is necessary to offload its execution to another thread. + +The thread pool that is used for offloading method calls is provided by the runtime that integrates {smallrye-fault-tolerance}. + +=== When to Use @AsynchronousNonBlocking + +Use `@AsynchronousNonBlocking` if the method _does not_ have blocking logic, and you want the execution to stay on the caller thread. +For example: + +[source,java] +---- +@ApplicationScoped +public class MyService { + @Retry + @AsynchronousNonBlocking // <1> + CompletionStage hello() { + ... + } +} +---- + +<1> Using the `@AsynchronousNonBlocking` annotation, because the method does not block and offloading execution to another thread is not necessary. + +If the guarded method uses `@Retry` and some delay between retries is configured, only the initial execution is guaranteed to occur on the original thread. +Subsequent attempts may be offloaded to an extra thread, so that the original thread is not blocked on the delay. + +If the guarded method uses `@Bulkhead`, the execution is _not_ guaranteed to occur on the original thread. +If the execution has to wait in the bulkhead queue, it may later end up on a different thread. + +If the original thread is an event loop thread and event loop integration is enabled, then the event loop is always used to execute the guarded method. +In such case, all retry attempts and queued bulkhead executions are guaranteed to happen on the original thread. + +=== How to Combine Them + +When these annotations are combined, an annotation on a method has priority over an annotation on a class. +For example: + +[source,java] +---- +@ApplicationScoped +@AsynchronousNonBlocking +public class MyService { + @Retry + CompletionStage hello() { // <1> + ... + } + + @Retry + @Asynchronous + CompletionStage helloBlocking() { // <2> + ... + } +} +---- + +<1> Treated as `@AsynchronousNonBlocking`, based on the class annotation. +<2> Treated as `@Asynchronous`, the method annotation has priority over the class annotation. + +It is an error to put both `@Asynchronous` and `@AsynchronousNonBlocking` on the same program element. + +=== Recommendation + +Use the <>. +In this mode, methods returning `CompletionStage` are automatically treated as if they were `@AsynchronousNonBlocking`. +Use `@Asynchronous` to mark blocking methods for thread offload. + +If you can't use the non-compatible mode, use `@AsynchronousNonBlocking` or `@Asynchronous` to mark all asynchronous methods. + +IMPORTANT: In previous releases, {smallrye-fault-tolerance} recommended to use the `@Blocking` and `@NonBlocking` annotations. +Using these annotations *for fault tolerance purposes* is now deprecated. +They are still supported, but at some point, {smallrye-fault-tolerance} will stop recognizing them. + +We also recommend avoiding `@Asynchronous` methods that return `Future`, because the only way to obtain the future value is blocking. + +[[async-types]] +== Additional Asynchronous Types + +{microprofile-fault-tolerance} supports asynchronous fault tolerance for methods that return `CompletionStage`. +(The `Future` type is not truly asynchronous, so we won't take it into account here.) +{smallrye-fault-tolerance} adds support for additional asynchronous types: + +* Mutiny: `Uni` +* RxJava: `Single`, `Maybe`, `Completable` + +These types are treated just like `CompletionStage`, so everything that works for `CompletionStage` works for these types as well. +Stream-like types (`Multi`, `Observable`, `Flowable`) are not supported, because their semantics can't be easily expressed in terms of `CompletionStage`. + +For example: + +[source,java] +---- +@ApplicationScoped +public class MyService { + @Retry + @AsynchronousNonBlocking // <1> + Uni hello() { // <2> + ... + } +} +---- + +<1> Using the `@AsynchronousNonBlocking` annotation described in <>, because the method doesn't block and offloading execution to another thread is not necessary. +<2> Returning the `Uni` type from Mutiny. +This shows that whatever works for `CompletionStage` also works for the other async types. + +The implementation internally converts the async types to a `CompletionStage` and back. +This means that to be able to use any particular asynchronous type, the corresponding converter must be present. +{smallrye-fault-tolerance} provides support libraries for popular asynchronous types, and these support libraries include the corresponding converters. + +It is possible that the runtime you use already provides the correct integration. +Otherwise, add a dependency to your application: + +* https://smallrye.io/smallrye-mutiny/[Mutiny]: `io.smallrye:smallrye-fault-tolerance-mutiny` +* https://github.com/ReactiveX/RxJava/tree/3.x[RxJava 3]: `io.smallrye:smallrye-fault-tolerance-rxjava3` + +.Quarkus +**** +In Quarkus, the Mutiny support library is present by default. +You can use fault tolerance on methods that return `Uni` out of the box. +**** + +== Backoff Strategies for `@Retry` + +When retrying failed operations, it is often useful to make a delay between retry attempts. +This delay is also called "backoff". +The `@Retry` annotation in {microprofile-fault-tolerance} supports a single backoff strategy: constant. +That is, the delay between all retry attempts is identical (with the exception of a random jitter). + +{smallrye-fault-tolerance} offers 3 annotations to specify a different backoff strategy: + +* `@ExponentialBackoff` +* `@FibonacciBackoff` +* `@CustomBackoff` + +One of these annotations may be present on any program element (method or class) that also has the `@Retry` annotation. +For example: + +[source,java] +---- +package com.example; + +@ApplicationScoped +public class MyService { + @Retry + @ExponentialBackoff + public void hello() { + ... + } +} +---- + +It is an error to add a backoff annotation to a program element that doesn't have `@Retry` (e.g. add `@Retry` on a class and `@ExponentialBackoff` on a method). +It is also an error to add more than one of these annotations to the same program element. + +When any one of these annotations is present, it modifies the behavior specified by the `@Retry` annotation. +The new behavior is as follows: + +For `@ExponentialBackoff`, the delays between retry attempts grow exponentially, using a defined `factor`. +By default, the `factor` is 2, so each delay is 2 * the previous delay. +For example, if the initial delay (specified by `@Retry`) is 1 second, then the second delay is 2 seconds, third delay is 4 seconds, fourth delay is 8 seconds etc. +It is possible to define a `maxDelay`, so that this growth has a limit. + +For `@FibonacciBackoff`, the delays between retry attempts grow per the Fibonacci sequence. +For example, if the initial delay (specified by `@Retry`) is 1 second, then the second delay is 2 seconds, third delay is 3 seconds, fourth delay is 5 seconds etc. +It is possible to define a `maxDelay`, so that this growth has a limit. + +Both `@ExponentialBackoff` and `@FibonacciBackoff` also apply jitter, exactly like plain `@Retry`. + +Also, since `@Retry` has a default `maxDuration` of 3 minutes and default `maxRetries` of 3, both `@ExponentialBackoff` and `@FibonacciBackoff` define a `maxDelay` of 1 minute. +If we redefine `maxRetries` to a much higher value, and the guarded method keeps failing, the delay would eventually become higher than 1 minute. +In that case, it will be limited to 1 minute. +Of course, `maxDelay` can be configured. +If set to `0`, there's no limit, and the delays will grow without bounds. + +For `@CustomBackoff`, computing the delays between retry attempts is delegated to a specified implementation of `CustomBackoffStrategy`. +This is an advanced option. + +For more information about these backoff strategies, see the javadoc of the annotations. + +=== Configuration + +These annotations may be configured using the same mechanism as {microprofile-fault-tolerance} annotations. +For example, to modify the `factor` of the `@ExponentialBackoff` annotation above, you can use: + +[source,properties] +---- +com.example.MyService/hello/ExponentialBackoff/factor=3 +---- + +=== Metrics + +These annotations do not have any special metrics. +All `@Retry` metrics are still present and reflect the altered behavior. + +[[noncompatible-mode]] +== Non-compatible Mode + +{smallrye-fault-tolerance} offers a mode where certain features are improved beyond specification, as described below. +This mode is *not compatible* with the {microprofile-fault-tolerance} specification (and doesn't necessarily pass the entire TCK). + +This mode is disabled by default. +To enable, set the configuration property `smallrye.faulttolerance.mp-compatibility` to `false`. + +.Quarkus +**** +In Quarkus, the non-compatible mode is enabled by default. +To restore compatibility, add the following to your `application.properties`: + +[source,properties] +---- +smallrye.faulttolerance.mp-compatibility=true +---- + +Note that the non-compatible mode is available since {smallrye-fault-tolerance} 5.2.0 and Quarkus 2.1.0.Final. +Previous versions are always compatible. +**** + +[[method-asynchrony]] +=== Determining Asynchrony from Method Signature + +In the non-compatible mode, method asynchrony is determined solely from its signature. +That is, methods that + +* have some fault tolerance annotation (such as `@Retry`), +* return `CompletionStage` (or some other <>), + +always have asynchronous fault tolerance applied. + +For example: + +[source,java] +---- +@ApplicationScoped +public class MyService { + @Retry + CompletionStage hello() { // <1> + ... + } + + @Retry + Uni helloMutiny() { // <2> + ... + } + + @Retry + @Asynchronous + CompletionStage helloBlocking() { // <3> + ... + } +} +---- + +<1> Executed on the original thread, because the method returns `CompletionStage`. +It is as if the method was annotated `@AsynchronousNonBlocking`. +<2> Executed on the original thread, because the method returns an <>. +It is as if the method was annotated `@AsynchronousNonBlocking`. +<3> The explicit `@Asynchronous` annotation is honored. +The method is executed on a thread pool. + +Note that the existing annotations still work without a change, both in compatible and non-compatible mode. +That is, if a method (or class) is annotated `@Asynchronous`, execution will be offloaded to a thread pool. +If a method (or class) is annotated `@AsynchronousNonBlocking`, execution will happen on the original thread. + +Also note that this doesn't affect methods returning `Future`. +You still have to annotate them `@Asynchronous` to make sure they are executed on a thread pool and are guarded properly. +As mentioned in the <> section, we discourage using these methods, because the only way to obtain the future value is blocking. + +=== Inspecting Exception Cause Chains + +The `@CircuitBreaker`, `@Fallback` and `@Retry` annotations can be used to specify that certain exceptions should be treated as failures and others as successes. +This is limited to inspecting the actual exception that was thrown. +However, in many usecases, exceptions are wrapped and the exception the user wants to decide on is only present in the cause chain. + +In the non-compatible mode, if the actual thrown exception isn't known failure or known success, {smallrye-fault-tolerance} inspects the cause chain. +To be specific, in case a `@Fallback` method throws an exception, the decision process is: + +1. if the exception is assignable to one of the `skipOn` exceptions, fallback is skipped and the exception is rethrown; +2. otherwise, if the exception is assignable to one of the `applyOn` exceptions, fallback is applied; +3. otherwise, if the cause chain of the exception contains an exception assignable to one of the `skipOn` exceptions, fallback is skipped and the exception is rethrown; +4. otherwise, if the cause chain of the exception contains an exception assignable to one of the `applyOn` exceptions, fallback is applied; +5. otherwise, the exception is rethrown. + +For example, say we have this method: + +[source, java] +---- +@Fallback(fallbackMethod = "fallback", + skipOn = ExpectedOutcomeException.class, + applyOn = IOException.class) +public Result doSomething() { + ... +} + +public Result fallback() { + ... +} +---- + +If `doSomething` throws an `ExpectedOutcomeException`, fallback is skipped and the exception is thrown. +If `doSomething` throws an `IOException`, fallback is applied. +If `doSomething` throws a `WrapperException` whose cause is `ExpectedOutcomeException`, fallback is skipped and the exception is thrown. +If `doSomething` throws a `WrapperException` whose cause is `IOException`, fallback is applied. + +Comparing with the `@Fallback` specification, {smallrye-fault-tolerance} inserts 2 more steps into the decision process that inspect the cause chain. +Note that these steps are executed if and only if the thrown exception matches neither `skipOn` nor `applyOn`. +If the thrown exception matches either of them, the cause chain is not inspected at all. + +Similar behavior applies to `@CircuitBreaker` and `@Retry`. +All 3 annotations follow the same principle: exceptions considered success have priority over those considered failure. + +|=== +| Fault Tolerance annotation | Exception is first tested against | and then against + +| `@Fallback` +| `skipOn` +| `applyOn` + +| `@CircuitBreaker` +| `skipOn` +| `failOn` + +| `@Retry` +| `abortOn` +| `retryOn` +|=== + +=== Fallback Method with Exception Parameter + +In the non-compatible mode, {smallrye-fault-tolerance} supports access to the causing exception in a `@Fallback` method. + +A fallback method, as defind by the {microprofile-fault-tolerance} specification, must have the same parameters as the guarded method. +{smallrye-fault-tolerance} permits defining one additional parameter, at the end of the parameter list, which must be of an exception type. +If such parameter is defined, the exception that caused the fallback will be supplied in it. + +For example: + +[source,java] +---- +@ApplicationScoped +public class MyService { + @Fallback(fallbackMethod = "fallback") + public String doSomething(String param) { + ... + } + + public String fallback(String param, IllegalArgumentException cause) { // <1> + ... + } +} +---- + +<1> The fallback method matches the guarded method signature, except for one additional parameter at the end. + +All rules of {microprofile-fault-tolerance} specification related to looking up fallback methods still apply. +That is, the return types must match, the parameter types must match (with this one exception), etc. + +If the thrown exception is not assignable to the exception parameter type, it is rethrown as if no fallback was declared. +In the previous example, if `IllegalStateException` was thrown, the fallback method would not be called, as `IllegalStateException` is not a subtype of `IllegalArgumentException`. + +If the guarded method has a vararg parameter and you want to declare a fallback method with an exception parameter, simply replace the vararg syntax with an array type: + +[source,java] +---- +@ApplicationScoped +public class MyService { + @Fallback(fallbackMethod = "fallback") + public String doSomething(String... params) { + ... + } + + public String fallback(String[] params, IllegalArgumentException cause) { + ... + } +} +---- + +==== Multiple Fallback Methods with Exception Parameter + +It is possible to declare multiple overloads of the fallback method, each having different type of the exception parameter: + +[source,java] +---- +@ApplicationScoped +public class MyService { + @Fallback(fallbackMethod = "fallback") + public String doSomething(String param) { + ... + } + + public String fallback(String param, IllegalArgumentException cause) { + ... + } + + public String fallback(String param, RuntimeException cause) { + ... + } +} +---- + +In that case, which fallback method is called depends on the type of thrown exception. +The method that declares a most-specific supertype of the actual exception is selected. + +In the previous example, if `IllegalArgumentException` was thrown by `doSomething`, the first fallback method would be called. +If `IllegalStateException` was thrown, the second fallback method would be called. + +If the thrown exception is not assignable to the exception parameter type of any fallback method, it is rethrown as if no fallback was declared. + +==== Fallback Methods with and without Exception Parameter + +It is possible to declare the fallback method with and without an exception parameter at the same time: + +[source,java] +---- +@ApplicationScoped +public class MyService { + @Fallback(fallbackMethod = "fallback") + public String doSomething(String param) { + ... + } + + public String fallback(String param, IllegalArgumentException cause) { + ... + } + + public String fallback(String param, RuntimeException cause) { + ... + } + + public String fallback(String param) { + ... + } +} +---- + +The fallback methods with an exception parameter have precedence. +The fallback method without an exception parameter is only called if the thrown exception is not assignable to any declared exception parameter. + +==== Interactions with `applyOn` / `skipOn` + +The presence or absence of a fallback method with specific exception parameter may seem related to the usage of `applyOn` / `skipOn` on the `@Fallback` annotation, but in fact, it is not. +These features are completely independent. + +Simply put, the `applyOn` / `skipOn` configuration is always evaluated first. +A fallback method is only selected and invoked when this configuration indicates that a fallback should apply. + +If `@Fallback` is configured to skip `IllegalStateException` and `IllegalStateException` is thrown, no fallback method is invoked. +That applies even if a fallback method with a matching exception parameter exists. + +For example: + +[source,java] +---- +@ApplicationScoped +public class MyService { + @Fallback(fallbackMethod = "fallback", skipOn = IllegalStateException.class) + public String doSomething(String param) { + ... + } + + public String fallback(String param, IllegalArgumentException cause) { + ... + } + + public String fallback(String param, RuntimeException cause) { + ... + } + + public String fallback(String param) { + ... + } +} +---- + +In this case: + +- if `doSomething` throws `IllegalArgumentException`, the first fallback method is called; +- if `doSomething` throws `IllegalStateException`, no fallback method is called, because this exception type is skipped; +- if `doSomething` throws any other `RuntimeException`, the second fallback method is called; +- if `doSomething` throws any other exception, the last fallback method is called. + +[[kotlin-suspend-functions]] +== Kotlin `suspend` Functions + +{smallrye-fault-tolerance} includes support for Kotlin suspending functions. +They are treated as <>, even though the internal implementation is more complex than support for Mutiny or RxJava. + +For example: + +[source,kotlin] +---- +@ApplicationScoped +open class MyService { + @Retry(maxRetries = 2) + @Fallback(fallbackMethod = "helloFallback") + open suspend fun hello(): String { // <1> + delay(100) + throw IllegalArgumentException() + } + + private suspend fun helloFallback(): String { // <2> + delay(100) + return "hello" + } +} +---- + +<1> As a suspending function, this method can only be called from another suspending function. +It will be guarded by the retry and fallback strategies, as defined using the annotations. +<2> Similarly to fallback methods in Java, fallback methods in Kotlin must have the same signature as the guarded method. +Since the guarded method is suspending, the fallback method must be suspending. + +As mentioned above, suspending functions are treated as async types. +This means that for asynchronous fault tolerance to work correctly on suspending functions, they must be determined to be asynchronous. +That happens automatically in the <>, based on the method signature, but if you use strictly compatible mode, the `@Asynchronous` or `@AsynchronousNonBlocking` annotation must be present. +It is expected that most users will use the Kotlin support in the non-compatible mode, so the example above does not include any such annotation. + +To be able to use this, a support library must be present. +It is possible that the runtime you use already provides the correct integration. +Otherwise, add a dependency to your application: `io.smallrye:smallrye-fault-tolerance-kotlin`. + +.Quarkus +**** +In Quarkus, the Kotlin support library is present by default, if you use the Quarkus Kotlin support. +You can declare fault tolerance annotations on suspending methods out of the box. +**** + +=== Programmatic API + +Suspending functions are currently only supported in the declarative, annotation-based API, as shown in the example above. +The xref:usage/programmatic-api.adoc[Programmatic API] of {smallrye-fault-tolerance} does not support suspending functions, but other than that, it can of course be used from Kotlin through its Java interop. + +== Reusable, Preconfigured Fault Tolerance + +The declarative, annotation-based API of {microprofile-fault-tolerance} doesn't allow sharing configuration of fault tolerance strategies across multiple classes. +In a single class, the configuration may be shared across all methods by putting the annotations on the class instead of individual methods, but even then, _stateful_ fault tolerance strategies are not shared. +Each method has its own bulkhead and/or circuit breaker, which is often not what you want. + +The xref:usage/programmatic-api.adoc[programmatic API] of {smallrye-fault-tolerance} allows using a single `FaultTolerance` object to guard multiple disparate actions, which allows reuse and state sharing. +It is possible to use a programmatically constructed `FaultTolerance` object declaratively, using the `@ApplyFaultTolerance` annotation. + +To be able to do that, we need a bean of type `FaultTolerance` with the `@Identifier` qualifier: + +[source,java] +---- +@ApplicationScoped +public class PreconfiguredFaultTolerance { + @Produces + @Identifier("my-fault-tolerance") + public static final FaultTolerance FT = FaultTolerance.create() + .withRetry().maxRetries(2).done() + .withFallback().handler(() -> "fallback").done() + .build(); +} +---- + +See the xref:usage/programmatic-api.adoc[programmatic API] documentation for more information about creating the `FaultTolerance` instance. + +It is customary to create the bean by declaring a `static` producer field, just like in the previous example. + +Once we have that, we can apply `my-fault-tolerance` to synchronous methods that return `String`: + +[source,java] +---- +@ApplicationScoped +public class MyService { + @ApplyFaultTolerance("my-fault-tolerance") + public String doSomething() { + ... + } +} +---- + +It is also possible to create a bean of type `FaultTolerance` and apply it to synchronous methods that return many different types. +Note that this effectively precludes defining a useful fallback, because fallback can only be defined when the value type is known. + +It is also possible to define a bean of type `FaultTolerance>` and apply it to asynchronous methods that return `CompletionStage`. +Likewise, it is possible to do this for <>. + +Note that you can't define a synchronous `FaultTolerance` object and apply it to any asynchronous method. +Similarly, you can't define an asynchronous `FaultTolerance>` and apply it to a synchronous method or an asynchronous method with different <>. +This limitation will be lifted in the future. + +== Rate Limit + +{smallrye-fault-tolerance} includes an additional fault tolerance strategy, not prescribed by the {microprofile-fault-tolerance} specification: _rate limit_. + +Rate limit enforces a maximum number of permitted invocations in a time window of some length. +For example, with a rate limit, one can make sure that a method may only be called 50 times per minute. +Invocations that would exceed the limit are rejected with an exception of type `RateLimitException`. + +Additionally, it is possible to define minimum spacing between invocations. +For example, with minimum spacing of 1 second, if a second invocation happens 500 millis after the first, it is rejected even if the limit wouldn't be exceeded yet. + +Rate limit is superficially similar to a bulkhead (concurrency limit), but is in fact quite different. +Bulkhead limits the number of executions happening concurrently at any point in time. +Rate limit limits the number of executions in a time window of some length, without considering concurrency. + +=== Rate Limit Usage + +A method or a class can be annotated with `@RateLimit`, which means the method or the methods in the class will apply the rate limit strategy. + +The following annotation members control the rate limit behavior: + +- `value`: the limit of maximum invocations to be permitted in the time window; +- `window` and `windowUnit`: the length of the time window; +- `minSpacing` and `minSpacingUnit`: the minimum spacing between two consecutive invocations; +- `type`: the type of time windows used for rate limiting, described below. + +The previous example with 50 maximum invocations per minute and minimum spacing of 1 second would look like this: + +[source,java] +---- +@RateLimit(value = 50, + window = 1, windowUnit = ChronoUnit.MINUTES, + minSpacing = 1, minSpacingUnit = ChronoUnit.SECONDS) +public void doSomething() { + ... +} +---- + +=== Time Window Type + +{smallrye-fault-tolerance} supports three types of a time window used for rate limiting: _fixed_, _rolling_ and _smooth_. + +Fixed time windows are a result of dividing time into non-overlapping intervals of given length. +The invocation limit is enforced for each interval independently. +This means that short bursts of invocations occuring near the time window boundaries may temporarily exceed the configured rate limit. +This kind of rate limiting is also called _fixed window_ rate limiting. + +Rolling time windows enforce the limit continuously, instead of dividing time into independent intervals. +The invocation limit is enforced for all possible time intervals of given length, regardless of overlap. +This is more precise, but requires more memory and may be slower. +This kind of rate limiting is also called _sliding log_ rate limiting. + +Smooth time windows enforce a uniform distribution of invocations under a rate calculated from given time window length and given limit. +If recent rate of invocations is under the limit, a subsequent burst of invocations is allowed during a shorter time span, but the calculated rate is never exceeded. +This kind of rate limiting is also called _token bucket_ or _leaky bucket (as a meter)_ rate limiting, with the additional property that all work units are considered to have the same size. + +The type of time window used for rate limiting is configured using the `type` annotation member: + +[source,java] +---- +@RateLimit(value = 50, + window = 1, windowUnit = ChronoUnit.MINUTES, + minSpacing = 1, minSpacingUnit = ChronoUnit.SECONDS, + type = RateLimitType.ROLLING) +public void doSomething() { + ... +} +---- + + +=== Lifecycle + +Rate limit needs to maintain some state between invocations, depending on the time window type. +It may be the number of recent invocations or the time stamp of last invocation. +This state is a singleton, irrespective of the lifecycle of the bean that uses the `@RateLimit` annotation. + +More specifically, the rate limit state is uniquely identified by the combination of the bean class (`java.lang.Class`) and the method object (`java.lang.reflect.Method`) representing the guarded method. + +For example, if there’s a guarded method `doWork` on a bean which is `@RequestScoped`, each request will have its own instance of the bean, but all invocations of `doWork` will share the same rate limit state. + +=== Interactions with Other Annotations + +The `@RateLimit` annotation can be used together with all other fault tolerance annotations. +If a method would hypothetically declare all fault tolerance annotations, the fault tolerance strategies would be nested like this: + +[source] +---- +Fallback( + Retry( + CircuitBreaker( + RateLimit( + Timeout( + Bulkhead( + ... the guarded method ... + ) + ) + ) + ) + ) +) +---- + +If `@Fallback` is used with `@RateLimit`, the fallback method or handler may be invoked if a `RateLimitException` is thrown, depending on the fallback configuration. + +If `@Retry` is used with `@RateLimit`, each retry attempt is processed by the rate limit as an independent invocation. +If `RateLimitException` is thrown, the execution may be retried, depending on how retry is configured. + +If `@CircuitBreaker` is used with `@RateLimit`, the circuit breaker is checked before enforcing the rate limit. +If rate limiting results in `RateLimitException`, this may be counted as a failure, depending on how the circuit breaker is configured. + +=== Configuration + +The configuration system of {microprofile-fault-tolerance} applies to `@RateLimit` without a change. + +For example, assume the following class: + +[source,java] +---- +package com.example; + +@ApplicationScoped +public class MyService { + @RateLimit(value = 50, + window = 1, windowUnit = ChronoUnit.MINUTES, + minSpacing = 1, minSpacingUnit = ChronoUnit.SECONDS) + public void doSomething() { + ... + } +} +---- + +The rate limit configured for the `doSomething` method may be reconfigured like this: + +[source,properties] +---- +com.example.MyService/doSomething/RateLimit/value=150 +---- + +=== Metrics + +Rate limit exposes metrics to MicroProfile Metrics, following other {microprofile-fault-tolerance} metrics. + +[cols="1,5"] +|=== +| Name | `ft.ratelimit.calls.total` +| Type | `Counter` +| Unit | None +| Description | The number of times the rate limit logic was run. This will usually be once per method call, but may be zero times if the circuit breaker prevented execution or more than once if the method call is retried. +| Tags +a| * `method` - the fully qualified method name +* `rateLimitResult` = `[permitted\|rejected]` - whether the rate limit permitted the method call +|=== + +Micrometer is of course also supported, as described in xref:usage/basic.adoc#metrics[Metrics]. diff --git a/content/docs/smallrye-fault-tolerance/5/usage/programmatic-api.adoc b/content/docs/smallrye-fault-tolerance/5/usage/programmatic-api.adoc new file mode 100644 index 000000000..c73dde1c5 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/5/usage/programmatic-api.adoc @@ -0,0 +1,340 @@ += Programmatic API +:version-root: ../ +include::{version-root}_attributes.adoc[] + +In addition to the declarative, annotation-based API of {microprofile-fault-tolerance}, {smallrye-fault-tolerance} also offers a programmatic API for advanced scenarios. +This API is present in the `io.smallrye:smallrye-fault-tolerance-api` artifact, just like all other additional APIs {smallrye-fault-tolerance} provides. + +== Installation + +If you use {smallrye-fault-tolerance} as part of a runtime that implements {microprofile-fault-tolerance}, you don't have to do anything. +The programmatic API is ready to use. +In this documentation, we'll call this the _CDI implementation_ of the programmatic API, because it's integrated with the {microprofile-fault-tolerance} implementation, which is naturally based on CDI. + +.Quarkus +**** +In Quarkus, the {smallrye-fault-tolerance} API is brought in automatically, as a transitive dependency of the Quarkus extension for {smallrye-fault-tolerance}. +That is, you don't need to do anything to be able to use the programmatic API. +**** + +.WildFly +**** +In WildFly, the {smallrye-fault-tolerance} API is not readily available to deployments. +If you want to use it, you need to add a module dependency to the deployment using `jboss-deployment-structure.xml`. + +Note that at the time of this writing, the {smallrye-fault-tolerance} module in WildFly is considered private. +If you do add a module dependency on it, to be able to use the {smallrye-fault-tolerance} API, you may be stepping out of the WildFly support scope. +**** + +In addition to the CDI implementation, {smallrye-fault-tolerance} also offers a _standalone implementation_ that is meant to be used outside of any runtime. +This implementation does not need CDI or anything else. +If you want to use {smallrye-fault-tolerance} in a standalone fashion, just add a dependency on `io.smallrye:smallrye-fault-tolerance-standalone`. +The API is brought in transitively. + +== Usage + +The entrypoint to the programmatic API is the `FaultTolerance` interface. + +This interface represents a configured set of fault tolerance strategies. +Their configuration, order of application and behavior in general corresponds to the declarative API, so if you know that, you'll feel right at home. +If not, the javadoc has all the information you need (though it often points to the annotation-based API for more information). + +To create an instance of `FaultTolerance`, you can use the `create` and `createAsync` static methods. +They return a builder which has to be used to add and configure all the fault tolerance strategies that should apply. +There is no external configuration, so all configuration properties have to be set explicitly, using the builder methods. +If you don't set a configuration property, it will default to the same value the annotation-based API uses. + +.Disabling Fault Tolerance +**** +There's one exception to the "no external configuration" rule. + +The CDI implementation looks for the well-known `MP_Fault_Tolerance_NonFallback_Enabled` configuration property in MicroProfile Config. +The standalone implementation looks for a system property with the same name. + +If such property exists and is set to `false`, only the fallback and thread offload fault tolerance strategies will be applied. +Everything else will be ignored. + +Note that this is somewhat different to the declarative, annotation-based API, where only fallback is retained and the `@Asynchronous` strategy is skipped as well. +Since this significantly changes execution semantics, the programmatic API will apply thread offload even if fault tolerance is disabled. + +Similarly to the declarative API, implementations of the programmatic API also read this property only once, when the `FaultTolerance` API is first used. +It is _not_ read again later. +**** + +Let's take a look at a simple example: + +[source,java] +---- +public class MyService { + private static final FaultTolerance guarded = FaultTolerance.create() // <1> + .withFallback().handler(() -> "fallback").done() // <2> + .build(); // <3> + + public String hello() throws Exception { + return guarded.call(() -> externalService.hello()); // <4> + } +} +---- + +<1> The `create` invocation typically has to include an explicit type argument (``). + The `FaultTolerance` interface has a type parameter which should be set to the return type of the guarded actions. + This is required to be able to guarantee that a fallback value is of the same type. +<2> The fallback handler may be a simple supplier of the fallback value, or a function that takes the exception and transforms it to the fallback value. + Here, we use the simpler option. + Note that we don't set any other configuration options. + This means that the default set of exceptions is used to determine when fallback should apply. +<3> The `build` method returns a `FaultTolerance` instance, which can later be used to guard arbitrary `String`-returning actions. +<4> Here, we call `externalService.hello()` and guard the call with the previously configured set of fault tolerance strategies. + (In this case, just fallback.) + The `call` method uses the `Callable` type to represent the called action. + Similar methods exist that accept a `Supplier` (`get`) or `Runnable` (`run`). + +The previous example shows how to apply fault tolerance to synchronous actions. +{smallrye-fault-tolerance} naturally also supports guarding asynchronous actions, using the `CompletionStage` type. +Unlike the declarative API, the programmatic API doesn't support asynchronous actions that return the `Future` type. + +[source,java] +---- +public class MyService { + private static final FaultTolerance> guarded = FaultTolerance.createAsync() // <1> + .withBulkhead().done() // <2> + .withThreadOffload(true) // <3> + .build(); + + public CompletionStage hello() throws Exception { + return guarded.call(() -> externalService.hello()); // <4> + } +} +---- + +<1> The `createAsync` method takes a type parameter of `String` and returns a builder for fault tolerance of type `CompletionStage`. +<2> Here, we add a bulkhead. + Since we don't configure any property, default values are used. + That is, at most 10 concurrent executions are permitted, and 10 more executions may be waiting in a queue. +<3> And here, we add a thread offload. + This is only possible for asynchronous actions, and corresponds to the `@Asynchronous` annotation from {microprofile-fault-tolerance}. +<4> Note that here, unlike the previous example, the `externalService.hello()` method is assumed to return `CompletionStage`. + +Asynchronous actions may be blocking or non-blocking. +In the example above, we assume the `externalService.hello()` call is blocking, so we set thread offload to `true`. +{smallrye-fault-tolerance} will automatically move the actual execution of the action to another thread. + +If we didn't configure `withThreadOffload`, however, the execution would continue on the original thread. +This is often desired for non-blocking actions, which are very common in modern reactive architectures. + +Also note that in this example, we configured multiple fault tolerance strategies: bulkhead and thread offload. +When that happens, the fault tolerance strategies are ordered according to the MicroProfile Fault Tolerance specification, just like in the declarative API. +Order of all the `with*` method invocations doesn’t matter. + +=== Synchronous vs. Asynchronous + +What's the difference between `FaultTolerance.>create()` and `FaultTolerance.createAsync()`? +Both may be used to guard an action that returns `CompletionStage`, correct? + +Well, yes and no. + +The synchronous variant (created using `create()`) will only guard the synchronous part of the action -- the part that ends by returning the `CompletionStage` instance. +It will _not_ guard the asynchronous behavior. + +For example, if an action returns a `CompletionStage` object, synchronous fault tolerance will consider that action successfully finished. +If that `CompletionStage` later completes with an exception, synchronous fault tolerance will never know. +What's more, the fact that this action has already "finished" means that the action will also leave the bulkhead, so concurrency limiting will not work properly. + +The asynchronous variant (created using `createAsync()`), on the other hand, will not treat the action as finished until the `CompletionStage` actually completes. +That is, the asynchronous action will only leave the bulkhead when it's complete, so concurrency limiting works as expected. +And if the `CompletionStage` completes exceptionally, asynchronous fault tolerance will treat that as a failure and react accordingly. + +To summarize, if you need to guard asynchronous actions, blocking or non-blocking, always use `createAsync`. + +=== Single-Action Usage + +The `FaultTolerance` API is general and permits guarding multiple different actions using the same set of fault tolerance strategies. +Often, that isn't necessary and we need to guard just a single action, altough possibly several times. + +For such use case, the `FaultTolerance` API provides shortcuts that work with the `Callable`, `Supplier` and `Runnable` types. + +First off, a `FaultTolerance` instance may be adapted to a `Callable`, `Supplier` or `Runnable` using the `adapt*` methods. +For example: + +[source,java] +---- +public class MyService { + private static final FaultTolerance guard = FaultTolerance.create() + .withTimeout().duration(5, ChronoUnit.SECONDS).done() + .build(); // <1> + + public String hello() throws Exception { + Callable callable = guard.adaptCallable(() -> externalService.hello()); // <2> + + return callable.call(); // <3> + } + +} +---- + +<1> Create a `FaultTolerance` object that can guard arbitrary `String`-returning actions. +<2> Adapt the general `FaultTolerance` instance to a `Callable` that guards the `externalService.hello()` invocation. + Similar methods exist that accept and return a `Supplier` (`adaptSupplier`) and `Runnable` (`adaptRunnable`). +<3> You can do whatever you wish with the adapted `Callable`. + Here, we just call it once, which isn't very interesting, but it could possibly be called multiple times, passed to other methods etc. + +This style of usage still creates a `FaultTolerance` instance first. +If that is not necessary, you can create a `Callable`, `Supplier` or `Runnable` directly: + +[source, java] +---- +public class MyService { + private static final Callable guard = FaultTolerance.createCallable(() -> externalService.hello()) // <1> + .withTimeout().duration(5, ChronoUnit.SECONDS).done() + .build(); + + public String hello() throws Exception { + return guard.call(); // <2> + } +} +---- + +<1> The `createCallable` method returns a fault tolerance builder that provides the same configuration options, but in the end, returns a `Callable`. + In this case, a `Callable`. + These methods typically don't require an explicit type argument, because it can be inferred from the type of action passed in. + Similar methods exist that return a builder which, in the end, returns a `Supplier` (`createSupplier`) or `Runnable` (`createRunnable`). +<2> Here, we don't have to do anything special, just call the existing `Callable`. + Again, it could possibly be called multiple times, passed to other methods etc. + +=== Stateful Fault Tolerance Strategies + +The bulkhead, circuit breaker and rate limit strategies are stateful. +That is, they hold some state required for their correct functioning, such as the number of current executions for bulkhead, the rolling window of successes/failures for circuit breaker, or the time window for rate limit. +If you use these strategies, you have to consider their lifecycle. + +The {smallrye-fault-tolerance} programmatic API makes such reasoning pretty straightforward. +Each `FaultTolerance` object has its own instance of each fault tolerance strategy, including the stateful strategies. +If you use a single `FaultTolerance` object for guarding multiple different actions, all those actions will be guarded by the same bulkhead, circuit breaker and/or rate limit. +If, on the other hand, you use different `FaultTolerance` objects for guarding different actions, each action will be guarded by its own bulkhead, circuit breaker and/or rate limit. + +If you use the `adapt*` methods, the resulting `Callable`, `Supplier` or `Runnable` objects will guard the underlying action using the original `FaultTolerance` instance, so stateful strategies will be shared. + +If you use the `create*` methods that directly return `Callable`, `Supplier` or `Runnable`, each such creation will have its own `FaultTolerance` instance under the hood, so stateful strategies will _not_ be shared. + +=== Circuit Breaker Maintenance + +The `CircuitBreakerMaintenance` API, accessed through `FaultTolerance.circuitBreakerMaintenance()` or by injection in the CDI implementation, can be used to manipulate all named circuit breakers. +A circuit breaker is given a name by calling `withCircuitBreaker().name("\...")` on the fault tolerance builder, or using the `@CircuitBreakerName` annotation in the declarative API. + +Additionally, `CircuitBreakerMaintenance.resetAll()` will also reset all unnamed circuit breakers declared using the `@CicruitBreaker` annotation. +For this to work, all unnamed circuit breakers have to be remembered. +This is safe in case of the declarative, annotation-based API, because the number of such declared circuit breakers is fixed. +At the same time, this would _not_ be safe to do for all unnamed circuit breakers created using the programmatic API, as their number is potentially unbounded. +(In other words, remembering all unnamed circuit breakers created using the programmatic API would easily lead to a memory leak.) + +Therefore, all circuit breakers created using the programmatic API must be given a name when `CircuitBreakerMaintenance` is supposed to affect them. +Note that duplicate names are not permitted and lead to an error, so lifecycle of the circuit breaker must be carefully considered. + +=== Event Listeners + +The programmatic API has one feature that the declarative API doesn't have: ability to observe certain events. +For example, when configuring a circuit breaker, it is possible to register a callback for circuit breaker state changes or for a situation when an open circuit breaker prevents an invocation. +When configuring a timeout, it is possible to register a callback for when the invocation times out, etc. etc. +For example: + +[source,java] +---- +private static final FaultTolerance guard = FaultTolerance.create() + .withTimeout().duration(5, ChronoUnit.SECONDS).onTimeout(() -> ...).done() // <1> + .build(); +---- + +<1> The `onTimeout` method takes a `Runnable` that will later be executed whenever an invocation guarded by `guard` times out. + +All event listeners registered like this must run quickly and must not throw exceptions. + +=== Summary of `FaultTolerance` Methods + +There's a number of static `create*` methods on the `FaultTolerance` interface. +Which one do you want to call depends on the result type of the builder and whether the guarded actions are synchronous or asynchronous. + +|=== +| The builder result type | Synchronous actions | Asynchronous actions + +| `FaultTolerance` +| `create()` -> `FaultTolerance` +| `createAsync()` -> `FaultTolerance>` + +| `Callable` +| `createCallable(Callable)` -> `Callable` +| `createAsyncCallable(Callable>)` -> `Callable>` + +| `Supplier` +| `createSupplier(Supplier)` -> `Supplier` +| `createAsyncSupplier(Supplier>)` -> `Supplier>` + +| `Runnable` +| `createRunnable(Runnable)` -> `Runnable` +| `createAsyncRunnable(Runnable)` -> `Runnable` +|=== + +When you have an instance of `FaultTolerance`, there's also a number of instance methods that either execute an action, or adapt an unguarded action to a guarded one. +Which one do you want to call depends on the type used to represent the action. + +|=== +| The action type | Execute | Adapt + +| `Callable` +| `call(Callable)` -> `T` +| `adaptCallable(Callable)` -> `Callable` + +| `Supplier` +| `get(Supplier)` -> `T` +| `adaptSupplier(Supplier)` -> `Supplier` + +| `Runnable` +| `run(Runnable)` -> `void` +| `adaptRunnable(Runnable)` -> `Runnable` +|=== + +== Mutiny Support + +In addition to the `FaultTolerance` interface, which provides support for guarding synchronous actions and asynchronous actions using `CompletionStage`, there's a special programmatic API entrypoint for asynchronous actions using the Mutiny library. +It is enough to include the Mutiny support library `io.smallrye:smallrye-fault-tolerance-mutiny`, as described in xref:usage/extra.adoc#async-types[Additional Asynchronous Types]. + +This entrypoint is called `MutinyFaultTolerance` and it includes static factory methods for creating a `Callable>`, `Supplier` and `FaultTolerance>`. +Guarding a `Multi` is not supported. + +These factory methods return the common fault tolerance builder, which is supposed to be used just like the builder used when guarding an async action of type `CompletionStage`. +For example: + +[source, java] +---- +public class MyService { + private final Supplier> guard = MutinyFaultTolerance.createSupplier(() -> externalService.hello()) // <1> + .withTimeout().duration(5, ChronoUnit.SECONDS).done() + .withFallback().handler(() -> Uni.createFrom().item("fallback")).done() + .build(); + + public Uni hello() { + return guard.get(); + } +} +---- + +<1> The call to `externalService.hello()` is supposed to return `Uni`. + +Note that the `Uni` type is lazy, so the action itself won't execute until the guarded `Uni` is subscribed to. + +.Quarkus +**** +In Quarkus, the Mutiny support library is present by default. +You can use `MutinyFaultTolerance` out of the box. +**** + +== Configuration and Metrics + +As mentioned above, with the single exception of `MP_Fault_Tolerance_NonFallback_Enabled`, there is no external configuration support. +This may change in the future, though possibly only in the CDI implementation. + +At the moment, the programmatic API of {smallrye-fault-tolerance} is not integrated with metrics. +This will change in the future, though possibly only in the CDI implementation. + +== Integration Concerns + +Integration concerns, which are particularly interesting for users of the standalone implementation, are xref:integration/programmatic-api.adoc[described] in the integration section. diff --git a/content/docs/smallrye-fault-tolerance/6/_attributes.adoc b/content/docs/smallrye-fault-tolerance/6/_attributes.adoc new file mode 100644 index 000000000..856204445 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/6/_attributes.adoc @@ -0,0 +1,24 @@ +:project-version: 6.11.4 +// Attributes shared by every page. smallrye.io overrides the ones it needs when it publishes. +// Pages in subdirectories set :version-root: (../ per level) before including this file. +ifndef::version-root[:version-root:] +:idprefix: +:idseparator: - +:sectanchors: +:partialsdir: {version-root}_partials +:examples-dir: {version-root}_examples +:smallrye-fault-tolerance: SmallRye Fault Tolerance +:smallrye-fault-tolerance-version: 6.11.4 +:microprofile-fault-tolerance: MicroProfile Fault Tolerance +:microprofile-fault-tolerance-version: 4.1.2 +:microprofile-fault-tolerance-url: https://download.eclipse.org/microprofile/microprofile-fault-tolerance-4.1/microprofile-fault-tolerance-spec-4.1.html +:vertx4-version: 4.5.25 +:generated-dir: {version-root}_generated + +// Added by smallrye.io (tools/docs/Docs.java sync) +:javadoc-version: 6.11.4 +:relfileprefix: /docs/smallrye-fault-tolerance/6/ +:imagesdir: /docs/smallrye-fault-tolerance/6 +:docs-project: smallrye-fault-tolerance +:docs-version: 6 +:docs-ref: 6.11.4 diff --git a/content/docs/smallrye-fault-tolerance/6/_nav.yml b/content/docs/smallrye-fault-tolerance/6/_nav.yml new file mode 100644 index 000000000..601e84f10 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/6/_nav.yml @@ -0,0 +1,82 @@ +title: SmallRye Fault Tolerance +nav: +- title: Overview + page: index +- title: How-to + items: + - title: Begin + page: howto/begin + - title: Supply Fallback Values + page: howto/fallback + - title: Limit Concurrency + page: howto/bulkhead + - title: Fail Fast on Recurring Failures + page: howto/circuit-breaker + - title: Watch for Timeouts + page: howto/timeout + - title: Retry on Failures + page: howto/retry + - title: Guard Asynchronous Methods + page: howto/asynchronous + - title: Limit Execution Rate + page: howto/rate-limit + - title: Use Multiple Strategies + page: howto/multiple +- title: Reference + items: + - title: Fallback + page: reference/fallback + - title: Bulkhead + page: reference/bulkhead + - title: Circuit Breaker + page: reference/circuit-breaker + - title: Timeout + page: reference/timeout + - title: Retry + page: reference/retry + - title: Asynchronous Execution + page: reference/asynchronous + - title: Rate Limit + page: reference/rate-limit + - title: Configuration + page: reference/config + - title: Metrics + page: reference/metrics + - title: Non-compatible Mode + page: reference/non-compat + - title: Programmatic API + page: reference/programmatic-api + - title: Reusable Fault Tolerance + page: reference/reusable +- title: Integration + items: + - title: Introduction + page: integration/intro + - title: Metrics + page: integration/metrics + - title: Thread Pool + page: integration/thread-pool + - title: Context Propagation + page: integration/context-propagation + - title: OpenTracing + page: integration/opentracing + - title: Event Loop + page: integration/event-loop + - title: Additional Asynchronous Types Integration Concerns + page: integration/async-types + - title: Kotlin Integration Concerns + page: integration/kotlin + - title: Programmatic API Integration Concerns + page: integration/programmatic-api +- title: Internals + items: + - title: Project Structure + page: internals/project-structure + - title: Instructions + page: internals/instructions + - title: Fault Tolerance Core + page: internals/core + - title: Configuration System + page: internals/config + - title: Logging + page: internals/logging diff --git a/content/docs/smallrye-fault-tolerance/6/_partials/internals.adoc b/content/docs/smallrye-fault-tolerance/6/_partials/internals.adoc new file mode 100644 index 000000000..6a5d9340f --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/6/_partials/internals.adoc @@ -0,0 +1,3 @@ +WARNING: Note that everything explained here is an implementation detail. +API stability for these interfaces and classes is _not_ guaranteed! +This documentation is only present to provide implementation insight for people who have to debug issues or want to contribute. diff --git a/content/docs/smallrye-fault-tolerance/6/_partials/non-compat.adoc b/content/docs/smallrye-fault-tolerance/6/_partials/non-compat.adoc new file mode 100644 index 000000000..e087cdb10 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/6/_partials/non-compat.adoc @@ -0,0 +1 @@ +IMPORTANT: This feature only works in the xref:reference/non-compat.adoc[non-compatible mode]. diff --git a/content/docs/smallrye-fault-tolerance/6/_partials/srye-feature.adoc b/content/docs/smallrye-fault-tolerance/6/_partials/srye-feature.adoc new file mode 100644 index 000000000..0f4a979cf --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/6/_partials/srye-feature.adoc @@ -0,0 +1 @@ +IMPORTANT: This is an additional feature of {smallrye-fault-tolerance} and is not specified by {microprofile-fault-tolerance}. diff --git a/content/docs/smallrye-fault-tolerance/6/_partials/time-unit-inconsistency.adoc b/content/docs/smallrye-fault-tolerance/6/_partials/time-unit-inconsistency.adoc new file mode 100644 index 000000000..b742c482b --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/6/_partials/time-unit-inconsistency.adoc @@ -0,0 +1,3 @@ +NOTE: Note the naming inconsistency between `{time-value-name}` and `{time-unit-bad-name}`. +It is common for the time unit property to be named after the time value property, e.g. `{time-unit-good-name}`. +Fixing this inconsistency would be a breaking change the {microprofile-fault-tolerance} specification is not willing to make. diff --git a/content/docs/smallrye-fault-tolerance/6/howto/asynchronous.adoc b/content/docs/smallrye-fault-tolerance/6/howto/asynchronous.adoc new file mode 100644 index 000000000..4c8394666 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/6/howto/asynchronous.adoc @@ -0,0 +1,102 @@ += How to Guard Asynchronous Methods +:version-root: ../ +include::{version-root}_attributes.adoc[] + +{smallrye-fault-tolerance} allows offloading method calls to another thread, as well as applying asynchronous fault tolerance. + +Asynchronous fault tolerance means guarding not just the synchronous method execution, but the entire asynchronous operation started by the method call, until the result is delivered. +For example: + +[source,java] +---- +CompletionStage doSomething() { + return CompletableFuture.supplyAsync(() -> { + if (someCondition) { + return someValue; + } else { + throw someException; + } + }); +} +---- + +This method always returns a `CompletionStage` and it never throws an exception. +However, if `someCondition` does not hold, the `CompletionStage` returned from the method will complete exceptionally. + +Ordinary (non-asynchronous) fault tolerance: + +* never considers this operation failing, because the method returns a value and does not throw an exception; +* considers this operation finished when the method returns. + +Asynchronous fault tolerance, on the other hand: + +* defers its decision process to the moment when the returned `CompletionStage` completes, +* and considers this operation running until that happens. + +As a special case, if an asynchronous method in fact does throw an exception (it typically shouldn't), it is treated as if it returned a `CompletionStage` that is already completed exceptionally with the thrown exception. + +== `@Asynchronous` + +A method that returns `java.util.concurrent.Future` or `java.util.concurrent.CompletionStage` may be annotated `@Asynchronous`. +Such method is executed on another thread. +Additionally, if the return type is `CompletionStage`, asynchronous fault tolerance applies. + +[NOTE] +==== +Note that if an `@Asynchronous` method returns a `Future`, asynchronous fault tolerance does _not_ apply. +Only the synchronous method execution is guarded. + +Asynchronous fault tolerance only applies if the `@Asynchronous` method returns `CompletionStage`. +==== + +Use `@Asynchronous` if the method has blocking logic, but you don’t want to block the caller thread. +For example: + +[source,java] +---- +@ApplicationScoped +public class MyService { + @Retry + @Asynchronous // <1> + CompletionStage hello() { + ... blocking IO here ... + } +} +---- + +<1> Using the `@Asynchronous` annotation, because the method blocks, and it is necessary to offload its execution to another thread. + +== `@AsynchronousNonBlocking` + +include::{partialsdir}/srye-feature.adoc[] + +A method that returns `CompletionStage` may be annotated `@AsynchronousNonBlocking`. +In such case, the method is executed on the original thread, but asynchronous fault tolerance still applies. + +Use `@AsynchronousNonBlocking` if the method _does not_ have blocking logic, and you want the execution to stay on the caller thread. +For example: + +[source,java] +---- +@ApplicationScoped +public class MyService { + @Retry + @AsynchronousNonBlocking // <1> + CompletionStage hello() { + ... non-blocking IO here ... + } +} +---- + +<1> Using the `@AsynchronousNonBlocking` annotation, because the method does not block and offloading execution to another thread is not necessary. + +=== Non-compatible mode + +include::{partialsdir}/non-compat.adoc[] + +In the non-compatible mode, the `@AsynchronousNonBlocking` annotation is not necessary. +A method that returns `CompletionStage` and is not annotated `@Asynchronous` is automatically treated as `@AsynchronousNonBlocking`. + +== Links + +For more information, see xref:reference/asynchronous.adoc[the Asynchronous Execution reference guide]. diff --git a/content/docs/smallrye-fault-tolerance/6/howto/begin.adoc b/content/docs/smallrye-fault-tolerance/6/howto/begin.adoc new file mode 100644 index 000000000..3b5f0f3e7 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/6/howto/begin.adoc @@ -0,0 +1,129 @@ += How to Begin +:version-root: ../ +include::{version-root}_attributes.adoc[] + +== Installation + +As an application developer, you don't have to "install" {smallrye-fault-tolerance} in any way. +Your chosen runtime, such as Quarkus or WildFly, already integrates {smallrye-fault-tolerance} to provide {microprofile-fault-tolerance}. +Please refer to the documentation of your selected runtime to understand how to enable MicroProfile Fault Tolerance. + +.Quarkus +**** +In case of Quarkus, it is enough to add a dependency on `io.quarkus:quarkus-smallrye-fault-tolerance`. + +See the https://quarkus.io/guides/smallrye-fault-tolerance[{smallrye-fault-tolerance} guide] in Quarkus documentation for more. +**** + +.WildFly +**** +In case of WildFly, the {microprofile-fault-tolerance} subsystem must be enabled. +It is not included in the default `standalone` or `standalone-full` profiles, but it is included in the `standalone-microprofile` profile. +It is easiest to just start WildFly in this profile. + +See the https://docs.wildfly.org/27/Admin_Guide.html#MicroProfile-Fault-Tolerance-SmallRye[{microprofile-fault-tolerance} chapter] in WildFly Admin Guide for more. +**** + +If you are a framework developer, interested in integrating {smallrye-fault-tolerance} in your framework or runtime, please refer to the xref:integration/intro.adoc[Integration] section. + +== Usage + +{microprofile-fault-tolerance} is built on top of CDI and interceptors. +This lets you guard method calls with selected fault tolerance strategies simply by annotating a CDI bean class or method with one of the fault tolerance annotations. +For example: + +[source,java] +---- +@ApplicationScoped // <1> +public class MyService { + @Retry // <2> + public String hello() { + ... + } +} +---- + +<1> `@ApplicationScoped` is a bean defining annotation. +It makes the `MyService` class a CDI bean. +<2> `@Retry` is a fault tolerance annotation. + +Here, if the `hello` method throws an exception, the invocation will be retried several times, until the method returns a value. + +The annotation may also be present on a class, in which case it applies to all business method in the class: + +[source,java] +---- +package com.example; + +@ApplicationScoped +@Retry // <1> +public class MyService { + public String hello() { // <2> + ... + } +} +---- +<1> `@Retry` is a fault tolerance annotation. +<2> The `hello()` method will use the retry strategy, because it is defined on the class. + +A class-level annotation may be overridden by explicitly annotating a method. + +TIP: CDI beginners often expect that this will work if you create an instance manually: `new MyService()`. +That is not the case. +You have to let the CDI container construct an instance for you and inject it: `@Inject MyService service`. +What you get injected is actually a proxy that implements the additional behaviors. + +The fault tolerance strategies present in {microprofile-fault-tolerance}, together with the corresponding annotations, are: + +* `@Asynchronous`: offload method execution to another thread +* `@Bulkhead`: limit concurrent invocations +* `@CircuitBreaker`: prevent invocations if previous invocations failed too often +* `@Fallback`: provide alternative result in case of a failure +* `@Retry`: retry several times in case of a failure +* `@Timeout`: fail if the invocation takes too long + +{smallrye-fault-tolerance} adds some more fault tolerance strategies and annotations: + +* `@ApplyGuard`: apply a programmatically-defined guard +* `@AsynchronousNonBlocking`: apply asynchronous fault tolerance without offloading method execution to another thread +* `@RateLimit`: limit rate of method invocations + +These are all described in the corresponding how-to and reference guides. +The {microprofile-fault-tolerance} features are also described in the {microprofile-fault-tolerance-url}[specification]. + +== Configuration + +The fault tolerance strategies can be configured using annotations, such as: + +[source,java] +---- +@ApplicationScoped +public class MyService { + @Retry(maxRetries = 10, retryOn = IOException.class) // <1> + public String hello() { + ... + } +} +---- + +<1> Retries will only be attempted if the thrown exception was `IOException`. +Other exceptions will be rethrown directly. +Also, the maximum number of retry attempts is changed to 10 (the default is 3). + +This is convenient, but changing such configuration requires recompilation. +For that reason, configuration using MicroProfile Config is also possible. +For example: + +[source,properties] +---- +smallrye.faulttolerance."com.example.MyService/hello".retry.max-retries=5 <1> + +# alternatively, a specification-defined property can be used +com.example.MyService/hello/Retry/maxRetries=5 <1> +---- +<1> Even though the `@Retry` annotation says that `maxRetries` should be 10, this configuration takes precedence. +The maximum number of retries will be 5. + +Since this example configuration does not modify the `retryOn` setting, the configuration provided in the annotation still applies. + +For more information about configuration, see xref:reference/config.adoc[the Configuration reference guide]. diff --git a/content/docs/smallrye-fault-tolerance/6/howto/bulkhead.adoc b/content/docs/smallrye-fault-tolerance/6/howto/bulkhead.adoc new file mode 100644 index 000000000..39326779c --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/6/howto/bulkhead.adoc @@ -0,0 +1,33 @@ += How to Limit Concurrency +:version-root: ../ +include::{version-root}_attributes.adoc[] + +It is possible to prevent an operation from having too many concurrent executions using a _bulkhead_. + +NOTE: In general, the term _bulkhead_ refers to system compartmentalization, or the practice of preventing failures in one part of the system from spreading to other parts of the system. +In {microprofile-fault-tolerance}, the term refers to one specific instance of the general idea: a _concurrency limiter_. + +== `@Bulkhead` + +The `@Bulkhead` annotation specifies that a concurrency limit should be enforced for all calls of the method. + +[source,java] +---- +@ApplicationScoped +public class MyService { + @Bulkhead // <1> + public String hello() { + ... + } +} +---- +<1> Declares that `hello()` must not have too many concurrent executions. +Since there is no configuration, the default is at most 10 concurrent calls. + +If some callers attempt to invoke the method when the concurrency limit has already been reached, they will fail with `BulkheadException`. + +It is possible to specify the maximum allowed concurrency, as well as define a queue for excess invocations in case of xref:howto/asynchronous.adoc[asynchronous methods]. + +== Links + +For more information, see xref:reference/bulkhead.adoc[the Bulkhead reference guide]. diff --git a/content/docs/smallrye-fault-tolerance/6/howto/circuit-breaker.adoc b/content/docs/smallrye-fault-tolerance/6/howto/circuit-breaker.adoc new file mode 100644 index 000000000..0dcd618ec --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/6/howto/circuit-breaker.adoc @@ -0,0 +1,33 @@ += How to Fail Fast on Recurring Failures +:version-root: ../ +include::{version-root}_attributes.adoc[] + +If an operation fails too often, it is possible to prevent executing that operation and fail fast using a _circuit breaker_. + +The circuit breaker starts _closed_, in which state it allows all invocations to proceed. +If too many invocations fail, the circuit breaker moves to _open_, in which state it prevents all invocations and fails fast. +After some time in _open_, the circuit breaker moves to _half-open_, in which state it determines whether it should continue failing fast (and move back to _open_) or allow invocations again (and move to _closed_). + +== `@CircuitBreaker` + +The `@CircuitBreaker` annotation specifies that method invocations should be prevented if the method failed too often recently. + +[source,java] +---- +@ApplicationScoped +public class MyService { + @CircuitBreaker // <1> + public String hello() { + ... + } +} +---- +<1> Declares that `hello()` must be guarded with a circuit breaker. + +If some callers attempt to invoke the method when the circuit breaker is _open_, they will fail with `CircuitBreakerOpenException`. + +It is possible to configure the number of recent invocations the circuit breaker should track, the failure rate, and so on. + +== Links + +For more information, see xref:reference/circuit-breaker.adoc[the Circuit Breaker reference guide]. diff --git a/content/docs/smallrye-fault-tolerance/6/howto/fallback.adoc b/content/docs/smallrye-fault-tolerance/6/howto/fallback.adoc new file mode 100644 index 000000000..a0fb8826d --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/6/howto/fallback.adoc @@ -0,0 +1,62 @@ += How to Supply Fallback Values +:version-root: ../ +include::{version-root}_attributes.adoc[] + +If an operation fails, it is often possible to replace the failure with an alternative result: a _fallback_. + +== `@Fallback` + +=== Using a Fallback Method + +The `@Fallback` annotation allows specifying a method that should be called in case an exception is thrown. +The fallback method must have the same parameter types as the guarded method, and must return a type that is assignable to the guarded method's return type. + +[source,java] +---- +@ApplicationScoped +public class MyService { + @Fallback(fallbackMethod = "fallback") // <1> + public String hello() { + ... + } + + private String fallback() { + return "fallback"; + } +} +---- +<1> Declares that if `hello()` throws an exception, the `fallback()` method is called and its result is returned instead. + +In this particular case, if `hello()` throws an exception, that exception is swallowed and the string `"fallback"` is returned. +It is possible to specify that fallback should only apply to certain exception types or that it should be skipped for certain exception types. + +=== Using a Fallback Handler + +Alternatively, the `@Fallback` annotation allows specifying a fallback handler that should be called. + +[source,java] +---- +@ApplicationScoped +public class MyService { + @Fallback(MyFallback.class) // <1> + public String hello() { + ... + } +} +---- +<1> Declares that if `hello()` throws an exception, the `MyFallback` handler is used to obtain a fallback value. + +[source,java] +---- +public class MyFallback implements FallbackHandler { // <1> + String handle(ExecutionContext context) { // <2> + return "fallback"; + } +} +---- +<1> A fallback handler must implement the `FallbackHandler` interface. +<2> The `ExecutionContext` provides access to the guarded method, its parameter values, and the thrown exception. + +== Links + +For more information, see xref:reference/fallback.adoc[the Fallback reference guide]. diff --git a/content/docs/smallrye-fault-tolerance/6/howto/multiple.adoc b/content/docs/smallrye-fault-tolerance/6/howto/multiple.adoc new file mode 100644 index 000000000..7ee19a470 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/6/howto/multiple.adoc @@ -0,0 +1,58 @@ += How to Use Multiple Strategies +:version-root: ../ +include::{version-root}_attributes.adoc[] + +It is possible to use multiple fault tolerance strategies to guard a single method. +For example, it is possible to use fallback together with retry and timeout: + +[source,java] +---- +@ApplicationScoped +public class MyService { + @Retry(maxRetries = 5) + @Fallback(fallbackMethod = "fallback") + @Timeout + public String hello() { + ... + } + + public String fallback() { + return "fallback"; + } +} +---- + +In such case, the fault tolerance strategies are nested in a pre-defined order: + +[source] +---- +Fallback( + Retry( + CircuitBreaker( + RateLimit( + Timeout( + Bulkhead( + ... the guarded method ... + ) + ) + ) + ) + ) +) +---- + +In the previous example, this means that timeout is nested inside retry, which in turn is nested inside fallback. +If the method times out (the default timeout is 1 second), it is retried (up to 5 times). +If no attempt succeeds, fallback is used. + +== Links + +Reference guides for all fault tolerance strategies explain in detail how they interact with one another: + +* xref:reference/asynchronous.adoc#interactions[@Asynchronous and @AsynchronousNonBlocking] +* xref:reference/bulkhead.adoc#interactions[@Bulkhead] +* xref:reference/circuit-breaker.adoc#interactions[@CircuitBreaker] +* xref:reference/fallback.adoc#interactions[@Fallback] +* xref:reference/rate-limit.adoc#interactions[@RateLimit] +* xref:reference/retry.adoc#interactions[@Retry] +* xref:reference/timeout.adoc#interactions[@Timeout] diff --git a/content/docs/smallrye-fault-tolerance/6/howto/rate-limit.adoc b/content/docs/smallrye-fault-tolerance/6/howto/rate-limit.adoc new file mode 100644 index 000000000..c818bfdfe --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/6/howto/rate-limit.adoc @@ -0,0 +1,33 @@ += How to Limit Execution Rate +:version-root: ../ +include::{version-root}_attributes.adoc[] + +include::{partialsdir}/srye-feature.adoc[] + +It is possible to prevent an operation from being executed too often using a _rate limit_. + +== `@RateLimit` + +The `@RateLimit` annotation specifies that a rate limit should be enforced for all method calls. + +[source,java] +---- +@ApplicationScoped +public class MyService { + @RateLimit // <1> + public String hello() { + ... + } +} +---- +<1> Declares that `hello()` must not be called too often. +Since there is no configuration, the default is at most 100 calls per 1 minute. + +If the method is called too often, excess attempts will fail with `RateLimitException`. + +It is possible to specify the maximum allowed rate of execution (the rate limit), together with minimum spacing between invocations. +There are also several types of rate limiting to choose from. + +== Links + +For more information, see xref:reference/rate-limit.adoc[the Rate Limit reference guide]. diff --git a/content/docs/smallrye-fault-tolerance/6/howto/retry.adoc b/content/docs/smallrye-fault-tolerance/6/howto/retry.adoc new file mode 100644 index 000000000..865403c37 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/6/howto/retry.adoc @@ -0,0 +1,33 @@ += How to Retry on Failures +:version-root: ../ +include::{version-root}_attributes.adoc[] + +If an operation fails, it is possible to _retry_ the operation in case the failure was just a temporary glitch. + +CAUTION: In case an operation fails due to the target system being overloaded, retrying will likely make the overload situation even worse. +Use sparingly, preferably with a xref:howto/bulkhead.adoc[concurrency limit] or a xref:howto/circuit-breaker.adoc[circuit breaker]. + +== `@Retry` + +The `@Retry` annotation specifies that a method should be called again in case a previous execution thrown an exception. + +[source,java] +---- +@ApplicationScoped +public class MyService { + @Retry // <1> + public String hello() { + ... + } +} +---- +<1> Declares that if `hello()` throws an exception, the exception is swallowed and `hello()` is called again. +Since there is no configuration, at most 3 retry attempts will be done before rethrowing the exception. + +It is possible to specify the maximum number of retry attempts, as well as the maximum duration for which retries should be attempted. +Further, it is possible to specify some delay between retry attempts, together with random jitter. +Finally, it is possible to specify that calls should be retried only for certain exception types or that retrying should be aborted for certain exception types. + +== Links + +For more information, see xref:reference/retry.adoc[the Retry reference guide]. diff --git a/content/docs/smallrye-fault-tolerance/6/howto/timeout.adoc b/content/docs/smallrye-fault-tolerance/6/howto/timeout.adoc new file mode 100644 index 000000000..c3e9fdf8b --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/6/howto/timeout.adoc @@ -0,0 +1,28 @@ += How to Watch for Timeouts +:version-root: ../ +include::{version-root}_attributes.adoc[] + +It is possible to prevent an operation from taking too long using a _timeout_. + +== `@Timeout` + +The `@Timeout` annotation specifies that all calls of the method should fail if they take too long. + +[source,java] +---- +@ApplicationScoped +public class MyService { + @Timeout // <1> + public String hello() { + ... + } +} +---- +<1> Declares that `hello()` must not take too long. +Since there is no configuration, the default is at most 1 second. + +If the method call takes too long, the caller will get a `TimeoutException` instead. + +== Links + +For more information, see xref:reference/timeout.adoc[the Timeout reference guide]. diff --git a/content/docs/smallrye-fault-tolerance/6/images/diagrams/mermaid-f40d3cbcabca.svg b/content/docs/smallrye-fault-tolerance/6/images/diagrams/mermaid-f40d3cbcabca.svg new file mode 100644 index 000000000..a31aed359 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/6/images/diagrams/mermaid-f40d3cbcabca.svg @@ -0,0 +1 @@ +

Recent invocations failed too often

Configured time elapsed

All probe invocations succeeded

Some probe invocations failed

[CLOSED] Allow invocations and track the number of successes and failures

[OPEN] Fail fast, prevent all invocations

[HALF-OPEN] Allow some probe invocations to proceed

\ No newline at end of file diff --git a/content/docs/smallrye-fault-tolerance/6/index.adoc b/content/docs/smallrye-fault-tolerance/6/index.adoc new file mode 100644 index 000000000..c8eccf252 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/6/index.adoc @@ -0,0 +1,77 @@ += SmallRye Fault Tolerance Documentation +include::_attributes.adoc[] + +{smallrye-fault-tolerance} {smallrye-fault-tolerance-version} is an implementation of https://github.com/eclipse/microprofile-fault-tolerance/[Eclipse {microprofile-fault-tolerance}] {microprofile-fault-tolerance-version} with additional features not defined by the specification. + +It provides a declarative, annotation-based API for guarding method calls: + +[source,java] +---- +@ApplicationScoped +public class MyService { + @Retry // <1> + public String hello() { + ... + } +} +---- +<1> If `hello()` throws an exception, the invocation will be retried several times, until the method returns a value. + +It also provides a programmatic API to build reusable guards: + +[source,java] +---- +public class MyService { + private static final TypedGuard GUARD = TypedGuard.create(String.class) + .withFallback().handler(() -> "fallback").done() // <1> + .build(); + + public String hello() throws Exception { + return GUARD.call(() -> externalService.hello()); // <2> + } +} +---- +<1> If the guarded call throws an exception, the fallback value will be returned instead. +<2> The guarded call simply invokes some external service. + +CAUTION: The fault tolerance strategies provided by {smallrye-fault-tolerance} are best suited for guarding network interactions. +To handle ordinary exceptions, it is typically best to use the plain old `try` / `catch` / `finally` blocks. +Using {smallrye-fault-tolerance} as a replacement of exception handling is often a bad idea. + +NOTE: Some additional features may have experimental status, marked by the `@Experimental` annotation. +The programmatic API to build reusable guards, as mentioned above, is one example, but there are more. + +== Runtimes + +Runtimes that currently use {smallrye-fault-tolerance} include: + +* https://quarkus.io/[Quarkus] +* https://wildfly.org/[WildFly] + +== Navigation + +This documentation site has several sections, as you can see in the menu. + +=== How-to Guides + +How-tos are task-oriented guides that let you quickly start using some feature. +They provide a short overview, but not many details. + +TIP: If you're just starting with {smallrye-fault-tolerance}, start with the how-tos. + +=== References + +References are detailed guides that let you understand some feature in depth. +They also provide information about more advanced options or features. + +TIP: If the how-to guide doesn't answer a question you have, the reference guide should. + +=== Integration + +The articles here address concerns that runtimes integrating {smallrye-fault-tolerance} might have. +Useful for people who maintain {smallrye-fault-tolerance} integration into some bigger framework or platform, but not required for application developers. + +=== Internals + +A few random articles that describe some parts of the {smallrye-fault-tolerance} code base. +Useful for prospective contributors or generally curious users, but not required for application developers. diff --git a/content/docs/smallrye-fault-tolerance/6/integration/async-types.adoc b/content/docs/smallrye-fault-tolerance/6/integration/async-types.adoc new file mode 100644 index 000000000..f4706c68c --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/6/integration/async-types.adoc @@ -0,0 +1,15 @@ += Additional Asynchronous Types Integration Concerns +:version-root: ../ +include::{version-root}_attributes.adoc[] + +This page describes integration concerns for xref:reference/asynchronous.adoc#async-types[Additional Asynchronous Types]. + +To enable support of additional asynchronous types, it is required that the support library is present. +That is: + +* https://smallrye.io/smallrye-mutiny/[Mutiny]: `io.smallrye:smallrye-fault-tolerance-mutiny` +* https://github.com/ReactiveX/RxJava/tree/3.x[RxJava 3]: `io.smallrye:smallrye-fault-tolerance-rxjava3` +* https://vertx.io/docs/vertx-core/java/#_future_results[Vert.x `Future`]: `io.smallrye:smallrye-fault-tolerance-vertx` + +These libraries include some service providers that {smallrye-fault-tolerance} will automatically load and use. +Therefore, no more integration is necessary. diff --git a/content/docs/smallrye-fault-tolerance/6/integration/context-propagation.adoc b/content/docs/smallrye-fault-tolerance/6/integration/context-propagation.adoc new file mode 100644 index 000000000..a39b7816d --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/6/integration/context-propagation.adoc @@ -0,0 +1,8 @@ += Context Propagation +:version-root: ../ +include::{version-root}_attributes.adoc[] + +{smallrye-fault-tolerance} supports MicroProfile Context Propagation for `@Asynchronous` method invocations, if 2 conditions are satisfied: + +- the integrator provides a MicroProfile Context Propagation enabled thread pool (see xref:integration/thread-pool.adoc[Thread Pool]); +- the `io.smallrye:smallrye-fault-tolerance-context-propagation` artifact is present (it provides alternative implementations of some internal services, loaded using `ServiceLoader`). diff --git a/content/docs/smallrye-fault-tolerance/6/integration/event-loop.adoc b/content/docs/smallrye-fault-tolerance/6/integration/event-loop.adoc new file mode 100644 index 000000000..df4a58fc5 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/6/integration/event-loop.adoc @@ -0,0 +1,37 @@ += Event Loop +:version-root: ../ +include::{version-root}_attributes.adoc[] + +{smallrye-fault-tolerance} provides an API to integrate with event loops used in non-blocking runtimes. + +WARNING: This integration API comes from the `core` module and so is treated as private. +If you intend to implement it, please let us know in the {smallrye-fault-tolerance} issue tracker. +We'd prefer all the implementations to live in the {smallrye-fault-tolerance} project. + +The integrator must implement the `EventLoop` interface, which lets {smallrye-fault-tolerance} submit ``Runnable``s to the event loop for execution. +Implementations are discovered through `ServiceLoader`, so the corresponding `META-INF/services/\...` file must also exist. + +At most one implementation of `EventLoop` may be present at runtime. +{smallrye-fault-tolerance} will use it to execute non-blocking guarded methods when the caller was originally on an event loop thread. +This is important in case of: + +- retries, when there's a delay between retry attempts (the delay must not block); +- bulkheads, when a task is queued (such task is executed when some other task finishes, which happens on an unknown thread). + +If no event loop integration is present, the tasks described above will be offloaded to the xref:integration/thread-pool.adoc[thread pool]. + +The event loop integration is also used in some other situations when guarding non-blocking methods: + +- fallbacks, when the original method fails (the fallback is executed on the original event loop); +- timeouts, when timeout occurs (the failure is signalled on the original event loop). + +== Vert.x + +{smallrye-fault-tolerance} includes an implementation of the event loop integration API for Vert.x. +This implementation is compiled and tested against Vert.x version {vertx4-version}, but should also work fine with Vert.x 3.x. + +This implementation is present in the `io.smallrye:smallrye-fault-tolerance-vertx` artifact. +Including this artifact is enough to enable the Vert.x integration. + +NOTE: The Vert.x implementation is treated as public, but only as described here. +That is, the artifact should be treated as opaque, no guarantees about its internals are made. diff --git a/content/docs/smallrye-fault-tolerance/6/integration/intro.adoc b/content/docs/smallrye-fault-tolerance/6/integration/intro.adoc new file mode 100644 index 000000000..fb0c49de5 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/6/integration/intro.adoc @@ -0,0 +1,41 @@ += Introduction +:version-root: ../ +include::{version-root}_attributes.adoc[] + +{smallrye-fault-tolerance} comprises the following main artifacts: + +* `io.smallrye:smallrye-fault-tolerance-api`: public API for the features provided on top of {microprofile-fault-tolerance}, including a programmatic API; +* `io.smallrye:smallrye-fault-tolerance-apiimpl`: implementation of the programmatic API; +* `io.smallrye:smallrye-fault-tolerance-core`: core implementations of fault tolerance strategies; +* `io.smallrye:smallrye-fault-tolerance`: CDI-based implementation of {microprofile-fault-tolerance} with {smallrye-fault-tolerance} extensions, including a programmatic API; +* `io.smallrye:smallrye-fault-tolerance-autoconfig-core`: runtime part of the {smallrye-fault-tolerance} configuration system. +** (There's also `io.smallrye:smallrye-fault-tolerance-autoconfig-processor`, but that is only used during {smallrye-fault-tolerance} build and isn't required at runtime.) + +These artifacts must be always present, together with the following dependencies: + +* Jakarta CDI, Annotations and Interceptors API; +* `org.eclipse.microprofile.fault-tolerance:microprofile-fault-tolerance-api`; +* `org.eclipse.microprofile.config:microprofile-config-api` and some implementation; +* `org.jboss.logging:jboss-logging`. + +Some other artifacts are provided to facilitate additional integrations. +These are described in the subsequent sections. + +Most of the integration is performed automatically by the CDI Portable Extension present in {smallrye-fault-tolerance}: `io.smallrye.faulttolerance.FaultToleranceExtension`. +Make sure the extension is active. +The remaining integration concerns are described in the following sections: + +* xref:integration/metrics.adoc[emitting metrics] for fault tolerance strategies; +* xref:integration/thread-pool.adoc[providing a thread pool] for `@Asynchronous` methods and other asynchronous tasks; +* xref:integration/context-propagation.adoc[enabling Context Propagation] for `@Asynchronous` methods; +* xref:integration/opentracing.adoc[enabling OpenTracing integration] with Context Propagation; +* xref:integration/event-loop.adoc[integrating an event loop] for non-blocking method invocations; +* xref:integration/async-types.adoc[integrating additional types] for asynchronous invocations; +* xref:integration/kotlin.adoc[integrating Kotlin support] for `suspend` functions; +* xref:integration/programmatic-api.adoc[integrating the programmatic API] that {smallrye-fault-tolerance} provides. + +== Standalone Implementation of the Programmatic API + +A completely standalone implementation of the programmatic API exists in the `io.smallrye:smallrye-fault-tolerance-standalone` artifact. +This is supposed to be used in a non-CDI environment. +Runtimes that integrate {smallrye-fault-tolerance} typically do so using CDI, and so should ignore this artifact. diff --git a/content/docs/smallrye-fault-tolerance/6/integration/kotlin.adoc b/content/docs/smallrye-fault-tolerance/6/integration/kotlin.adoc new file mode 100644 index 000000000..98d9804f9 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/6/integration/kotlin.adoc @@ -0,0 +1,11 @@ += Kotlin Integration Concerns +:version-root: ../ +include::{version-root}_attributes.adoc[] + +This page describes integration concerns for xref:reference/asynchronous.adoc#kotlin-suspend-functions[Kotlin `suspend` functions]. + +To enable support of Kotlin suspending functions, it is required that the support library is present. +Its coordinates are: `io.smallrye:smallrye-fault-tolerance-kotlin`. + +This library includes some service providers that {smallrye-fault-tolerance} will automatically load and use. +Therefore, no more integration is necessary. diff --git a/content/docs/smallrye-fault-tolerance/6/integration/metrics.adoc b/content/docs/smallrye-fault-tolerance/6/integration/metrics.adoc new file mode 100644 index 000000000..6bc41895f --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/6/integration/metrics.adoc @@ -0,0 +1,75 @@ += Metrics +:version-root: ../ +include::{version-root}_attributes.adoc[] + +{smallrye-fault-tolerance} provides support for MicroProfile Metrics, OpenTelemetry and Micrometer. +Alternatively, metrics may be completely disabled at the integration level. + +As usual, this integration is based on CDI. +{smallrye-fault-tolerance} includes an internal interface `MetricsProvider` and these implementations: + +* `io.smallrye.faulttolerance.metrics.MicroProfileMetricsProvider` +* `io.smallrye.faulttolerance.metrics.OpenTelemetryProvider` +* `io.smallrye.faulttolerance.metrics.MicrometerProvider` +* `io.smallrye.faulttolerance.metrics.NoopProvider` + +There are 2 possible ways how to integrate metrics: + +* exactly 1 class from the list above is a bean; +* more than 1 class from the list above is a bean, in which case, `io.smallrye.faulttolerance.metrics.CompoundMetricsProvider` must also be a bean. + +NOTE: Only the _names_ of the classes listed above are treated as public. +That is, the classes should be treated as opaque, no guarantees about their internals are made. + +== Default Integration + +In case the integrator uses the CDI Portable Extension `FaultToleranceExtension` and lets the container create an instance, metrics presence is discovered automatically. +All present metrics systems are used. + +== Using a `FaultToleranceExtension` Constructor + +In case the integrator uses the CDI Portable Extension `FaultToleranceExtension` and creates its instance manually, they can use a constructor. + +In addition to a zero-parameter constructor, which is used in the default integration as described above, there are constructors that take a parameter of `MetricsIntegration` or `Set`. + +`MetricsIntegration` is an enum with these values: + +* `MICROPROFILE_METRICS`: use MicroProfile Metrics integration +* `OPENTELEMETRY`: use OpenTelemetry (MicroProfile Telemetry) integration +* `MICROMETER`: use Micrometer integration +* `NOOP`: no metrics + +== Metrics Providers + +Metrics providers have additional requirements, as described below. + +=== MicroProfile Metrics + +If MicroProfile Metrics should be used, the integrator must ensure that the following artifacts are present: + +* `org.eclipse.microprofile.metrics:microprofile-metrics-api`; +* some implementation of MicroProfile Metrics. + +=== OpenTelemetry + +If OpenTelemetry should be used, the integrator must ensure that the following artifact is present: + +* `io.opentelemetry:opentelemetry-api`. + +Further, a bean of type `io.opentelemetry.api.metrics.Meter` must exist. +This bean is used to emit the actual metrics. + +=== Micrometer + +If Micrometer should be used, the integrator must ensure that the following artifact is present: + +* `io.micrometer:micrometer-core`. + +Further, a bean of type `io.micrometer.core.instrument.MeterRegistry` must exist. +This bean is used to emit the actual metrics. + +=== Noop + +If the "noop" provider is used, metrics are completely disabled. + +No metrics API and implementation is required in this case. diff --git a/content/docs/smallrye-fault-tolerance/6/integration/opentracing.adoc b/content/docs/smallrye-fault-tolerance/6/integration/opentracing.adoc new file mode 100644 index 000000000..9d5955ff8 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/6/integration/opentracing.adoc @@ -0,0 +1,13 @@ += OpenTracing +:version-root: ../ +include::{version-root}_attributes.adoc[] + +{smallrye-fault-tolerance} includes an integration between MicroProfile Context Propagation and OpenTracing. +That is, {smallrye-fault-tolerance} includes an implementation of `ThreadContextProvider` which propagates the active OpenTracing span from one thread to another. +This can be used when xref:integration/context-propagation.adoc[Context Propagation] is enabled. + +This integration is present in the `io.smallrye:smallrye-fault-tolerance-tracing-propagation` artifact. +It is used automatically when xref:integration/context-propagation.adoc[Context Propagation] is present. +When Context Propagation is not present, this artifact can still be in place, it just won't do anything. + +Technically, this doesn't even belong to {smallrye-fault-tolerance}, but it's present for historical reasons. diff --git a/content/docs/smallrye-fault-tolerance/6/integration/programmatic-api.adoc b/content/docs/smallrye-fault-tolerance/6/integration/programmatic-api.adoc new file mode 100644 index 000000000..1bef9aa24 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/6/integration/programmatic-api.adoc @@ -0,0 +1,49 @@ += Programmatic API Integration Concerns +:version-root: ../ +include::{version-root}_attributes.adoc[] + +This page describes integration concerns of the xref:reference/programmatic-api.adoc[Programmatic API] of {smallrye-fault-tolerance}. + +== CDI Implementation + +Runtimes that provide the CDI implementation of the programmatic API don't have to do any extra integration work. +The existing integration is enough. + +== Standalone Implementation + +The standalone implementation exposes a single integration point: the `StandaloneFaultTolerance.configure()` static method. +This method must be called before fault tolerance is first used. +It accepts a custom implementation of the `Configuration` interface, which allows customizing: + +* `enabled()`: if `false`, all fault tolerance strategies except fallback and thread offload are disabled +* `executor()`: executor for thread offloads and other asynchronous tasks +* `metricsAdapter()`: adapter for metrics, see below + +When no `Configuration` is provided, sensible defaults are used: + +* fault tolerance is enabled unless system property `MP_Fault_Tolerance_NonFallback_Enabled` is set to `false` +* a thread pool obtained using `Executors.newCachedThreadPool()` is used as an executor +* no metrics are emitted + +Users of the standalone implementation that also use an event loop based library, such as Vert.x, may integrate the event loop support as described in xref:integration/event-loop.adoc[Event Loop]. + +=== Lifecycle + +The thread pool and all other internal resources are initialized lazily, on the first use. +The `StandaloneFaultTolerance.shutdown()` method may be used on application shutdown to shut down these internal resources. + +The executor is only shut down when no `Configuration` was provided and {smallrye-fault-tolerance} had to create an executor on its own. +If `Configuration` was provided, the executor shutdown is left to the integrator; it is _not_ attempted automatically. + +At the end of `StandaloneFaultTolerance.shutdown()`, the `Configuration.onShutdown()` method is called. + +After `StandaloneFaultTolerance.shutdown()`, it is not possible to reinitialize {smallrye-fault-tolerance} again. + +=== Metrics + +In the standalone implementation, MicroProfile Metrics make no sense, as that is exclusively based on CDI. +It is however possible to integrate with OpenTelemetry or Micrometer. + +The `Configuration.metricsAdapter()` method must be implemented and return an instance of `io.smallrye.faulttolerance.standalone.OpenTelemetryAdapter` or `io.smallrye.faulttolerance.standalone.MicrometerAdapter`. +The constructor of `OpenTelemetryAdapter` accepts the `Meter` to which metrics shall be emitted. +The constructor of `MicrometerAdapter` accepts the `MeterRegistry` to which metrics shall be emitted. diff --git a/content/docs/smallrye-fault-tolerance/6/integration/thread-pool.adoc b/content/docs/smallrye-fault-tolerance/6/integration/thread-pool.adoc new file mode 100644 index 000000000..b223fd4fb --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/6/integration/thread-pool.adoc @@ -0,0 +1,30 @@ += Thread Pool +:version-root: ../ +include::{version-root}_attributes.adoc[] + +{smallrye-fault-tolerance} expects integrators to provide a thread pool for running `@Asynchronous` methods and other asynchronous tasks. +This integration is based on CDI. + +The integrator should provide a CDI bean which implements `AsyncExecutorProvider`. +The bean should be `@Singleton`, must be marked as alternative and selected globally for the application. + +There is a default thread pool in case no integration is provided, but it shouldn't be used outside of tests or experiments. +This is provided by the `AsyncExecutorProvider` default implementation, `DefaultAsyncExecutorProvider`. + +Integrators that are themselves Jakarta EE application servers and don't want to expose their own thread pool can subclass `DefaultAsyncExecutorProvider`. +This at least allows customizing the `ThreadFactory`. + +== Configuration + +If the integrator doesn't provide a thread pool, the default thread pool is used as described above. +In such case, the following configuration properties are recognized: + +- `io.smallrye.faulttolerance.mainThreadPoolSize`: maximum number of threads in the thread pool. +Defaults to `100`. + +- `io.smallrye.faulttolerance.mainThreadPoolQueueSize`: size of the queue that the thread pool should use. +Can be `> 0`, in which case the queue will be bounded to given size. +Can be `0`, in which case there will be no queue, and the thread pool will reject excess tasks. +Can be `-1`, in which case the queue will be unbounded. +Values `< -1` are an error. +Defaults to `-1`. diff --git a/content/docs/smallrye-fault-tolerance/6/internals/config.adoc b/content/docs/smallrye-fault-tolerance/6/internals/config.adoc new file mode 100644 index 000000000..6e505c7b2 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/6/internals/config.adoc @@ -0,0 +1,119 @@ += Configuration System +:version-root: ../ +include::{version-root}_attributes.adoc[] + +include::{partialsdir}/internals.adoc[] + +The xref:internals/core.adoc[core] fault tolerance strategies are configured simply by constructor parameters. +This is fine, since they are an internal implementation detail. +However, the constructor parameters correspond closely to the members of {microprofile-fault-tolerance} annotations. +The idea is that core fault tolerance strategies have no dependency on the {microprofile-fault-tolerance} API, but given a fault tolerance annotation, creating an instance of the corresponding strategy should be straightforward. + +On top of the core layer, {smallrye-fault-tolerance} implements {microprofile-fault-tolerance}, including a configuration system conforming to the specification. +This document briefly describes how the configuration system works. + +== `FaultToleranceOperation` + +The fault tolerance interceptor, which is the main entrypoint, looks up a `FaultToleranceOperation` instance for each class/method combination it has to process. +The `FaultToleranceOperation` holds a bean class, a descriptor of the guarded method, and configuration for each fault tolerance strategy applied to the method. +It also orchestrates configuration validation. +The interceptor uses a `FaultToleranceOperation` to instantiate all the core fault tolerance strategies for given guarded method. + +The `FaultToleranceOperation` instances are created early on, during application deployment, for all beans that use {microprofile-fault-tolerance}. +It may happen that a `FaultToleranceOperation` is created lazily, for special constructs such as runtime-built proxies, but that is largely irrelevant from the configuration perspective. + +== `FaultToleranceMethod` + +A `FaultToleranceOperation` is created from a `FaultToleranceMethod`. +A `FaultToleranceMethod` holds, again, a bean class, a descriptor of the guarded method, and the annotation instance for each annotation that is present on the guarded method or the class declaring the method. +It also knows what annotations have been declared directly on the method, and what annotations have been declared on the class. +This is because the {microprofile-fault-tolerance} configuration rules distinguish these two cases. + +A `FaultToleranceMethod` must be created in a CDI extension, because it represents the knowledge CDI has about the guarded method. +Note that CDI extensions may add, remove or modify annotations, so `FaultToleranceMethod` doesn't just hold the annotation instances as present in the class bytecode. + +.Quarkus +**** +In environments that bootstrap the CDI container during application build, such as Quarkus, the set of all ``FaultToleranceMethod``s in the application should be collected during build and transferred to runtime. +Then, at application runtime, ``FaultToleranceOperation``s should be created and validated. + +{smallrye-fault-tolerance} doesn't do anything like that, but is designed with Quarkus (and CDI Lite) in mind. +**** + +The main difference between `FaultToleranceMethod` and `FaultToleranceOperation` is that `FaultToleranceMethod` holds annotation instances, while `FaultToleranceOperation` holds configuration. + +== `@AutoConfig` + +As described above, `FaultToleranceMethod` holds annotation instances, while `FaultToleranceOperation` holds configuration. +Yet, `FaultToleranceOperation` exposes the annotation types (such as `@CircuitBreaker`) to anyone who needs the configuration. +The configuration consumers simply call the annotation methods (such as `requestVolumeThreshold()`), and configuration is handled behind the scenes, following the {microprofile-fault-tolerance} configuration rules. + +The infrastructure behind this is present in the `autoconfig` module. +This module allows creating simple configuration interfaces like this: + +[source,java] +---- +@AutoConfig +interface CircuitBreakerConfig extends CircuitBreaker, Config { + default void validate() { + ... + } +} +---- + +The config interface must: + +. be annotated with `@AutoConfig`; +. extend the annotation interface (`CircuitBreaker` in this case); +. extend the `Config` (or `ConfigDeclarativeOnly`) interface (from the `autoconfig/core` module); +. provide a `default` implementation of the `validate` method (defined in `Config`). + +For each such interface, an implementation class will be automatically generated by an annotation processor (implemented in the `autoconfig/processor` module). +In this case, it would be called `CircuitBreakerConfigImpl`. + +This implementation class has a `static` method called `create()` that accepts a `FaultToleranceMethod`. +It follows that for each guarded method, a new instance must be created. +If given `FaultToleranceMethod` doesn't contain an instance of particular annotation, `create()` simply returns `null`. + +The implementation class also overrides all methods from the annotation type. +Each annotation method is implemented to first consult MicroProfile Config, and only second to consult the annotation instance (which, as we described above, is present on the `FaultToleranceMethod`). + +The implementation class is supposed to be used just like the annotation type itself. +In other words, to obtain configured values, call the annotation methods on the config interface instance. +For example, to obtain the `requestVolumeThreshold` value, with respect to the {microprofile-fault-tolerance} configuration rules, we obtain an instance of `CircuitBreakerConfigImpl` and call `requestVolumeThreshold()`. + +The `FaultToleranceOperation` class holds instances of these config interfaces, and exposes them to configuration consumers. +It actually only exposes the annotation types, not the full config interface, but that is enough. + +== Putting it all together + +Let's recap how it all works. + +First, for each guarded method in the application, a `FaultToleranceMethod` instance is created, holding all the annotations that affect the method. + +Second, for each `FaultToleranceMethod`, a `FaultToleranceOperation` is created. +That, in turn, creates an instance of all relevant config interfaces. + +Finally, the fault tolerance interceptor looks up a `FaultToleranceOperation` and creates a chain of fault tolerance strategies. +For each fault tolerance strategy in the chain, the interceptor looks up configuration values from the corresponding config interface. + +The chain of fault tolerance strategies is created on the first invocation to a given guarded method and cached for all subsequent invocations. +That is how we satisfy the requirement that all stateful fault tolerance strategies are singletons, but that's a different story. + +== Supplement: Programmatic API + +The description above is slightly simplified for readability. +It is precise enough to describe how configuration works in the declarative, annotation-based API. + +The programmatic API itself supports configuration when used together with `@ApplyGuard`. +However, it doesn't support configuring _all_ strategies; some are built in a different manner and no configuration is possible. + +Therefore, the `FaultToleranceOperation` class described above is actually split into 2 classes: + +* `BasicFaultToleranceOperation`, which is shared for both configuration systems and holds all `Config` objects, +* its subclass `FaultToleranceOperation`, which only applies in case of the declarative API and holds all `ConfigDeclarativeOnly` objects. + +In addition to the description above, implementations of the `Config` interface (but not `ConfigDeclarativeOnly`) also include a `create()` method that takes a `String` identifier and a `Supplier` of the backing annotation instance. +This is supposed to be used to create an instance for configuring the programmatic API, because there's no `FaultToleranceMethod` in such case. +Also, a zero-configuration implementation of the `Config` interface is generated, called `...NoConfigImpl`. +This exists to be able to use the config interface even in the non-`@ApplyGuard` case, where no configuration is possible. diff --git a/content/docs/smallrye-fault-tolerance/6/internals/core.adoc b/content/docs/smallrye-fault-tolerance/6/internals/core.adoc new file mode 100644 index 000000000..271f108a9 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/6/internals/core.adoc @@ -0,0 +1,123 @@ += Fault Tolerance Core +:version-root: ../ +include::{version-root}_attributes.adoc[] + +include::{partialsdir}/internals.adoc[] + +The core idea in {smallrye-fault-tolerance} is a `FaultToleranceStrategy`. +It is an interface that looks like this: + +[source,java] +---- +interface FaultToleranceStrategy { + Future apply(FaultToleranceContext ctx); +} +---- + +NOTE: The `Future` type here is _not_ a `java.util.concurrent.Future`. +It comes from {smallrye-fault-tolerance} and it could be described as a very bare-bones variant of `CompletableFuture`, except it's split into `Completer` and `Future`. +It is not supposed to be used outside of this project. + +The `FaultToleranceContext` is similar to a `Callable>`; it represents the method invocation guarded by this fault tolerance strategy. +The fault tolerance strategy does its work around `ctx.call()`. +It can catch exceptions, invoke `ctx.call()` multiple times, invoke something else, etc. +As an example, let's consider this strategy, applicable to methods that return a `String`: + +[source,java] +---- +public class MyStringFallback implements FaultToleranceStrategy { + @Override + public Future apply(FaultToleranceContext ctx) { + Completer completer = Completer.create(); + try { + ctx.call().then((value, error) -> { + if (error == null) { + completer.complete(value); + } else { + completer.complete("my string value"); + } + }); + } catch (Exception ignored) { + completer.complete("my string value"); + } + return completer.future(); + } +} +---- + +This is a very simple fallback mechanism, which returns a pre-defined value in case of an exception. + +In the {smallrye-fault-tolerance} codebase, you can find implementations of all the strategies required by {microprofile-fault-tolerance}: retry, fallback, timeout, circuit breaker or bulkhead. +Asynchronous invocation, delegated to a thread pool, is of course also supported. + +When multiple fault tolerance strategies are supposed to be used to guard one method, they form a chain. +Continuing with our simple example, adding the ability to chain with another strategy would look like this: + +[source,java] +---- +public class MyStringFallback implements FaultToleranceStrategy { + private final FaultToleranceStrategy delegate; + + public MyStringFallback(FaultToleranceStrategy delegate) { + this.delegate = delegate; + } + + @Override + public Future apply(FaultToleranceContext ctx) { + Completer completer = Completer.create(); + try { + delegate.apply(ctx).then((value, error) -> { + if (error == null) { + completer.complete(value); + } else { + completer.complete("my string value"); + } + }); + } catch (Exception ignored) { + completer.complete("my string value"); + } + return completer.future(); + } +} +---- + +We see that one strategy delegates to another, passing the `FaultToleranceContext` along. +In fact, all the implementations in {smallrye-fault-tolerance} are written like this: they expect to be used in a chain, so they take another `FaultToleranceStrategy` to which they delegate. +But if all strategies have this form, when is `ctx.call()` actually invoked? +Good question! +The ultimate `ctx.call()` invocation is done by a special fault tolerance strategy which is called, well, `Invocation`. + +As an example which uses real {microprofile-fault-tolerance} annotations, let's consider this method: + +[source,java] +---- +@Retry(...) +@Timeout(...) +@Fallback(...) +public void doSomething() { + ... +} +---- + +The chain of fault tolerance strategies will look roughly like this: + +[source] +---- +Fallback( + Retry( + Timeout( + Invocation( + // ctx.call() will happen here + // that will, in turn, invoke doSomething() + ) + ) + ) +) +---- + +The order in which the strategies are chained (or, in fact, nested) is specified by {microprofile-fault-tolerance}. + +Other strategies might also be present in more complex cases. +For example, if metrics are enabled, a special strategy is used that collects metrics. +If the method is `@Asynchronous`, a special strategy is used that offloads method execution to another thread. +Etc. diff --git a/content/docs/smallrye-fault-tolerance/6/internals/instructions.adoc b/content/docs/smallrye-fault-tolerance/6/internals/instructions.adoc new file mode 100644 index 000000000..2b275e995 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/6/internals/instructions.adoc @@ -0,0 +1,21 @@ += Instructions +:version-root: ../ +include::{version-root}_attributes.adoc[] + +To compile this project and install the artifacts into local Maven repository, run: + +[source,bash] +---- +mvn clean install +---- + +This will also run all the tests and the TCK. + +You can use + +[source,bash] +---- +mvn clean install -DskipTests +---- + +to skip the tests and make the build a lot faster. diff --git a/content/docs/smallrye-fault-tolerance/6/internals/logging.adoc b/content/docs/smallrye-fault-tolerance/6/internals/logging.adoc new file mode 100644 index 000000000..bacfff5ec --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/6/internals/logging.adoc @@ -0,0 +1,16 @@ += Logging +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Logging in {smallrye-fault-tolerance} is based on https://jboss-logging.github.io/jboss-logging-tools/[JBoss Logging Tools] (see also https://github.com/smallrye/smallrye/wiki/I18N-Messages-and-Logging[SmallRye instructions]). +It's an annotation processor that takes annotated interfaces and generates logger implementations with out of the box support for message codes and internationalization. +We use a project code of `SRFTL`. +Message codes are divided into ranges that are defined in the `message-ranges.txt` file in the root directory. + +In addition to logging, JBoss Logging Tools can also be used for creating exceptions, again, with out of the box support for message codes and internationalization. +Contrary to both documentation sites mentioned above, it _is_ possible to mix logging and exceptions in a single interface, as demonstrated by `CdiLogger`. +However, {smallrye-fault-tolerance} doesn't use this feature yet, with the exception of a few methods in `CdiLogger`. +This should be rectified in the future. + +Note that `DEBUG` and `TRACE` logging doesn't need message codes or internationalization, so we usually don't declare logger methods for them, unless the log message requires parameters. +We still declare the logger interface, let it extend `BasicLogger` and use the singleton instance to log plain ``String``s. diff --git a/content/docs/smallrye-fault-tolerance/6/internals/project-structure.adoc b/content/docs/smallrye-fault-tolerance/6/internals/project-structure.adoc new file mode 100644 index 000000000..4006ad846 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/6/internals/project-structure.adoc @@ -0,0 +1,39 @@ += Project Structure +:version-root: ../ +include::{version-root}_attributes.adoc[] + +* `api`: {smallrye-fault-tolerance} API, additional to the {microprofile-fault-tolerance} API. + Treated as a public API. +* `implementation/core`: Implementation of core fault tolerance strategies. + Independent of the {microprofile-fault-tolerance} API or the {smallrye-fault-tolerance} API. + Treated as private API, no compatibility guaranteed. +* `implementation/apiimpl`: Implementation of the programmatic API. + Based on the `core` module. + Treated as private API, no compatibility guaranteed. +* `implementation/fault-tolerance`: Implementation of the {microprofile-fault-tolerance} API and the {smallrye-fault-tolerance} API. + Based on CDI and the `core` module. + The configuration system is based on the `autoconfig` module. +* `implementation/autoconfig/core`: Core classes of the {smallrye-fault-tolerance} configuration system. + Required at runtime. +* `implementation/autoconfig/processor`: An annotation processor that generates implementations of config interfaces. + Only required during {smallrye-fault-tolerance} build. +* `implementation/context-propagation`: Optional integration with MicroProfile Context Propagation. + See xref:integration/context-propagation.adoc[Context Propagation integration]. +* `implementation/kotlin`: Optional integration with Kotlin. + See xref:integration/kotlin.adoc[Kotlin Integration Concerns]. +* `implementation/mutiny`: Optional integration with Mutiny. + See xref:integration/async-types.adoc[Additional Asynchronous Types Integration Concerns]. +* `implementation/rxjava3`: Optional integration with RxJava 3. + See xref:integration/async-types.adoc[Additional Asynchronous Types Integration Concerns]. +* `implementation/standalone`: Standalone implementation of the {smallrye-fault-tolerance} programmatic API. + See xref:integration/programmatic-api.adoc[Programmatic API Integration Concerns]. +* `implementation/tracing-propagation`: Optional integration between MicroProfile Context Propagation and OpenTracing. + See xref:integration/opentracing.adoc[OpenTracing integration]. +* `implementation/vertx`: Optional integration of the Vert.x event loop and Vert.x `Future`. + See xref:integration/event-loop.adoc[Event Loop integration] and xref:integration/async-types.adoc[Additional Asynchronous Types Integration Concerns]. +* `testsuite/basic`: Basic test suite. + Some parts of it overlap with the Eclipse MicroProfile Fault Tolerance TCK. +* `testsuite/integration`: Integration test suite. + Includes several Context Propagation and Vert.x tests. +* `testsuite/tck`: Runs the implementation against the Eclipse MicroProfile Fault Tolerance TCK. +* `doc`: This documentation. diff --git a/content/docs/smallrye-fault-tolerance/6/reference/asynchronous.adoc b/content/docs/smallrye-fault-tolerance/6/reference/asynchronous.adoc new file mode 100644 index 000000000..64507ae60 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/6/reference/asynchronous.adoc @@ -0,0 +1,277 @@ += Asynchronous Execution +:version-root: ../ +include::{version-root}_attributes.adoc[] + +For introduction, see xref:howto/asynchronous.adoc[How to Guard Asynchronous Methods]. +The how-to guide also explains what _asynchronous fault tolerance_ is and when does it apply. + +== Description + +=== `@Asynchronous` + +This annotation may only be placed on methods that return `java.util.concurrent.Future` or java.util.concurrent.`CompletionStage`. +It may also be placed on a class, in which case all business methods must return `Future` or `CompletionStage`. + +The method call is offloaded to another thread. +The thread pool that is used for offloading method calls is provided by the runtime that integrates {smallrye-fault-tolerance}. + +=== `@AsynchronousNonBlocking` + +include::{partialsdir}/srye-feature.adoc[] + +In addition to the {microprofile-fault-tolerance} `@Asynchronous` annotation, {smallrye-fault-tolerance} offers an annotation for asynchronous non-blocking processing: `@AsynchronousNonBlocking`. + +This annotation may only be placed on methods that return `CompletionStage`, because the `Future` type cannot really be used for non-blocking processing. +It may also be placed on a class, in which case all business methods must return `CompletionStage`. + +As opposed to `@Asynchronous`, the `@AsynchronousNonBlocking` annotation does _not_ offload the method execution to another thread. +The method is allowed to proceed on the original thread, assuming that it returns quickly and all processing it does is performed in a non-blocking way. + +=== `@Asynchronous` and `@AsynchronousNonBlocking` Compared + +|=== +|Annotation |Method return type |Execution + +|`@Asynchronous` +|`Future` or `CompletionStage` +|offloaded to a thread pool + +|`@AsynchronousNonBlocking` +|`CompletionStage` +|proceeds on the original thread +|=== + +=== `@Asynchronous` and `@AsynchronousNonBlocking` Combined + +When these annotations are combined, an annotation on a method has priority over an annotation on a class. +For example: + +[source,java] +---- +@ApplicationScoped +@Asynchronous +public class MyService { + @Retry + CompletionStage hello() { // <1> + ... + } + + @Retry + @AsynchronousNonBlocking + CompletionStage helloBlocking() { // <2> + ... + } +} +---- + +<1> Treated as `@Asynchronous`, based on the class annotation. +<2> Treated as `@AsynchronousNonBlocking`, the method annotation has priority over the class annotation. + +It is an error to put both `@Asynchronous` and `@AsynchronousNonBlocking` on the same program element. + +[[recommendation]] +=== Recommendation + +Use the <>. +In this mode, methods returning `CompletionStage` are automatically treated as if they were `@AsynchronousNonBlocking`. +Use `@Asynchronous` to mark blocking methods for thread offload. + +If you can't use the non-compatible mode, use `@AsynchronousNonBlocking` or `@Asynchronous` to mark all asynchronous methods. + +IMPORTANT: In previous releases, {smallrye-fault-tolerance} recommended to use the `@Blocking` and `@NonBlocking` annotations. +Using these annotations *for fault tolerance purposes* is now deprecated. +They are still supported, but at some point, {smallrye-fault-tolerance} will stop recognizing them. + +We also recommend avoiding `@Asynchronous` methods that return `Future`, because the only way to obtain the future value is blocking. + +[[interactions]] +== Interactions with Other Strategies + +See xref:howto/multiple.adoc[How to Use Multiple Strategies] for an overview of how fault tolerance strategies are nested. + +=== `@Asynchronous` + +`@Asynchronous` does not interact with other fault tolerance strategies, with the obvious exception that it moves execution to another thread. + +=== `@AsynchronousNonBlocking` + +If the guarded method uses `@Retry` and some delay between retries is configured, only the initial execution is guaranteed to occur on the original thread. +Subsequent attempts may be offloaded to an extra thread, so that the original thread is not blocked on the delay. + +If the guarded method uses `@Bulkhead`, the execution is _not_ guaranteed to occur on the original thread. +If the execution has to wait in the bulkhead queue, it may later end up on a different thread. + +If the original thread is an event loop thread and event loop integration is enabled, then the event loop is always used to execute the guarded method. +In such case, all retry attempts and queued bulkhead executions are guaranteed to happen on the original thread. + +[[configuration]] +== Configuration + +There is no configuration for `@Asynchronous` or `@AsynchronousNonBlocking`. + +[[metrics]] +== Metrics + +Asynchronous execution does not emit any metrics. + +See xref:reference/metrics.adoc[the Metrics reference guide] for general metrics information. + +== Extra Features + +[[async-types]] +=== Additional Asynchronous Types + +include::{partialsdir}/srye-feature.adoc[] + +{microprofile-fault-tolerance} supports asynchronous fault tolerance for methods that return `CompletionStage`. +(The `java.util.concurrent.Future` type is not truly asynchronous, so we won't take it into account here.) +{smallrye-fault-tolerance} adds support for additional asynchronous types: + +* Mutiny: `Uni` +* RxJava 3: `Single`, `Maybe`, `Completable` +* Vert.x: `io.vertx.core.Future` + +These types are treated just like `CompletionStage`, so everything that works for `CompletionStage` works for these types as well. +Stream-like types (`Multi`, `Observable`, `Flowable`) are not supported, because their semantics can't be easily expressed in terms of `CompletionStage`. + +For example: + +[source,java] +---- +@ApplicationScoped +public class MyService { + @Retry + @AsynchronousNonBlocking // <1> + Uni hello() { // <2> + ... + } +} +---- + +<1> Using the `@AsynchronousNonBlocking` annotation, because the method doesn't block and offloading execution to another thread is not necessary. +<2> Returning the `Uni` type from Mutiny. +This shows that whatever works for `CompletionStage` also works for the other async types. + +The implementation internally converts the async types to a `CompletionStage` and back. +This means that to be able to use any particular asynchronous type, the corresponding converter must be present. +{smallrye-fault-tolerance} provides support libraries for popular asynchronous types, and these support libraries include the corresponding converters. + +It is possible that the runtime you use already provides the correct integration. +Otherwise, add a dependency to your application: + +* https://smallrye.io/smallrye-mutiny/[Mutiny]: `io.smallrye:smallrye-fault-tolerance-mutiny` +* https://github.com/ReactiveX/RxJava/tree/3.x[RxJava 3]: `io.smallrye:smallrye-fault-tolerance-rxjava3` +* https://vertx.io/docs/vertx-core/java/#_future_results[Vert.x `Future`]: `io.smallrye:smallrye-fault-tolerance-vertx` + +.Quarkus +**** +In Quarkus, the Mutiny and Vert.x support libraries are present by default. +You can use fault tolerance on methods that return `Uni` (and Vert.x `Future`) out of the box. +**** + +[[kotlin-suspend-functions]] +=== Kotlin `suspend` Functions + +include::{partialsdir}/srye-feature.adoc[] + +{smallrye-fault-tolerance} includes support for Kotlin suspending functions. +They are treated as <>, even though the internal implementation is more complex than support for Mutiny or RxJava 3. + +For example: + +[source,kotlin] +---- +@ApplicationScoped +open class MyService { + @Retry(maxRetries = 2) + @Fallback(fallbackMethod = "helloFallback") + open suspend fun hello(): String { // <1> + delay(100) + throw IllegalArgumentException() + } + + private suspend fun helloFallback(): String { // <2> + delay(100) + return "hello" + } +} +---- + +<1> As a suspending function, this method can only be called from another suspending function. +It will be guarded by the retry and fallback strategies, as defined using the annotations. +<2> Similarly to fallback methods in Java, fallback methods in Kotlin must have the same signature as the guarded method. +Since the guarded method is suspending, the fallback method must be suspending. + +As mentioned above, suspending functions are treated as async types. +This means that for asynchronous fault tolerance to work correctly on suspending functions, they must be determined to be asynchronous. +That happens automatically in the <>, based on the method signature, but if you use strictly compatible mode, the `@Asynchronous` or `@AsynchronousNonBlocking` annotation must be present. +It is expected that most users will use the Kotlin support in the non-compatible mode, so the example above does not include any such annotation. + +To be able to use this, a support library must be present. +It is possible that the runtime you use already provides the correct integration. +Otherwise, add a dependency to your application: `io.smallrye:smallrye-fault-tolerance-kotlin`. + +.Quarkus +**** +In Quarkus, the Kotlin support library is present by default, if you use the Quarkus Kotlin support. +You can declare fault tolerance annotations on suspending methods out of the box. +**** + +==== Programmatic API + +Suspending functions are currently only supported in the declarative, annotation-based API, as shown in the example above. +The xref:reference/programmatic-api.adoc[Programmatic API] of {smallrye-fault-tolerance} does not support suspending functions, but other than that, it can of course be used from Kotlin through its Java interop. + +[[method-asynchrony]] +=== Determining Asynchrony from Method Signature + +include::{partialsdir}/srye-feature.adoc[] + +include::{partialsdir}/non-compat.adoc[] + +In the non-compatible mode, method asynchrony is determined solely from its signature. +That is, methods that + +* have some fault tolerance annotation (such as `@Retry`), +* return `CompletionStage` (or some other <>), + +always have asynchronous fault tolerance applied. + +For example: + +[source,java] +---- +@ApplicationScoped +public class MyService { + @Retry + CompletionStage hello() { // <1> + ... + } + + @Retry + Uni helloMutiny() { // <2> + ... + } + + @Retry + @Asynchronous + CompletionStage helloBlocking() { // <3> + ... + } +} +---- + +<1> Executed on the original thread, because the method returns `CompletionStage`. +It is as if the method was annotated `@AsynchronousNonBlocking`. +<2> Executed on the original thread, because the method returns an <>. +It is as if the method was annotated `@AsynchronousNonBlocking`. +<3> The explicit `@Asynchronous` annotation is honored. +The method is executed on a thread pool. + +Note that the existing annotations still work without a change, both in compatible and non-compatible mode. +That is, if a method (or class) is annotated `@Asynchronous`, execution will be offloaded to a thread pool. +If a method (or class) is annotated `@AsynchronousNonBlocking`, execution will happen on the original thread. + +Also note that this doesn't affect methods returning `java.util.concurrent.Future`. +You still have to annotate them `@Asynchronous` to make sure they are executed on a thread pool and are guarded properly. +As mentioned in the <>, we discourage using these methods, because the only way to obtain the future value is blocking. diff --git a/content/docs/smallrye-fault-tolerance/6/reference/bulkhead.adoc b/content/docs/smallrye-fault-tolerance/6/reference/bulkhead.adoc new file mode 100644 index 000000000..a12b417d5 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/6/reference/bulkhead.adoc @@ -0,0 +1,143 @@ += Bulkhead +:version-root: ../ +include::{version-root}_attributes.adoc[] + +For introduction, see xref:howto/bulkhead.adoc[How to Limit Concurrency]. + +== Description + +If a guarded method is called by too many callers concurrently, the excess attempts will fail with `BulkheadException`. + +If the guarded method is `@Asynchronous` or `@AsynchronousNonBlocking`, excess attempts are first put into a queue. +The queued attempts are executed later. +When the queue becomes full, excess attempts are rejected with `BulkheadException`. + +=== Lifecycle + +Bulkhead needs to maintain some state between invocations: the number of current executions and the queue of asynchronous executions. +This state is a singleton, irrespective of the lifecycle of the bean that uses the `@Bulkhead` annotation. + +More specifically, the bulkhead state is uniquely identified by the combination of the bean class (`java.lang.Class`) and the method object (`java.lang.reflect.Method`) representing the guarded method. + +For example, if there’s a guarded method `doWork` on a bean which is `@RequestScoped`, each request will have its own instance of the bean, but all invocations of `doWork` will share the same bulkhead state. + +[[interactions]] +== Interactions with Other Strategies + +See xref:howto/multiple.adoc[How to Use Multiple Strategies] for an overview of how fault tolerance strategies are nested. + +If `@Fallback` is used with `@Bulkhead`, the fallback method or handler may be invoked if a `BulkheadException` is thrown, depending on the fallback configuration. + +If `@Retry` is used with `@Bulkhead`, each retry attempt is processed by the bulkhead as an independent invocation. +If `BulkheadException` is thrown, the execution may be retried, depending on how retry is configured. + +If `@CircuitBreaker` is used with `@Bulkhead`, the circuit breaker is checked before enforcing the concurrency limit. +If concurrency limiting results in `BulkheadException`, this may be counted as a failure, depending on how the circuit breaker is configured. + +If `@RateLimit` is used with `@Bulkhead`, the rate limit is enforced before enforcing the concurrency limit. + +If `@Timeout` is used with `@Bulkhead`, the timeout watcher is started before enforcing the concurrency limit. + +[[configuration]] +== Configuration + +There are 2 configuration options, corresponding to the 2 members of the `@Bulkhead` annotation. + +=== `value` + +Type: `int` + +Default: `10` + +The concurrency limit. + +=== `waitingTaskQueue` + +Type: `int` + +Default: `10` + +The size of queue for excess attempts. +Only effective when the method is `@Asynchronous` or `@AsynchronousNonBlocking`. +See xref:reference/asynchronous.adoc[the Asynchronous Execution reference guide]. + +[[metrics]] +== Metrics + +Bulkhead exposes the following metrics: + +[cols="1,5"] +|=== +| Name | `ft.bulkhead.calls.total` +| Type +a| * MP Metrics: `Counter` +* OpenTelemetry: `LongCounter` +* Micrometer: `Counter` +| Unit | None +| Description | The number of times the bulkhead logic was run. This is usually once per method call, but may be zero times if the circuit breaker or rate limit prevented execution or more than once if the method call was retried. +| Tags +a| * `method` - the fully qualified method name +* `bulkheadResult` = `[accepted\|rejected]` - whether the bulkhead allowed the method call to run +|=== + +[cols="1,5"] +|=== +| Name | `ft.bulkhead.executionsRunning` +| Type +a| * MP Metrics: `Gauge` +* OpenTelemetry: `LongUpDownCounter` +* Micrometer: `Gauge` +| Unit | None +| Description | Number of currently running executions. +| Tags +a| * `method` - the fully qualified method name +|=== + +[cols="1,5"] +|=== +| Name | `ft.bulkhead.executionsWaiting` +| Type +a| * MP Metrics: `Gauge` +* OpenTelemetry: `LongUpDownCounter` +* Micrometer: `Gauge` +| Unit | None +| Description | Number of executions currently waiting in the queue. +| Tags +a| * `method` - the fully qualified method name +| Notes | Only added if the method is also `@Asynchronous` or `@AsynchronousNonBlocking` +|=== + +[cols="1,5"] +|=== +| Name | `ft.bulkhead.runningDuration` +| Type +a| * MP Metrics: `Histogram` +* OpenTelemetry: `DoubleHistogram` with explicit bucket boundaries `[0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10]` +* Micrometer: `Timer` +| Unit +a| * MP Metrics: nanoseconds +* OpenTelemetry: seconds +* Micrometer: nanoseconds +| Description | Histogram of the time that method executions spent running. +| Tags +a| * `method` - the fully qualified method name +|=== + +[cols="1,5"] +|=== +| Name | `ft.bulkhead.waitingDuration` +| Type +a| * MP Metrics: `Histogram` +* OpenTelemetry: `DoubleHistogram` with explicit bucket boundaries `[0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10]` +* Micrometer: `Timer` +| Unit +a| * MP Metrics: nanoseconds +* OpenTelemetry: seconds +* Micrometer: nanoseconds +| Description | Histogram of the time that method executions spent waiting in the queue. +| Tags +a| * `method` - the fully qualified method name +| Notes | Only added if the method is also `@Asynchronous` or `@AsynchronousNonBlocking` +|=== + +See xref:reference/metrics.adoc[the Metrics reference guide] for general metrics information. diff --git a/content/docs/smallrye-fault-tolerance/6/reference/circuit-breaker.adoc b/content/docs/smallrye-fault-tolerance/6/reference/circuit-breaker.adoc new file mode 100644 index 000000000..8ed75b2a2 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/6/reference/circuit-breaker.adoc @@ -0,0 +1,287 @@ += Circuit Breaker +:version-root: ../ +include::{version-root}_attributes.adoc[] + +For introduction, see xref:howto/circuit-breaker.adoc[How to Fail Fast on Recurring Failures]. + +== Description + +The circuit breaker is a simple state machine: + +image::images/diagrams/mermaid-f40d3cbcabca.svg[circuit-breaker] + +The circuit breaker starts _closed_. +In this state, the circuit breaker maintains a rolling window of recent invocations. +For each invocation, the rolling window tracks whether it finished successfully or failed. + +The rolling window must be full to take any state transition decision. +For example, if the rolling window has size 10, a closed circuit breaker always allows at least 10 invocations. + +If the rolling window contains a number of failures higher than the configured ratio, a closed circuit breaker moves to _open_. +When the circuit breaker is open, invocations are not allowed to proceed. +Instead, the circuit breaker fails fast and throws `CircuitBreakerOpenException`. + +For example, if the rolling window has size 10 and the failure rate is 0.5, it means that 5 invocations out of the most recent 10 invocations must fail for the circuit breaker to move to _open_. + +After some time, an open circuit breaker moves to _half-open_ to determine whether failing fast is still appropriate. +A half-open circuit breaker allows some configured number of probe attempts to proceed. +If all of them succeed, the circuit breaker moves to _closed_ and invocations are allowed again. +If some probe invocations fail, the circuit breaker moves back to _open_ and invocations are prevented. + +Successes and failures are determined from the method result. +More specifically: + +. If the guarded method returns normally, it is treated as success. +. Otherwise, if the guarded method throws an exception whose type is assignable to any of the `skipOn` types (see <>), it is treated as success. +. Otherwise, if the guarded method throws an exception whose type is assignable to any of the `failOn` types (see <>), it is treated as failure. +. Otherwise, it is treated as success. + +=== Lifecycle + +Circuit breaker needs to maintain some state between invocations: the number of recent successful invocations, the number of recent failed invocations, and so on. +This state is a singleton, irrespective of the lifecycle of the bean that uses the `@CircuitBreaker` annotation. + +More specifically, the circuit breaker state is uniquely identified by the combination of the bean class (`java.lang.Class`) and the method object (`java.lang.reflect.Method`) representing the guarded method. + +For example, if there’s a guarded method `doWork` on a bean which is `@RequestScoped`, each request will have its own instance of the bean, but all invocations of `doWork` will share the same circuit breaker state. + +[[interactions]] +== Interactions with Other Strategies + +See xref:howto/multiple.adoc[How to Use Multiple Strategies] for an overview of how fault tolerance strategies are nested. + +If `@Fallback` is used with `@CircuitBreaker`, the fallback method or handler may be invoked if a `CircuitBreakerOpenException` is thrown, depending on the fallback configuration. + +If `@Retry` is used with `@CircuitBreaker`, each retry attempt is processed by the circuit breaker as an independent invocation. +If `CircuitBreakerOpenException` is thrown, the execution may be retried, depending on how retry is configured. + +[[configuration]] +== Configuration + +There are 7 configuration options, corresponding to the 7 members of the `@CircuitBreaker` annotation. + +=== `requestVolumeThreshold` + +Type: `int` + +Default: `20` + +The size of the rolling window. +That is, the number of recent consecutive invocations tracked by a _closed_ circuit breaker. + +=== `failureRatio` + +Type: `double` + +Default: `0.5` + +The ratio of failures in the rolling window that causes a _closed_ circuit breaker to move to _open_. + +=== `delay` + `delayUnit` + +Type: `long` + `ChronoUnit` + +Default: `5000 millis`, or 5 seconds + +The delay after which an _open_ circuit breaker moves to _half-open_. + +=== `successThreshold` + +Type: `int` + +Default: `1` + +The number of probe invocations allowed when the circuit breaker is _half-open_. +If they all succeed, the circuit breaker moves to _closed_, otherwise it moves back to _open_. + +[[fail-on]] +=== `failOn` + +Type: `Class[]` + +Default: `{Throwable.class}` + +Set of exception types that the circuit breaker considers failures. + +[[skip-on]] +=== `skipOn` + +Type: `Class[]` + +Default: `{}` + +Set of exception types that the circuit breaker considers success. +This configuration takes precedence over `failOn`. + +[[metrics]] +== Metrics + +Circuit breaker exposes the following metrics: + +[cols="1,5"] +|=== +| Name | `ft.circuitbreaker.calls.total` +| Type +a| * MP Metrics: `Counter` +* OpenTelemetry: `LongCounter` +* Micrometer: `Counter` +| Unit | None +| Description | The number of times the circuit breaker logic was run. This is usually once per method call, but may be more than once if the method call is retried. +| Tags +a| * `method` - the fully qualified method name +* `circuitBreakerResult` = `[success\|failure\|circuitBreakerOpen]` - the result of the method call, as considered by the circuit breaker +** `success` - the method ran and was successful +** `failure` - the method ran and failed +** `circuitBreakerOpen` - the method did not run because the circuit breaker was in open or half-open state +|=== + +[cols="1,5"] +|=== +| Name | `ft.circuitbreaker.state.total` +| Type +a| * MP Metrics: `Gauge` +* OpenTelemetry: `LongCounter` +* Micrometer: `TimeGauge` +| Unit +a| * MP Metrics: nanoseconds +* OpenTelemetry: nanoseconds +* Micrometer: nanoseconds +| Description | Amount of time the circuit breaker has spent in each state +| Tags +a| * `method` - the fully qualified method name +* `state` = `[open\|closed\|halfOpen]` - the circuit breaker state +| Notes | Although this metric is a `Gauge`, its value increases monotonically. +|=== + +[cols="1,5"] +|=== +| Name | `ft.circuitbreaker.opened.total` +| Type +a| * MP Metrics: `Counter` +* OpenTelemetry: `LongCounter` +* Micrometer: `Counter` +| Unit | None +| Description | Number of times the circuit breaker has moved from closed state to open state +| Tags +a| * `method` - the fully qualified method name +|=== + +[cols="1,5"] +|=== +| Name | `ft.circuitbreaker.state.current` +2+a| +include::{partialsdir}/srye-feature.adoc[] +| Type +a| * MP Metrics: `Gauge` +* OpenTelemetry: `LongUpDownCounter` +* Micrometer: `Gauge` +| Unit | None +| Description | Whether the circuit breaker is currently in given state (`1`) or not (`0`) +| Tags +a| * `method` - the fully qualified method name +* `state` = `[open\|closed\|halfOpen]` - the circuit breaker state +|=== + +See xref:reference/metrics.adoc[the Metrics reference guide] for general metrics information. + +== Extra Features + +=== Circuit Breaker Maintenance + +include::{partialsdir}/srye-feature.adoc[] + +It is sometimes useful to see the circuit breaker status from within the application, or reset it to the initial state. +This is possible in two steps: + +. Give the circuit breaker a name by annotating the guarded method with `@CircuitBreakerName`: ++ +[source,java] +---- +@ApplicationScoped +public class MyService { + @CircuitBreaker + @CircuitBreakerName("hello-cb") // <1> + public String hello() { + ... + } +} +---- ++ +<1> The circuit breaker guarding the `MyService.hello` method is given a name `hello-cb`. +. Inject `CircuitBreakerMaintenance` and call its methods: ++ +[source,java] +---- +@ApplicationScoped +public class Example { + @Inject + CircuitBreakerMaintenance maintenance; + + public void test() { + System.out.println("Circuit breaker state: " + + maintenance.currentState("hello-cb")); // <1> + maintenance.resetAll(); // <2> + } +} +---- ++ +<1> Obtains current circuit breaker state. +<2> Resets all circuit breakers to the initial state. + +The `CircuitBreakerMaintenance` interface provides 4 methods: + +. `currentState(name)`: returns current state of given circuit breaker. +The return type `CircuitBreakerState` is an `enum` with 3 values: `CLOSED`, `OPEN`, `HALF_OPEN`. +. `onStateChange(name, callback)`: registers a callback that will be called when given circuit breaker changes state. +. `reset(name)`: resets given circuit breaker to the initial state. +. `resetAll()`: resets all circuit breakers in the application to the initial state. + +See the javadoc of those methods for more information. + +[[inspecting-exception-cause-chains]] +=== Inspecting Exception Cause Chains + +include::{partialsdir}/srye-feature.adoc[] + +include::{partialsdir}/non-compat.adoc[] + +The `@CircuitBreaker` annotation can specify that certain exceptions should be treated as failures (`failOn`) and others as successes (`skipOn`). +The specification limits this to inspecting the actual exception that was thrown. +However, in many cases, exceptions are wrapped and the exception the user wants to decide on is only present in the cause chain. + +For that reason, in the non-compatible mode, if the actual thrown exception isn't known failure or known success, {smallrye-fault-tolerance} inspects the cause chain. +To be specific, in case a `@CircuitBreaker` method throws an exception, the decision process is: + +1. If the `skipOn` exceptions are not default and the exception is assignable to one of the `skipOn` exceptions, the circuit breaker treats it as a success. +2. Otherwise, if the `failOn` exceptions are not default and the exception is assignable to one of the `failOn` exceptions, the circuit breaker treats it as a failure. +3. Otherwise, if the exception is assignable to one of the `skipOn` exceptions or its cause chain contains an exception assignable to one of the `skipOn` exceptions, the circuit breaker treats it as a success. +4. Otherwise, if the exception is assignable to one of the `failOn` exceptions or its cause chain contains an exception assignable to one of the `failOn` exceptions, the circuit breaker treats it as a failure. +5. Otherwise, the exception is treated as a success. + +For example: + +[source,java] +---- +@CircuitBreaker(requestVolumeThreshold = 10, + skipOn = ExpectedOutcomeException.class, // <1> + failOn = IOException.class) // <2> +public Result doSomething() { + ... +} +---- + +<1> If `doSomething` throws an `ExpectedOutcomeException`, or a `WrapperException` whose cause is `ExpectedOutcomeException`, the circuit breaker treats it as a success. +<2> If `doSomething` throws an `IOException`, or a `WrapperException` whose cause is `IOException`, the circuit breaker treats it as a failure. + +[source,java] +---- +@CircuitBreaker(requestVolumeThreshold = 10, + skipOn = ExpectedOutcomeException.class) // <1> <2> +public Result doSomething() { + ... +} +---- + +<1> If `doSomething` throws an `ExpectedOutcomeException`, or a `WrapperException` whose cause is `ExpectedOutcomeException`, the circuit breaker treats it as a success. +<2> There's no `failOn`, so the 2nd step in the algorithm above is skipped. +This is what turns the `WrapperException` whose cause is `ExpectedOutcomeException` into a success. diff --git a/content/docs/smallrye-fault-tolerance/6/reference/config.adoc b/content/docs/smallrye-fault-tolerance/6/reference/config.adoc new file mode 100644 index 000000000..7a9288a12 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/6/reference/config.adoc @@ -0,0 +1,207 @@ += Configuration +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The fault tolerance strategies are primarily configured using annotations. +This is convenient, but changing such configuration requires recompilation. +For that reason, configuration using MicroProfile Config is also possible. + +NOTE: The configuration system of {microprofile-fault-tolerance}, as described in this guide, also applies to annotations defined by {smallrye-fault-tolerance}. + +== Annotation on Method + +The annotation may be present on a method: + +[source,java] +---- +package com.example; + +@ApplicationScoped +public class MyService { + @Retry(maxRetries = 10, retryOn = IOException.class) // <1> + public String hello() { + ... + } +} +---- + +<1> Retries will only be attempted if the thrown exception was `IOException`. +Other exceptions will be rethrown directly. +Also, the maximum number of retry attempts is changed to 10 (the default is 3). + +To change the `maxRetries` property using MicroProfile Config, set: + +[source,properties] +---- +smallrye.faulttolerance."com.example.MyService/hello".retry.max-retries=5 + +# alternatively, a specification-defined property can be used +com.example.MyService/hello/Retry/maxRetries=5 +---- + +== Annotation on Class + +The annotation may be present on a class, in which case it applies to all business method in the class: + +[source,java] +---- +package com.example; + +@ApplicationScoped +@Retry(maxRetries = 10, retryOn = IOException.class) // <1> +public class MyService { + public String hello() { + ... + } +} +---- +<1> The `hello()` method will use the `@Retry` strategy, as configured in this annotation. + +A class-level annotation may be overridden by explicitly annotating a method. + +The MicroProfile Config configuration property doesn't include the method name in this case: + +[source,properties] +---- +smallrye.faulttolerance."com.example.MyService".retry.max-retries=5 + +# alternatively, a specification-defined property can be used +com.example.MyService/Retry/maxRetries=5 +---- + +[IMPORTANT] +==== +Attempting to configure some property for a method or a class which does not have the annotation has no effect. + +* If the annotation is present on a method, the configuration property must target the method. +* If the annotation is present on a class, the configuration property must target the class. +==== + +== Global Configuration + +It is also possible to reconfigure some property for all annotations in the application. +For example: + +[source,properties] +---- +smallrye.faulttolerance.global.retry.max-retries=5 + +# alternatively, a specification-defined property can be used +Retry/maxRetries=5 +---- + +All `@Retry` annotations in the application will be considered to have `maxRetries` set to `5`. + +== Configuration Priority + +The most specific configuration has priority over more general configuration. + +For example: + +[source,java] +---- +package com.example; + +@ApplicationScoped +@Retry(maxRetries = 5) +public class MyService { + public String simpleHello() { + ... + } + + @Retry(maxRetries = 10) + public String complexHello() { + ... + } +} +---- + +The following configuration: + +[source,properties] +---- +smallrye.faulttolerance.global.retry.max-retries=30 +smallrye.faulttolerance."com.example.MyService".retry.max-retries=15 +smallrye.faulttolerance."com.example.MyService/complexHello".retry.max-retries=20 + +# alternatively, specification-defined properties can be used +Retry/maxRetries=30 +com.example.MyService/Retry/maxRetries=15 +com.example.MyService/complexHello/Retry/maxRetries=20 +---- + +Has the following effects: + +* `MyService.complexHello()` has `maxRetries` set to `20`. +* `MyService.simpleHello()` has `maxRetries` set to `15`. +* Other `@Retry` methods in the application have `maxRetries` set to `30`. + +== Disabling Fault Tolerance + +It is possible to disable certain fault tolerance annotation on some method, if the annotation is present on the method: + +[source,properties] +---- +smallrye.faulttolerance."com.example.MyService/hello".retry.enabled=false + +# alternatively, a specification-defined property can be used +com.example.MyService/hello/Retry/enabled=false +---- + +Or on some class, if the annotation is present on the class: + +[source,properties] +---- +smallrye.faulttolerance."com.example.MyService".retry.enabled=5 + +# alternatively, a specification-defined property can be used +com.example.MyService/Retry/enabled=false +---- + +Or globally: + +[source,properties] +---- +smallrye.faulttolerance.global.retry.enabled=false + +# alternatively, a specification-defined property can be used +Retry/enabled=false +---- + +It is also possible to disable all fault tolerance completely: + +[source,properties] +---- +smallrye.faulttolerance.enabled=false + +# alternatively, a specification-defined property can be used +MP_Fault_Tolerance_NonFallback_Enabled=false +---- + +This will leave only fallbacks enabled, all other annotations will be disabled. + +== {smallrye-fault-tolerance} Configuration Properties + +As demonstrated in the examples above, {smallrye-fault-tolerance} provides its own configuration properties, in addition to the specification-defined properties. +The specification-defined properties can of course be used, but the {smallrye-fault-tolerance} configuration properties have higher priority. + +The mapping is relatively straightforward: + +- `///` moves to `smallrye.faulttolerance."/"..` +- `//` moves to `smallrye.faulttolerance.""..` +- `/` moves to `smallrye.faulttolerance.global..` + +All the `` and `` parts are changed from camel case (`BeforeRetry`, `methodName`) to kebab case (`before-retry`, `method-name`). +Two annotation members are special cased to improve consistency: + +- `Retry/durationUnit` moves to `retry.max-duration-unit`, because the value property is called `maxDuration` (`max-duration`) +- `Retry/jitterDelayUnit` moves to `retry.jitter-unit`, because the value property is called `jitter` + +Further: + +- `MP_Fault_Tolerance_NonFallback_Enabled` moves to `smallrye.faulttolerance.enabled` +- `MP_Fault_Tolerance_Metrics_Enabled` moves to `smallrye.faulttolerance.metrics.enabled` + +== Links + +Configuration is described in detail in the {microprofile-fault-tolerance-url}#configuration[{microprofile-fault-tolerance} specification]. diff --git a/content/docs/smallrye-fault-tolerance/6/reference/fallback.adoc b/content/docs/smallrye-fault-tolerance/6/reference/fallback.adoc new file mode 100644 index 000000000..af6316c15 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/6/reference/fallback.adoc @@ -0,0 +1,302 @@ += Fallback +:version-root: ../ +include::{version-root}_attributes.adoc[] + +For introduction, see xref:howto/fallback.adoc[How to Supply Fallback Values]. + +== Description + +If a guarded method throws an exception, fallback will prevent that exception from being propagated to the caller. +Instead, a fallback method or fallback handler is called and their result is returned. + +More specifically: + +. If the guarded method returns normally, its return value is returned. +. Otherwise, if the guarded method throws an exception whose type is assignable to any of the `skipOn` types (see <>), the exception is rethrown. +. Otherwise, if the guarded method throws an exception whose type is assignable to any of the `applyOn` types (see <>), the fallback value is obtained and returned. +. Otherwise, the exception is rethrown. + +[[interactions]] +== Interactions with Other Strategies + +See xref:howto/multiple.adoc[How to Use Multiple Strategies] for an overview of how fault tolerance strategies are nested. + +If `@Fallback` is used, the fallback method or handler may be invoked if other fault tolerance strategies throw an exception, depending on the fallback configuration. + +[[configuration]] +== Configuration + +There are 4 configuration options, corresponding to the 4 members of the `@Fallback` annotation. + +Exactly one of `value` or `fallbackMethod` must be set explicitly, they don't have a useful default value. + +=== `value` + +Type: `Class>` + +Default: empty + +Class of the fallback handler implementation that should be used to obtain the fallback value. +When this is set, the `fallbackMethod` must be left empty. + +The fallback handler class must implement the `FallbackHandler` interface. +The type argument to `FallbackHandler` must be assignable to the return type of the guarded method. +For example, if the guarded method returns `Number`, the fallback handler may implement `FallbackHandler` or `FallbackHandler`. + +=== `fallbackMethod` + +Type: `String` + +Default: empty + +Name of the fallback method that should be used to obtain the fallback value. +When this is set, the `value` must be left empty. + +The fallback method must be declared directly on the class where the guarded method is declared, or must be inherited from any of the class's supertypes. +The fallback method must have the same parameter types as the guarded method. +The fallback method must have a return type that is assignable to the return type of the guarded method. +For example, if the guarded method returns `Number`, the fallback method may return `Number` or `Integer`. + +[[apply-on]] +=== `applyOn` + +Type: `Class[]` + +Default: `{Throwable.class}` + +Set of exception types to which fallback applies. + +[[skip-on]] +=== `skipOn` + +Type: `Class[]` + +Default: `{}` + +Set of exception types to which fallback does _not_ apply. +This configuration takes precedence over `applyOn`. + +[[metrics]] +== Metrics + +Fallback does not emit any metrics on its own. + +See xref:reference/metrics.adoc[the Metrics reference guide] for general metrics information. +Fallback is included in the overall method invocation metric described there. + +== Extra Features + +[[fallback-methods-with-exception-parameter]] +=== Fallback Methods with Exception Parameter + +include::{partialsdir}/srye-feature.adoc[] + +include::{partialsdir}/non-compat.adoc[] + +{smallrye-fault-tolerance} provides access to the causing exception in a fallback method. + +A fallback method, as defind by the {microprofile-fault-tolerance} specification, must have the same parameters as the guarded method. +{smallrye-fault-tolerance} permits defining one additional parameter, at the end of the parameter list, which must be of an exception type. +If such parameter is defined, the exception that caused the fallback will be supplied in it. + +For example: + +[source,java] +---- +@ApplicationScoped +public class MyService { + @Fallback(fallbackMethod = "fallback") + public String doSomething(String param) { + ... + } + + public String fallback(String param, IllegalArgumentException cause) { // <1> + ... + } +} +---- + +<1> The fallback method matches the guarded method signature, except for one additional parameter at the end. + +All rules of {microprofile-fault-tolerance} specification related to looking up fallback methods still apply. +That is, the return types must match, the parameter types must match (with this one exception), etc. + +If the thrown exception is not assignable to the exception parameter type, it is rethrown as if no fallback was declared. +In the previous example, if `IllegalStateException` was thrown, the fallback method would not be called, as `IllegalStateException` is not a subtype of `IllegalArgumentException`. + +If the guarded method has a vararg parameter and you want to declare a fallback method with an exception parameter, simply replace the vararg syntax with an array type: + +[source,java] +---- +@ApplicationScoped +public class MyService { + @Fallback(fallbackMethod = "fallback") + public String doSomething(String... params) { + ... + } + + public String fallback(String[] params, IllegalArgumentException cause) { + ... + } +} +---- + +==== Multiple Fallback Methods with Exception Parameter + +It is possible to declare multiple overloads of the fallback method, each having different type of the exception parameter: + +[source,java] +---- +@ApplicationScoped +public class MyService { + @Fallback(fallbackMethod = "fallback") + public String doSomething(String param) { + ... + } + + public String fallback(String param, IllegalArgumentException cause) { + ... + } + + public String fallback(String param, RuntimeException cause) { + ... + } +} +---- + +In that case, which fallback method is called depends on the type of thrown exception. +The method that declares a most-specific supertype of the actual exception is selected. + +In the previous example, if `IllegalArgumentException` was thrown by `doSomething`, the first fallback method would be called. +If `IllegalStateException` was thrown, the second fallback method would be called. + +If the thrown exception is not assignable to the exception parameter type of any fallback method, it is rethrown as if no fallback was declared. + +==== Fallback Methods with and without Exception Parameter + +It is possible to declare the fallback method with and without an exception parameter at the same time: + +[source,java] +---- +@ApplicationScoped +public class MyService { + @Fallback(fallbackMethod = "fallback") + public String doSomething(String param) { + ... + } + + public String fallback(String param, IllegalArgumentException cause) { + ... + } + + public String fallback(String param, RuntimeException cause) { + ... + } + + public String fallback(String param) { + ... + } +} +---- + +The fallback methods with an exception parameter have precedence. +The fallback method without an exception parameter is only called if the thrown exception is not assignable to any declared exception parameter. + +==== Interactions with `applyOn` / `skipOn` + +The presence or absence of a fallback method with specific exception parameter may seem related to the usage of `applyOn` / `skipOn` on the `@Fallback` annotation, but in fact, it is not. +These features are completely independent. + +Simply put, the `applyOn` / `skipOn` configuration is always evaluated first. +A fallback method is only selected and invoked when this configuration indicates that a fallback should apply. + +If `@Fallback` is configured to skip `IllegalStateException` and `IllegalStateException` is thrown, no fallback method is invoked. +That applies even if a fallback method with a matching exception parameter exists. + +For example: + +[source,java] +---- +@ApplicationScoped +public class MyService { + @Fallback(fallbackMethod = "fallback", skipOn = IllegalStateException.class) + public String doSomething(String param) { + ... + } + + public String fallback(String param, IllegalArgumentException cause) { + ... + } + + public String fallback(String param, RuntimeException cause) { + ... + } + + public String fallback(String param) { + ... + } +} +---- + +In this case: + +- if `doSomething` throws `IllegalArgumentException`, the first fallback method is called; +- if `doSomething` throws `IllegalStateException`, no fallback method is called, because this exception type is skipped; +- if `doSomething` throws any other `RuntimeException`, the second fallback method is called; +- if `doSomething` throws any other exception, the last fallback method is called. + +[[inspecting-exception-cause-chains]] +=== Inspecting Exception Cause Chains + +include::{partialsdir}/srye-feature.adoc[] + +include::{partialsdir}/non-compat.adoc[] + +The `@Fallback` annotation can specify that certain exceptions should be treated as failures (`applyOn`) and others as successes (`skipOn`). +The specification limits this to inspecting the actual exception that was thrown. +However, in many cases, exceptions are wrapped and the exception the user wants to decide on is only present in the cause chain. + +For that reason, in the non-compatible mode, if the actual thrown exception isn't known failure or known success, {smallrye-fault-tolerance} inspects the cause chain. +To be specific, in case a `@Fallback` method throws an exception, the decision process is: + +1. If the `skipOn` exceptions are not default and the exception is assignable to one of the `skipOn` exceptions, fallback is skipped and the exception is rethrown. +2. Otherwise, if the `applyOn` exceptions are not default and the exception is assignable to one of the `applyOn` exceptions, fallback is applied. +3. Otherwise, if the exception is assignable to one of the `skipOn` exceptions or its cause chain contains an exception assignable to one of the `skipOn` exceptions, fallback is skipped and the exception is rethrown. +4. Otherwise, if the exception is assignable to one of the `applyOn` exceptions or its cause chain contains an exception assignable to one of the `applyOn` exceptions, fallback is applied. +5. Otherwise, the exception is rethrown. + +For example: + +[source,java] +---- +@Fallback(fallbackMethod = "fallback", + skipOn = ExpectedOutcomeException.class, // <1> + applyOn = IOException.class) // <2> +public Result doSomething() { + ... +} + +public Result fallback() { + ... +} +---- + +<1> If `doSomething` throws an `ExpectedOutcomeException`, or a `WrapperException` whose cause is `ExpectedOutcomeException`, fallback is skipped and the exception is thrown. +<2> If `doSomething` throws an `IOException`, or a `WrapperException` whose cause is `IOException`, fallback is applied. + +[source,java] +---- +@Fallback(fallbackMethod = "fallback", + skipOn = ExpectedOutcomeException.class) // <1> <2> +public Result doSomething() { + ... +} + +public Result fallback() { + ... +} +---- + +<1> If `doSomething` throws an `ExpectedOutcomeException`, or a `WrapperException` whose cause is `ExpectedOutcomeException`, fallback is skipped and the exception is thrown. +<2> There's no `applyOn`, so the 2nd step in the algorithm above is skipped. +This is what turns the `WrapperException` whose cause is `ExpectedOutcomeException` into a skipped fallback. diff --git a/content/docs/smallrye-fault-tolerance/6/reference/metrics.adoc b/content/docs/smallrye-fault-tolerance/6/reference/metrics.adoc new file mode 100644 index 000000000..3aae5532e --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/6/reference/metrics.adoc @@ -0,0 +1,114 @@ += Metrics +:version-root: ../ +include::{version-root}_attributes.adoc[] + +{smallrye-fault-tolerance} exposes metrics, as {microprofile-fault-tolerance-url}#_integration_with_microprofile_metrics_and_microprofile_telemetry[specified] by {microprofile-fault-tolerance}. + +[[general]] +== General Metrics + +For all methods guarded with some fault tolerance strategy, the following metric is emitted: + +[cols="1,5"] +|=== +| Name | `ft.invocations.total` +| Type +a| * MP Metrics: `Counter` +* OpenTelemetry: `LongCounter` +* Micrometer: `Counter` +| Unit | None +| Description | The number of times the method was called. +| Tags +a| * `method` - the fully qualified method name +* `result` = `[valueReturned\|exceptionThrown]` - whether the invocation returned a value or threw an exception +* `fallback` = `[applied\|notApplied\|notDefined]` - `applied` if fallback was used, `notApplied` if a fallback is configured but was not used, `notDefined` if a fallback is not configured +|=== + +For xref:reference/asynchronous.adoc[asynchronous methods], "value returned" means that the `CompletionStage` completed successfully, while "exception thrown" means that the `CompletionStage` completed exceptionally. + +== Specific Metrics + +In addition to the general metrics described above, most fault tolerance strategies emit their own metrics. +Those are described in the reference guides: + +* xref:reference/bulkhead.adoc#metrics[Bulkhead] +* xref:reference/circuit-breaker.adoc#metrics[Circuit Breaker] +* xref:reference/rate-limit.adoc#metrics[Rate Limit] +* xref:reference/retry.adoc#metrics[Retry] +* xref:reference/timeout.adoc#metrics[Timeout] + +== Timer Metrics + +For task scheduling purposes (e.g. watching timeouts or delaying retries), {smallrye-fault-tolerance} maintains one thread called the _timer thread_. +Most of the time, it is kept sleeping (parked), it only wakes up (unparks) when necessary to submit tasks to executors. + +The behavior of the timer thread can be observed through the following metrics: + +[cols="1,5"] +|=== +| Name | `ft.timer.scheduled` +| Type +a| * MP Metrics: `Gauge` +* OpenTelemetry: `LongUpDownCounter` +* Micrometer: `Gauge` +| Unit | None +| Description | The number of tasks that are currently scheduled (for future execution) on the timer. +| Tags +a| * `id` - the ID of the timer, to distinguish multiple timers in a multi-application environment +|=== + +== Micrometer Support + +In addition to the MicroProfile Metrics and OpenTelemetry support (as specified by {microprofile-fault-tolerance}), {smallrye-fault-tolerance} also provides support for https://micrometer.io/[Micrometer]. +The set of metrics emitted to Micrometer is the same, using the same metric names and tags. + +Note that distribution summaries in Micrometer, including timers, do not emit quantiles by default. +Micrometer recommends that libraries should not configure them out of the box, so if you need them, you should use a `MeterFilter`. + +The following implementation makes sure Micrometer emits the same quantiles as MicroProfile Metrics for all fault tolerance metrics: + +[source,java] +---- +static final MeterFilter ENABLE_HISTOGRAMS = new MeterFilter() { + @Override + public DistributionStatisticConfig configure(Meter.Id id, DistributionStatisticConfig config) { + if (id.getName().startsWith("ft.")) { + return DistributionStatisticConfig.builder() + .percentiles(0.5, 0.75, 0.95, 0.98, 0.99, 0.999) + .build() + .merge(config); + } + return config; + } +}; +---- + +== Disabling Metrics + +It is possible to completely disable fault tolerance metrics using MicroProfile Config: + +[source,properties] +---- +smallrye.faulttolerance.metrics.enabled=false + +# alternatively, a specification-defined property can be used +MP_Fault_Tolerance_Metrics_Enabled=false +---- + +It is also possible to disable individual metric providers: + +[source,properties] +---- +# to disable MP Metrics +smallrye.faulttolerance.mpmetrics.disabled=true + +# to disable OpenTelemetry Metrics +smallrye.faulttolerance.opentelemetry.disabled=true + +# to disable Micrometer +smallrye.faulttolerance.micrometer.disabled=true +---- + +Note that setting `smallrye.faulttolerance.*.disabled` to `false` does not mean the provider is enabled unconditionally. +When that provider is not discovered or selected by the integrator, it cannot be enabled in any way. +These properties are only meant for disabling an otherwise enabled metrics provider; not the other way around. diff --git a/content/docs/smallrye-fault-tolerance/6/reference/non-compat.adoc b/content/docs/smallrye-fault-tolerance/6/reference/non-compat.adoc new file mode 100644 index 000000000..20fcf0b64 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/6/reference/non-compat.adoc @@ -0,0 +1,36 @@ += Non-compatible Mode +:version-root: ../ +include::{version-root}_attributes.adoc[] + +include::{partialsdir}/srye-feature.adoc[] + +{smallrye-fault-tolerance} offers a mode where certain features are improved beyond specification, as described below. +This mode is *not compatible* with the {microprofile-fault-tolerance} specification (and doesn't necessarily pass the entire TCK). + +This mode is disabled by default. +To enable, set the configuration property `smallrye.faulttolerance.mp-compatibility` to `false`. + +.Quarkus +**** +In Quarkus, the non-compatible mode is enabled by default. +To restore compatibility, add the following to your `application.properties`: + +[source,properties] +---- +smallrye.faulttolerance.mp-compatibility=true +---- + +Note that the non-compatible mode is available since {smallrye-fault-tolerance} 5.2.0 and Quarkus 2.1.0.Final. +Previous versions are always compatible. +**** + +== Improvements + +The improvements enabled in the non-compatible mode are: + +* xref:reference/asynchronous.adoc#method-asynchrony[Determining asynchrony from method signatures] +* Inspecting exception cause chains +** in xref:reference/fallback.adoc#inspecting-exception-cause-chains[fallback] +** in xref:reference/circuit-breaker.adoc#inspecting-exception-cause-chains[circuit breaker] +** in xref:reference/retry.adoc#inspecting-exception-cause-chains[retry] +* xref:reference/fallback.adoc#fallback-methods-with-exception-parameter[Fallback methods with exception parameter] diff --git a/content/docs/smallrye-fault-tolerance/6/reference/programmatic-api.adoc b/content/docs/smallrye-fault-tolerance/6/reference/programmatic-api.adoc new file mode 100644 index 000000000..f36db7e87 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/6/reference/programmatic-api.adoc @@ -0,0 +1,414 @@ += Programmatic API +:version-root: ../ +include::{version-root}_attributes.adoc[] + +include::{partialsdir}/srye-feature.adoc[] + +In addition to the declarative, annotation-based API of {microprofile-fault-tolerance}, {smallrye-fault-tolerance} also offers a programmatic API for advanced scenarios. +This API is present in the `io.smallrye:smallrye-fault-tolerance-api` artifact, just like all other additional APIs {smallrye-fault-tolerance} provides. + +== Installation + +If you use {smallrye-fault-tolerance} as part of a runtime that implements {microprofile-fault-tolerance}, you don't have to do anything. +The programmatic API is ready to use. +In this documentation, we'll call this the _CDI implementation_ of the programmatic API, because it's integrated with the {microprofile-fault-tolerance} implementation, which is naturally based on CDI. + +.Quarkus +**** +In Quarkus, the {smallrye-fault-tolerance} API is brought in automatically, as a transitive dependency of the Quarkus extension for {smallrye-fault-tolerance}. +That is, you don't need to do anything to be able to use the programmatic API. +**** + +.WildFly +**** +In WildFly, the {smallrye-fault-tolerance} API is not readily available to deployments. +If you want to use it, you need to add a module dependency to the deployment using `jboss-deployment-structure.xml`. + +Note that at the time of this writing, the {smallrye-fault-tolerance} module in WildFly is considered private. +If you do add a module dependency on it, to be able to use the {smallrye-fault-tolerance} API, you may be stepping out of the WildFly support scope. +**** + +In addition to the CDI implementation, {smallrye-fault-tolerance} also offers a _standalone implementation_ that is meant to be used outside of any runtime. +This implementation does not need CDI or anything else. +If you want to use {smallrye-fault-tolerance} in a standalone fashion, just add a dependency on `io.smallrye:smallrye-fault-tolerance-standalone`. +The API is brought in transitively. + +== Usage + +The entrypoints to the programmatic API are the `Guard` and `TypedGuard` interfaces. + +These interfaces represent a configured set of fault tolerance strategies. +Their configuration, order of application and behavior in general corresponds to the declarative API, so if you know that, you'll feel right at home. +If not, the javadoc has all the information you need (though it often points to the annotation-based API for more information). + +The interfaces are very similar, there are only 2 differences: + +. `Guard` requires specifying the return type of the guarded operation for each `call()` or `get()` (or `adapt*`) method call. + `TypedGuard`, on the other hand, requires specifying the return type once, when creating an instance using `create()`. + Therefore, `Guard` may be used for guarding many different types, while `TypedGuard` may only be used to guard single type. +. `TypedGuard` allows defining a fallback. + `Guard` does not. + +In the following text, we'll only talk about `Guard`, but it also applies to `TypedGuard` without a change (except of the differences mentioned above). + +To create an instance of `Guard`, you can use the `create` static method. +It returns a builder which has to be used to add and configure all the fault tolerance strategies that should apply. +There is no external configuration, so all configuration properties have to be set explicitly, using the builder methods. +If you don't set a configuration property, it will default to the same value the annotation-based API uses. + +.Disabling Fault Tolerance +**** +There's one exception to the "no external configuration" rule. + +The CDI implementation looks for the `smallrye.faulttolerance.enabled` / `MP_Fault_Tolerance_NonFallback_Enabled` configuration properties in MicroProfile Config. +The standalone implementation looks for system properties with the same name, or obtains the value from custom configuration (as xref:integration/programmatic-api.adoc[described] in the integration section). + +If at least one of these properties exists and is set to `false`, only the fallback and thread offload fault tolerance strategies will be applied. +Everything else will be ignored. + +Note that this is somewhat different to the declarative, annotation-based API, where only fallback is retained and the `@Asynchronous` strategy is skipped as well. +Since this significantly changes execution semantics, the programmatic API will apply thread offload even if fault tolerance is disabled. + +Similarly to the declarative API, implementations of the programmatic API also read this property only once, when the `Guard` API is first used. +It is _not_ read again later. +**** + +Let's take a look at a simple example: + +[source,java] +---- +public class MyService { + private static final Guard GUARD = Guard.create() + .withRetry().maxRetries(3).done() + .build(); + + public String hello() throws Exception { + return GUARD.call(() -> externalService.hello(), String.class); // <1> + } +} +---- + +<1> Here, we call `externalService.hello()` and guard the call with the previously configured set of fault tolerance strategies. + The `call` method used here takes the `Callable` type, which represent the guarded action. + The `get` method works just like `call`, but accepts a `Supplier`. + +The previous example shows how to apply fault tolerance to synchronous actions. +{smallrye-fault-tolerance} naturally also supports guarding asynchronous actions, using the `java.util.concurrent.CompletionStage` type. +Unlike the declarative API, the programmatic API doesn't support asynchronous actions that return the `java.util.concurrent.Future` type. + +[source,java] +---- +public class MyService { + private static final Guard GUARD = Guard.create() + .withBulkhead().done() + .withThreadOffload(true) // <1> + .build(); + + public CompletionStage hello() throws Exception { + return GUARD.call(() -> externalService.hello(), + new TypeLiteral>() {}); // <2> + } +} +---- + +<1> The thread offload here only applies to asynchronous actions. + If you use the same `GUARD` to guard a synchronous action, no thread offload will apply. +<2> Note that here, we use the `TypeLiteral` class (from CDI) to specify the type of the guarded action. + +Asynchronous actions may be blocking or non-blocking. +In the example above, we assume the `externalService.hello()` call is blocking, so we set thread offload to `true`. +{smallrye-fault-tolerance} will automatically move the actual execution of the action to another thread. + +If we didn't configure `withThreadOffload`, however, the execution of an asynchronous action would continue on the original thread. +This is often desired for non-blocking actions, which are very common in modern reactive architectures. + +Also note that in this example, we configured multiple fault tolerance strategies: bulkhead and thread offload. +When that happens, the fault tolerance strategies are ordered according to the {microprofile-fault-tolerance} specification, just like in the declarative API. +Order of all the `with*` method invocations doesn’t matter. + +=== Single-Action Usage + +The `Guard` API is general and permits guarding multiple different actions using the same set of fault tolerance strategies. +Often, we need to guard just a single action, although possibly several times. + +For such use case, the `Guard` instance may be adapted to a `Callable` or `Supplier` using the `adapt*` methods: + +[source,java] +---- +public class MyService { + private static final Callable guard = Guard.create() + .withTimeout().duration(5, ChronoUnit.SECONDS).done() + .build() // <1> + .adaptCallable(() -> externalService.hello(), String.class); // <2> + + public String hello() throws Exception { + return callable.call(); // <3> + } + +} +---- + +<1> Create a `Guard` object that can guard arbitrary actions. +<2> Adapt the general `Guard` instance to a `Callable` that guards the `externalService.hello()` invocation. + Similar method exists that accepts and returns a `Supplier`: `adaptSupplier`. +<3> You can do whatever you wish with the adapted `Callable`. + Here, we just call it once, which isn't very interesting, but it could possibly be called multiple times, passed to other methods etc. + +=== Synchronous vs. Asynchronous + +The `Guard` and `TypedGuard` interfaces both decide whether the guarded action is synchronous or asynchronous based on the action type. +This is given to `Guard` when calling or adapting the action, and to `TypedGuard` when creating it. + +If the type is asynchronous (such as `CompletionStage`), the asynchronous behavior will also be guarded; the action is only considered complete when the `CompletionStage` actually completes. +If the type is not asynchronous, only the synchronous behavior will be guarded; the action is considered complete when the method returns. + +To enable support of additional asynchronous types, it is enough to include the support libraries, as described in xref:reference/asynchronous.adoc#async-types[Additional Asynchronous Types]. + +==== Mutiny Support + +For example, when the Mutiny support library `io.smallrye:smallrye-fault-tolerance-mutiny` is present, both `Guard` and `TypedGuard` will recognize `Uni` as an asynchronous type and guard it properly. +Guarding a `Multi` is not supported. + +For example: + +[source,java] +---- +public class MyService { + private final Supplier> GUARD = TypedGuard.create( + new TypeLiteral>() {}) + .withTimeout().duration(5, ChronoUnit.SECONDS).done() + .withFallback().handler(() -> Uni.createFrom().item("fallback")).done() + .build() + .adaptSupplier(() -> externalService.hello()); // <1> + + public Uni hello() { + return guard.get(); + } +} +---- + +<1> The call to `externalService.hello()` is supposed to return `Uni`. + +Note that the `Uni` type is lazy, so the action itself won't execute until the guarded `Uni` is subscribed to. + +.Quarkus +**** +In Quarkus, the Mutiny support library is present by default. +You can guard `Uni`-returning actions out of the box. +**** + +=== Stateful Fault Tolerance Strategies + +The bulkhead, circuit breaker and rate limit strategies are stateful. +That is, they hold some state required for their correct functioning, such as the number of current executions for bulkhead, the rolling window of successes/failures for circuit breaker, or the time window for rate limit. +If you use these strategies, you have to consider their lifecycle. + +The {smallrye-fault-tolerance} programmatic API makes such reasoning pretty straightforward. +Each `Guard` object has its own instance of each fault tolerance strategy, including the stateful strategies. +If you use a single `Guard` object for guarding multiple different actions, all those actions will be guarded by the same bulkhead, circuit breaker and/or rate limit. +If, on the other hand, you use different `Guard` objects for guarding different actions, each action will be guarded by its own bulkhead, circuit breaker and/or rate limit. + +If you call the `adapt*` methods on the same `Guard` multiple times, the resulting `Callable` or `Supplier` objects will guard the underlying action using the original `Guard` instance, so stateful strategies will be shared. + +=== Circuit Breaker Maintenance + +The `CircuitBreakerMaintenance` API, accessed through `CircuitBreakerMaintenance.get()` or by injection in the CDI implementation, can be used to manipulate all named circuit breakers. +A circuit breaker is given a name by calling `withCircuitBreaker().name("\...")` on the fault tolerance builder, or using the `@CircuitBreakerName` annotation in the declarative API. + +Additionally, `CircuitBreakerMaintenance.resetAll()` will also reset all unnamed circuit breakers declared using the `@CicruitBreaker` annotation. +For this to work, all unnamed circuit breakers have to be remembered. +This is safe in case of the declarative, annotation-based API, because the number of such declared circuit breakers is fixed. +At the same time, this would _not_ be safe to do for all unnamed circuit breakers created using the programmatic API, as their number is potentially unbounded. +(In other words, remembering all unnamed circuit breakers created using the programmatic API would easily lead to a memory leak.) + +Therefore, all circuit breakers created using the programmatic API must be given a name when `CircuitBreakerMaintenance` is supposed to affect them. +Note that duplicate names are not permitted and lead to an error, so lifecycle of the circuit breaker must be carefully considered. + +=== Event Listeners + +The programmatic API has one feature that the declarative API doesn't have: ability to observe certain events. +For example, when configuring a circuit breaker, it is possible to register a callback for circuit breaker state changes or for a situation when an open circuit breaker prevents an invocation. +When configuring a timeout, it is possible to register a callback for when the invocation times out, etc. etc. +For example: + +[source,java] +---- +private static final Guard GUARD = Guard.create() + .withTimeout().duration(5, ChronoUnit.SECONDS).onTimeout(() -> ...).done() // <1> + .build(); +---- + +<1> The `onTimeout` method takes a `Runnable` that will later be executed whenever an invocation guarded by `GUARD` times out. + +All event listeners registered like this must run quickly and must not throw exceptions. + +== Configuration + +As mentioned above, except of `smallrye.faulttolerance.enabled` / `MP_Fault_Tolerance_NonFallback_Enabled`, there is no support for external configuration of fault tolerance strategies. +This may change in the future, though possibly only in the CDI implementation. + +== Metrics + +The programmatic API is integrated with metrics. +All metrics, as described xref:reference/metrics.adoc[in the Metrics reference guide] and the linked guides, are supported. +The only difference is the value of the `method` tag. +With the programmatic API, the `method` tag will be set to the _description_ of the guarded operation, provided on the `Guard` builder. + +[source,java] +---- +private static final Guard GUARD = Guard.create() + .withDescription("hello") // <1> + .withRetry().maxRetries(3).done() + .build(); +---- + +<1> A description of `hello` is set, it will be used as a value of the `method` tag in all metrics. + +It is possible to create multiple `Guard` objects with the same description. +In this case, it won't be possible to distinguish the different `Guard` objects in metrics; their values will be aggregated. + +If no description is provided, metrics are not emitted. + +== Differences to the Specification + +`Guard` and `TypedGuard` have the following differences to standard MicroProfile Fault Tolerance: + +* asynchronous actions of type `java.util.concurrent.Future` are not supported; +* the fallback, circuit breaker and retry strategies always inspect the cause chain of exceptions, following the behavior of SmallRye Fault Tolerance in the non-compatible mode. + +== Kotlin `suspend` Functions + +The `Guard` and `TypedGuard` APIs do not support Kotlin `suspend` functions at the moment. + +== Integration Concerns + +Integration concerns, which are particularly interesting for users of the standalone implementation, are xref:integration/programmatic-api.adoc[described] in the integration section. + +[[migration_from_faulttolerance]] +== Migration from `FaultTolerance` + +The 1st version of the programmatic API had the `FaultTolerance` interface. +This is deprecated and scheduled for removal in {smallrye-fault-tolerance} 7.0. +The replacement are the `Guard` and `TypedGuard` types. + +When migrating, one first has to decide which type to use as a replacement. +The `TypedGuard` is much closer to the `FaultTolerance` type, so that is the easiest way to migrate. +There are some differences still: + +- the `create()` static method now takes a parameter that expresses the guarded action type, either as a `Class` or as a `TypeLiteral`; +- there are no `cast()` and `castAsync()` methods, because `TypedGuard` only guards actions of a single type; +- there is no support for guarding or adapting ``Runnable``s, only ``Callable``s and ``Supplier``s are supported. + As a replacement for `Runnable`, a `Supplier` (or `Supplier>` etc.) can be used. + +For example, these usages of `FaultTolerance`: + +[source,java] +---- +static final FaultTolerance FT1 = FaultTolerance.create() + .withDescription("ft1") + .withRetry().maxRetries(3).done() + .withFallback().handler(() -> "fallback").done() + .build(); + +static final FaultTolerance> FT2 = FaultTolerance.createAsync() + .withDescription("ft2") + .withBulkhead().limit(5).queueSize(100).done() + .withTimeout().duration(3, ChronoUnit.SECONDS).done() + .build(); +---- + +can be rewritten to: + +[source,java] +---- +static final TypedGuard GUARD1 = TypedGuard.create(String.class) + .withDescription("ft1") + .withRetry().maxRetries(3).done() + .withFallback().handler(() -> "fallback").done() + .build(); + +static final TypedGuard> GUARD2 = TypedGuard.create( + new TypeLiteral>() {}) + .withDescription("ft2") + .withBulkhead().limit(5).queueSize(100).done() + .withTimeout().duration(3, ChronoUnit.SECONDS).done() + .build(); +---- + +After creating a `TypedGuard`, there's no change in how it's used. +The methods `call()`, `get()`, `adaptCallable()` and `adaptSupplier()` have the same signatures as before. + +`TypedGuard` is the only possible replacement if you need fallback. +If you don't need fallback, and especially if you need to guard actions of multiple types, `Guard` is a better choice. + +The differences are: + +- `Guard` has no type parameter (is not generic) and does not allow defining fallback; +- there are no `cast()` and `castAsync()` methods, because they are no longer needed; +- there is no support for guarding or adapting ``Runnable``s, only ``Callable``s and ``Supplier``s are supported. + As a replacement for `Runnable`, a `Supplier` (or `Supplier>` etc.) can be used. + +For example, these usages of `FaultTolerance`: + +[source,java] +---- +static final FaultTolerance FT1 = FaultTolerance.create() + .withDescription("ft1") + .withRetry().maxRetries(3).done() + .withCircuitBreaker().done() + .build(); + +static final FaultTolerance> FT2 = FaultTolerance.createAsync() + .withDescription("ft2") + .withBulkhead().limit(5).queueSize(100).done() + .withTimeout().duration(3, ChronoUnit.SECONDS).done() + .build(); +---- + +can be rewritten to: + +[source,java] +---- +static final Guard GUARD1 = Guard.create() + .withDescription("ft1") + .withRetry().maxRetries(3).done() + .withCircuitBreaker().done() + .build(); + +static final Guard GUARD2 = Guard.create() + .withDescription("ft2") + .withBulkhead().limit(5).queueSize(100).done() + .withTimeout().duration(3, ChronoUnit.SECONDS).done() + .build(); +---- + +Now, as mentioned above, `Guard` is used slightly differently than `FaultTolerance`. +The methods `call()`, `get()`, `adaptCallable()` and `adaptSupplier()` take an extra parameter that expresses the type of the guarded action, again either as a `Class` or as a `TypeLiteral`. + +For example, these usages of `FaultTolerance`: + +[source,java] +---- +String result1 = FT1.call(() -> externalService.hello()); +CompletionStage result2 = FT2.get(() -> externalService.helloAsync()); + +Callable callable = FT1.adaptCallable( + () -> externalService.hello()); +Supplier> supplier = FT2.adaptSupplier( + () -> externalService.helloAsync()); +---- + +can be rewritten to: + +[source,java] +---- +String result1 = GUARD1.call(() -> externalService.hello(), String.class); +CompletionStage result2 = GUARD2.get(() -> externalService.helloAsync(), + new TypeLiteral>() {}); + +Callable callable = GUARD1.adaptCallable( + () -> externalService.hello(), String.class); +Supplier> supplier = GUARD2.adaptSupplier( + () -> externalService.helloAsync(), + new TypeLiteral>() {}); +---- diff --git a/content/docs/smallrye-fault-tolerance/6/reference/rate-limit.adoc b/content/docs/smallrye-fault-tolerance/6/reference/rate-limit.adoc new file mode 100644 index 000000000..8e9ea45cd --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/6/reference/rate-limit.adoc @@ -0,0 +1,149 @@ += Rate Limit +:version-root: ../ +include::{version-root}_attributes.adoc[] + +For introduction, see xref:howto/rate-limit.adoc[How to Limit Execution Rate]. + +include::{partialsdir}/srye-feature.adoc[] + +== Description + +Rate limit enforces a maximum number of permitted invocations in a time window of some length. +For example, with a rate limit, one can make sure that a method may only be called 50 times per minute. +Invocations that would exceed the limit are rejected with an exception of type `RateLimitException`. + +Additionally, it is possible to define minimum spacing between invocations. +For example, with minimum spacing of 1 second, if a second invocation happens 500 millis after the first, it is rejected even if the limit would not be exceeded yet. + +Rate limit is superficially similar to a bulkhead (concurrency limit), but is in fact quite different. +Bulkhead limits the number of executions happening concurrently at any point in time. +Rate limit limits the number of executions in a time window of some length, without considering concurrency. + +A method or a class can be annotated with `@RateLimit`, which means the method or the methods in the class will apply the rate limit strategy. +The previous example with 50 maximum invocations per minute and minimum spacing of 1 second would look like this: + +[source,java] +---- +@RateLimit(value = 50, + window = 1, windowUnit = ChronoUnit.MINUTES, + minSpacing = 1, minSpacingUnit = ChronoUnit.SECONDS) +public void doSomething() { + ... +} +---- + +The `RateLimitException` class has a method `getRetryAfterMillis()` which returns the number of milliseconds after which the user may retry the rejected invocation. +Retrying sooner is guaranteed to be rejected again. +Note that this information is accurate only at the time the invocation is rejected. +It may be invalidated by any subsequent or concurrent invocations, so there is no guarantee that a retry attempt after the given number of milliseconds will in fact be permitted. + +=== Lifecycle + +Rate limit needs to maintain some state between invocations: the number of recent invocations, the time stamp of last invocation, and so on. +This state is a singleton, irrespective of the lifecycle of the bean that uses the `@RateLimit` annotation. + +More specifically, the rate limit state is uniquely identified by the combination of the bean class (`java.lang.Class`) and the method object (`java.lang.reflect.Method`) representing the guarded method. + +For example, if there’s a guarded method `doWork` on a bean which is `@RequestScoped`, each request will have its own instance of the bean, but all invocations of `doWork` will share the same rate limit state. + +[[interactions]] +== Interactions with Other Strategies + +See xref:howto/multiple.adoc[How to Use Multiple Strategies] for an overview of how fault tolerance strategies are nested. + +If `@Fallback` is used with `@RateLimit`, the fallback method or handler may be invoked if a `RateLimitException` is thrown, depending on the fallback configuration. + +If `@Retry` is used with `@RateLimit`, each retry attempt is processed by the rate limit as an independent invocation. +If `RateLimitException` is thrown, the execution may be retried, depending on how retry is configured. + +If `@CircuitBreaker` is used with `@RateLimit`, the circuit breaker is checked before enforcing the rate limit. +If rate limiting results in `RateLimitException`, this may be counted as a failure, depending on how the circuit breaker is configured. + +[[configuration]] +== Configuration + +There are 6 configuration options, corresponding to the 6 members of the `@RateLimit` annotation. + +=== `value` + +Type: `int` + +Default: `100` + +The limit of maximum invocations to be permitted in the time window. + +=== `window` + `windowUnit` + +Type: `long` + `ChronoUnit` + +Default: `1 second` + +The length of the time window. + +=== `minSpacing` + `minSpacingUnit` + +Type: `long` + `ChronoUnit` + +Default: `0 seconds` + +The minimum spacing between two consecutive invocations. + +=== `type` + +Type: `RateLimitType` + +Default: `RateLimitType.FIXED` + +The type of time window used for rate limiting. +There are 3 types of time windows used for rate limiting: _fixed_, _rolling_ and _smooth_. + +Fixed time windows are a result of dividing time into non-overlapping intervals of given length. +The invocation limit is enforced for each interval independently. +This means that short bursts of invocations occuring near the time window boundaries may temporarily exceed the configured rate limit. +This kind of rate limiting is also called _fixed window_ rate limiting. + +Rolling time windows enforce the limit continuously, instead of dividing time into independent intervals. +The invocation limit is enforced for all possible time intervals of given length, regardless of overlap. +This is more precise, but requires more memory and may be slower. +This kind of rate limiting is also called _sliding log_ rate limiting. + +Smooth time windows enforce a uniform distribution of invocations under a rate calculated from given time window length and given limit. +If recent rate of invocations is under the limit, a subsequent burst of invocations is allowed during a shorter time span, but the calculated rate is never exceeded. +This kind of rate limiting is also called _token bucket_ or _leaky bucket (as a meter)_ rate limiting, with the additional property that all work units are considered to have the same size. + +With fixed and rolling time windows, rejected invocations always count towards the limit, so if a caller continuously invokes the guarded method faster than the configuration allows, all invocations are rejected until the caller slows down. +With smooth time windows, rejected invocations do not count towards the recent rate of invocations. + +Example usage: + +[source,java] +---- +@RateLimit(value = 50, + window = 1, windowUnit = ChronoUnit.MINUTES, + minSpacing = 1, minSpacingUnit = ChronoUnit.SECONDS, + type = RateLimitType.ROLLING) +public void doSomething() { + ... +} +---- + +[[metrics]] +== Metrics + +Rate limit exposes the following metrics: + +[cols="1,5"] +|=== +| Name | `ft.ratelimit.calls.total` +| Type +a| * MP Metrics: `Counter` +* OpenTelemetry: `LongCounter` +* Micrometer: `Counter` +| Unit | None +| Description | The number of times the rate limit logic was run. This is usually once per method call, but may be zero times if the circuit breaker prevented execution or more than once if the method call was retried. +| Tags +a| * `method` - the fully qualified method name +* `rateLimitResult` = `[permitted\|rejected]` - whether the rate limit permitted the method call +|=== + +See xref:reference/metrics.adoc[the Metrics reference guide] for general metrics information. diff --git a/content/docs/smallrye-fault-tolerance/6/reference/retry.adoc b/content/docs/smallrye-fault-tolerance/6/reference/retry.adoc new file mode 100644 index 000000000..cfc4aaf6c --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/6/reference/retry.adoc @@ -0,0 +1,352 @@ += Retry +:version-root: ../ +include::{version-root}_attributes.adoc[] + +For introduction, see xref:howto/retry.adoc[How to Retry on Failures]. + +== Description + +If a guarded method throws an exception, retry will swallow that exception and call the method again. + +More specifically: + +. If the guarded method returns normally, its return value is returned. +. Otherwise, if the guarded method throws an exception whose type is assignable to any of the `abortOn` types (see <>), retrying is aborted and the exception is rethrown. +. Otherwise, if the guarded method throws an exception whose type is assignable to any of the `retryOn` types (see <>), the method call is retried. +. Otherwise, the exception is rethrown. + +[[interactions]] +== Interactions with Other Strategies + +See xref:howto/multiple.adoc[How to Use Multiple Strategies] for an overview of how fault tolerance strategies are nested. + +If `@Fallback` is used with `@Retry`, the fallback method or handler may be invoked if all retry attempts failed, depending on the fallback configuration. + +[[configuration]] +== Configuration + +There are 9 configuration options, corresponding to the 9 members of the `@Retry` annotation. + +=== `maxRetries` + +Type: `int` + +Default: `3` + +The maximum number of retries. +`-1` means the maximum number of retries is unlimited. + +=== `delay` + `delayUnit` + +Type: `long` + `ChronoUnit` + +Default: `0 millis` + +The delay between retry attempts. + +=== `maxDuration` + `durationUnit` + +Type: `long` + `ChronoUnit` + +Default: `180_000 millis`, or 3 minutes + +The maximum duration for which retries will be attempted. + +:time-value-name: maxDuration +:time-unit-bad-name: durationUnit +:time-unit-good-name: maxDurationUnit +include::{partialsdir}/time-unit-inconsistency.adoc[] + +=== `jitter` + `jitterDelayUnit` + +Type: `long` + `ChronoUnit` + +Default: `200 millis` + +The maximum jitter to adjust the delay between retries. +If set, the delay before each retry attempt will be computed as follows: + +. Generate random number from the closed interval `[-jitter, jitter]`. +. Add the generated random number to the configured `delay`. +. If the result is by any chance negative, use `0`. + +:time-value-name: jitter +:time-unit-bad-name: jitterDelayUnit +:time-unit-good-name: jitterUnit +include::{partialsdir}/time-unit-inconsistency.adoc[] + +[[retry-on]] +=== `retryOn` + +Type: `Class[]` + +Default: `{Exception.class}` + +Set of exception types that trigger a retry. + +[[abort-on]] +=== `abortOn` + +Type: `Class[]` + +Default: `{}` + +Set of exception types that abort retrying. +This configuration takes precedence over `retryOn`. + +[[metrics]] +== Metrics + +Retry exposes the following metrics: + +[cols="1,5"] +|=== +| Name | `ft.retry.calls.total` +| Type +a| * MP Metrics: `Counter` +* OpenTelemetry: `LongCounter` +* Micrometer: `Counter` +| Unit | None +| Description | The number of times the retry logic was run. This will always be once per method call. +| Tags +a| * `method` - the fully qualified method name +* `retried` = `[true\|false]` - whether any retries occurred +* `retryResult` = `[valueReturned\|exceptionNotRetryable\|maxRetriesReached\|maxDurationReached]` - the reason that last attempt to call the method was not retried +|=== + +[cols="1,5"] +|=== +| Name | `ft.retry.retries.total` +| Type +a| * MP Metrics: `Counter` +* OpenTelemetry: `LongCounter` +* Micrometer: `Counter` +| Unit | None +| Description | The number of times the method was retried +| Tags +a| * `method` - the fully qualified method name +|=== + +See xref:reference/metrics.adoc[the Metrics reference guide] for general metrics information. + +== Extra Features + +=== Backoff Strategies for `@Retry` + +include::{partialsdir}/srye-feature.adoc[] + +When retrying failed operations, it is often useful to make a delay between retry attempts. +This delay is also called "backoff". +The `@Retry` annotation in {microprofile-fault-tolerance} supports a single backoff strategy: constant. +That is, the delay between all retry attempts is identical (except for a random jitter). + +{smallrye-fault-tolerance} offers 3 annotations to specify a different backoff strategy: + +* `@ExponentialBackoff` +* `@FibonacciBackoff` +* `@CustomBackoff` + +One of these annotations may be present on any program element (method or class) that also has the `@Retry` annotation. +For example: + +[source,java] +---- +package com.example; + +@ApplicationScoped +public class MyService { + @Retry + @ExponentialBackoff + public void hello() { + ... + } +} +---- + +It is an error to add a backoff annotation to a program element that doesn't have `@Retry` (e.g. add `@Retry` on a class and `@ExponentialBackoff` on a method). +It is also an error to add more than one of these annotations to the same program element. + +When any one of these annotations is present, it modifies the behavior specified by the `@Retry` annotation. +The new behavior is as follows: + +For `@ExponentialBackoff`, the delays between retry attempts grow exponentially, using a defined `factor`. +By default, the `factor` is 2, so each delay is 2 * the previous delay. +For example, if the initial delay (specified by `@Retry`) is 1 second, then the second delay is 2 seconds, third delay is 4 seconds, fourth delay is 8 seconds etc. +It is possible to define a `maxDelay`, so that this growth has a limit. + +For `@FibonacciBackoff`, the delays between retry attempts grow per the Fibonacci sequence. +For example, if the initial delay (specified by `@Retry`) is 1 second, then the second delay is 2 seconds, third delay is 3 seconds, fourth delay is 5 seconds etc. +It is possible to define a `maxDelay`, so that this growth has a limit. + +Both `@ExponentialBackoff` and `@FibonacciBackoff` also apply jitter, exactly like plain `@Retry`. + +Also, since `@Retry` has a default `maxDuration` of 3 minutes and default `maxRetries` of 3, both `@ExponentialBackoff` and `@FibonacciBackoff` define a `maxDelay` of 1 minute. +If we redefine `maxRetries` to a much higher value, and the guarded method keeps failing, the delay would eventually become higher than 1 minute. +In that case, it will be limited to 1 minute. +Of course, `maxDelay` can be configured. +If set to `0`, there's no limit, and the delays will grow without bounds. + +For `@CustomBackoff`, computing the delays between retry attempts is delegated to a specified implementation of `CustomBackoffStrategy`. +This is an advanced option. + +For more information about these backoff strategies, see the javadoc of the annotations. + +=== Predicate-Based `@Retry` + +include::{partialsdir}/srye-feature.adoc[] + +By default, an operation is retried only if it fails and the exception is assignable to one of the classes defined in `@Retry.retryOn` (and not assignable to any of the classes defined in `@Retry.abortOn`). +With the `@RetryWhen` annotation, it is possible to instead define a custom predicate for the exception, as well as a predicate for the result. + +The `@RetryWhen` annotation may be present on any program element (method or class) that also has the `@Retry` annotation. +For example: + +[source,java] +---- +package com.example; + +@ApplicationScoped +public class MyService { + @Retry + @RetryWhen( + result = IsNull.class, // <1> + exception = IsRuntimeException.class // <2> + ) + public String hello() { + ... + } + + public static final class IsNull implements Predicate { + @Override + public boolean test(Object o) { + return o == null; + } + } + + public static final class IsRuntimeException implements Predicate { + @Override + public boolean test(Throwable throwable) { + return throwable instanceof RuntimeException; + } + } +} +---- + +<1> When the method returns `null`, it will be retried. +<2> When the method throws a `RuntimeException`, it will be retried. + +All other results are considered expected and are not retried. + +It is an error to specify `@RetryWhen.exception` if `@Retry.retryOn` / `@Retry.abortOn` is specified. +Specifying `@RetryWhen.result` together with `@Retry.retryOn` / `@Retry.abortOn` is possible, although perhaps not the best idea. + +It is an error to add a `@RetryWhen` annotation to a program element that doesn't have `@Retry` (e.g. add `@Retry` on a class and `@RetryWhen` on a method). + +For more information about `@RetryWhen`, see the javadoc of the annotation. + +=== Before Retry Actions + +include::{partialsdir}/srye-feature.adoc[] + +The `@BeforeRetry` annotation supports specifying an action that should be performed before any retry. +The action will _not_ be performed before the initial attempt. + +The `@BeforeRetry` annotation may be present on any program element (method or class) that also has the `@Retry` annotation. +For example: + +[source,java] +---- +package com.example; + +@ApplicationScoped +public class MyService { + @Retry + @BeforeRetry(methodName = "beforeRetry") // <1> + public String hello() { + ... + } + + public void beforeRetry() { // <2> + ... + } +} +---- + +<1> Specifies that before retrying, the `beforeRetry` method should be called. +<2> The method must return `void` and have no parameters. + +Alternatively, it is possible to specify a class that is an implementation of the `BeforeRetryHandler` interface: + +[source,java] +---- +package com.example; + +@ApplicationScoped +public class MyService { + @Retry + @BeforeRetry(MyBeforeRetry.class) // <1> + public String hello() { + ... + } + + public static class MyBeforeRetry implements BeforeRetryHandler { + public void handle(ExecutionContext context) { + ... + } + } +} +---- + +<1> Specifies that the `MyBeforeRetry` handler should be called before retrying. + +It is only possible to specify one option; specifying both `value` and `methodName` leads to a deployment problem. + +Note that the before retry action must fast and non-blocking (i.e., it must not do any IO or long-running computations) and must not throw an exception. + +[[inspecting-exception-cause-chains]] +=== Inspecting Exception Cause Chains + +include::{partialsdir}/srye-feature.adoc[] + +include::{partialsdir}/non-compat.adoc[] + +The `@Retry` annotation can specify that certain exceptions should be treated as failures (`retryOn`) and others as successes (`abortOn`). +The specification limits this to inspecting the actual exception that was thrown. +However, in many cases, exceptions are wrapped and the exception the user wants to decide on is only present in the cause chain. + +For that reason, in the non-compatible mode, if the actual thrown exception isn't known failure or known success, {smallrye-fault-tolerance} inspects the cause chain. +To be specific, in case a `@Retry` method throws an exception, the decision process is: + +1. If the `abortOn` exceptions are not default and the exception is assignable to one of the `abortOn` exceptions, retry is aborted and the exception is rethrown. +2. Otherwise, if the `retryOn` exceptions are not default and the exception is assignable to one of the `retryOn` exceptions, retry is attempted. +3. Otherwise, if the exception is assignable to one of the `abortOn` exceptions or its cause chain contains an exception assignable to one of the `abortOn` exceptions, retry is aborted and the exception is rethrown. +4. Otherwise, if the exception is assignable to one of the `retryOn` exceptions or its cause chain contains an exception assignable to one of the `retryOn` exceptions, retry is attempted. +5. Otherwise, the exception is rethrown. + +For example: + +[source,java] +---- +@Retry(maxRetries = 5, + abortOn = ExpectedOutcomeException.class, // <1> + retryOn = IOException.class) // <2> +public Result doSomething() { + ... +} +---- + +<1> If `doSomething` throws an `ExpectedOutcomeException`, or a `WrapperException` whose cause is `ExpectedOutcomeException`, retry is aborted and the exception is thrown. +<2> If `doSomething` throws an `IOException`, or a `WrapperException` whose cause is `IOException`, retry is attempted. + +[source,java] +---- +@Retry(maxRetries = 5, + abortOn = ExpectedOutcomeException.class) // <1> <2> +public Result doSomething() { + ... +} +---- + +<1> If `doSomething` throws an `ExpectedOutcomeException`, or a `WrapperException` whose cause is `ExpectedOutcomeException`, retry is aborted and the exception is thrown. +<2> There's no `retryOn`, so the 2nd step in the algorithm above is skipped. +This is what turns the `WrapperException` whose cause is `ExpectedOutcomeException` into an aborted retry. diff --git a/content/docs/smallrye-fault-tolerance/6/reference/reusable.adoc b/content/docs/smallrye-fault-tolerance/6/reference/reusable.adoc new file mode 100644 index 000000000..9a3379455 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/6/reference/reusable.adoc @@ -0,0 +1,244 @@ += Reusable Fault Tolerance +:version-root: ../ +include::{version-root}_attributes.adoc[] + +include::{partialsdir}/srye-feature.adoc[] + +The declarative, annotation-based API of {microprofile-fault-tolerance} doesn't allow sharing configuration of fault tolerance strategies across multiple classes. +In a single class, the configuration may be shared across all methods by putting the annotations on the class instead of individual methods, but even then, _stateful_ fault tolerance strategies are not shared. +Each method has its own bulkhead, circuit breaker and/or rate limit, which is often not what you want. + +The xref:reference/programmatic-api.adoc[programmatic API] of {smallrye-fault-tolerance} allows using a single `Guard` or `TypedGuard` object to guard multiple disparate actions, which allows reuse and state sharing. +It is possible to use a programmatically constructed `Guard` or `TypedGuard` object declaratively, using the `@ApplyGuard` annotation. + +To be able to do that, we need a bean of type `Guard` (or `TypedGuard`) with the `@Identifier` qualifier: + +[source,java] +---- +@ApplicationScoped +public class PreconfiguredFaultTolerance { + @Produces + @Identifier("my-fault-tolerance") + public static final Guard GUARD = Guard.create() + .withRetry().maxRetries(2).done() + .withTimeout().done() + .build(); +} +---- + +See the xref:reference/programmatic-api.adoc[programmatic API] documentation for more information about creating the `Guard` or `TypedGuard` instance. + +It is customary to create the bean by declaring a `static` producer field, just like in the previous example. + +Once we have that, we can apply `my-fault-tolerance` to any method: + +[source,java] +---- +@ApplicationScoped +public class MyService { + @ApplyGuard("my-fault-tolerance") + public String doSomething() { + ... + } + + @ApplyGuard("my-fault-tolerance") + public CompletionStage doSomethingElse() { + ... + } +} +---- + +== Defining Fallback + +Note that it is not possible to define a fallback on `Guard`, because fallback is tied to the action type. +It is possible to define a fallback on `TypedGuard`, because it can only be used to guard methods with a single return type, equal to the type the `TypedGuard` was created with. + +However, the `@ApplyGuard` annotation pays attention to the `@Fallback` annotation. +If `@Fallback` is defined, it is used both by `Guard` and `TypedGuard` instances, and it overrides the possible fallback defined on the `TypedGuard`. + +== Defining Thread Offload + +Both `Guard` and `TypedGuard` allow enabling or disabling thread offload. +This is ignored by `@ApplyGuard` if the annotated method is asynchronous as determined from the method signature and possible annotations (`@Asynchronous` and `@AsynchronousNonBlocking`). +See xref:reference/asynchronous.adoc[] for more information about how that determination works. + +The thread offload configuration of `Guard` or `TypedGuard` is only honored when the method cannot be determined to be asynchronous, but it still declares an asynchronous return type. + +.Compatible Mode +**** +In the compatible mode, methods are determined to be asynchronous when they are annotated `@Asynchronous` or `@AsynchronousNonBlocking` and they declare an asynchronous return type. +For such methods, the `Guard` / `TypedGuard` thread offload configuration does not apply. + +For methods that declare an asynchronous return type but are not annotated with `@Asynchronous` or `@AsynchronousNonBlocking`, the configuration on `Guard` / `TypedGuard` is honored. +**** + +.Non-compatible Mode +**** +In the non-compatible mode, methods are determined to be asynchronous whenever they declare an asynchronous return type. +The `@Asynchronous` and `@AsynchronousNonBlocking` only affect whether the invocation of that method is offloaded to an extra thread. +If none of these annotations is present, no thread offload happens. + +In other words, in non-compatible mode, the `Guard` / `TypedGuard` thread offload configuration never applies. + +See xref:reference/non-compat.adoc[] for more information about the non-compatible mode. +**** + +== Configuration + +Even though the programmatic API of `Guard` and `TypedGuard` does not support configuration, the declarative API of `@ApplyGuard` does. + +The configuration closely resembles xref:reference/config.adoc[the configuration of the declarative API], where instead of the `` or `/`, you use the identifier that's used in the `@Identifier` qualifier and in the `@ApplyGuard` annotation. + +CAUTION: When configuring the `Guard` or `TypedGuard` using the following configuration properties, it is recommended to never use the `[Typed]Guard` programmatically. +The reason is that configuration is applied on the first invocation, and if that invocation is programmatic, the configuration key is unknown. + +For example, let's assume the following preconfigured `Guard` object: + +[source,java] +---- +@ApplicationScoped +public class PreconfiguredFaultTolerance { + @Produces + @Identifier("my-fault-tolerance") + public static final Guard GUARD = Guard.create() + .withRetry().maxRetries(2).done() + .withTimeout().done() + .build(); +} +---- + +This `Guard` supports retry and timeout; any attempt to configure other strategies will be ignored. + +To reconfigure the number of maximum retries, the following configuration properties may be used: + +[source,properties] +---- +smallrye.faulttolerance."my-fault-tolerance".retry.max-retries=5 + +# alternatively, a specification-defined property can be used +my-fault-tolerance/Retry/maxRetries=5 +---- + +Global configuration that applies to all usages of retry is also respected by `@ApplyGuard`: + +[source,properties] +---- +smallrye.faulttolerance.global.retry.max-retries=5 + +# alternatively, a specification-defined property can be used +Retry/maxRetries=5 +---- + +Note that the configuration keys follow the configuration of the declarative API, even if the methods in the programmatic API builders are named differently. +For example, to set the rate limit in the programmatic API, the `RateLimitBuilder` uses a method called `limit()`, while the corresponding annotation member of `@RateLimit` is named `value()`. +The configuration key in both cases is `rate-limit.value` (or `RateLimit/value`). + +=== Disabling Fault Tolerance + +Configuration properties for disabling individual strategies also work: + +[source,properties] +---- +smallrye.faulttolerance."my-fault-tolerance".retry.enabled=false + +# alternatively, a specification-defined property can be used +my-fault-tolerance/Retry/enabled=false +---- + +Or globally: + +[source,properties] +---- +smallrye.faulttolerance.global.retry.enabled=false + +# alternatively, a specification-defined property can be used +Retry/enabled=false +---- + +=== Supported Strategies + +The following fault tolerance strategies, when defined on a `Guard` (or `TypedGuard`) and used through `@ApplyGuard`, may be configured: + +* bulkhead (except of `BulkheadBuilder.enableSynchronousQueueing()`) +* circuit breaker +* rate limit +* retry, including exponential backoff and Fibonacci backoff +* timeout + +In the programmatic API, the following strategies are not built declaratively, and so their configuration is ignored: + +* fallback +* circuit breaker name (cannot be configured in the declarative API either) +* certain parts of retry: custom backoff, before retry action and retry predicates +* thread offload + +== Metrics + +Methods annotated `@ApplyGuard` gather metrics similarly to methods annotated with {microprofile-fault-tolerance} annotations. +That is, each method gets its own metrics, with the `method` tag being `.`. + +At the same time, state is still shared. +All methods annotated `@ApplyGuard` share the same bulkhead, circuit breaker and/or rate limit. + +If the `Guard` or `TypedGuard` object used for `@ApplyGuard` is also used xref:reference/programmatic-api.adoc[programmatically], that usage is coalesced in metrics (where the `method` tag is set to the description). + +== Differences to the Specification + +`@ApplyGuard` has the same differences to standard MicroProfile Fault Tolerance as `Guard` / `TypedGuard`: + +* asynchronous actions of type `java.util.concurrent.Future` are not supported; +* the fallback, circuit breaker and retry strategies always inspect the cause chain of exceptions, following the behavior of SmallRye Fault Tolerance in the non-compatible mode. + +== Kotlin `suspend` Functions + +Even though the programmatic API of `Guard` and `TypedGuard` does not support Kotlin `suspend` functions, the declarative API of `@ApplyGuard` does. +When the guard is a `Guard`, no restrictions apply. + +When the guard is a `TypedGuard`, however, its type must be a synchronous return type of the `suspend` function. +For example, when the `suspend` function is declared to return a `String` asynchronously: + +[source,kotlin] +---- +@ApplyGuard("my-fault-tolerance") +@Fallback(fallbackMethod = "fallback") +suspend fun hello(): String { + delay(100) + throw IllegalArgumentException() +} +---- + +The `TypedGuard` must be declared to guard actions of type `String`: + +[source,kotlin] +---- +@Produces +@Identifier("my-fault-tolerance") +val GUARD = TypedGuard.create(String::class.java) + .withRetry().maxRetries(2).done() + .withFallback().handler(Supplier { "fallback" }).done() + .build() +---- + +This means that a possible fallback declared on the `TypedGuard` must be synchronous; it cannot be a `suspend` lambda. + +The `@Fallback` method, if declared, must have a matching signature and so must be a `suspend` function: + +[source,kotlin] +---- +suspend fun fallback(): String { + delay(100) + return "fallback" +} +---- + +[[migration_from_applyfaulttolerance]] +== Migration from `@ApplyFaultTolerance` + +The 1st version of the programmatic API had the `@ApplyFaultTolerance` annotation. +That annotation is deprecated and scheduled for removal in {smallrye-fault-tolerance} 7.0. + +To migrate, replace `@ApplyFaultTolerance` with `@ApplyGuard` and change the `FaultTolerance<>` producers to produce `Guard` or `TypedGuard<>`. +See xref:reference/programmatic-api.adoc#migration_from_faulttolerance[the programmatic API migration guide] for more details about that. + +Note that it is not possible to define both `Guard` and `TypedGuard<>` with the same identifier; that leads to a deployment problem. +Therefore, for each producer of `FaultTolerance<>`, you have to decide whether the replacement should be `Guard` or `TypedGuard<>`. diff --git a/content/docs/smallrye-fault-tolerance/6/reference/timeout.adoc b/content/docs/smallrye-fault-tolerance/6/reference/timeout.adoc new file mode 100644 index 000000000..1dd6b9037 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/6/reference/timeout.adoc @@ -0,0 +1,81 @@ += Timeout +:version-root: ../ +include::{version-root}_attributes.adoc[] + +For introduction, see xref:howto/timeout.adoc[How to Watch for Timeouts]. + +== Description + +If a guarded method takes too long, the caller will get a `TimeoutException`. + +If the guarded method is not `@Asynchronous` or `@AsynchronousNonBlocking`, the thread executing the method is interrupted. +Note that interruption requires cooperation from the executing code (for example, it must not swallow `InterruptedException`). +In other words, it is not guaranteed that interrupting the method will actually make it stop. + +If the guarded method is `@Asynchronous` or `@AsynchronousNonBlocking`, timeout is always signalled correctly, but there is no cancellation. +Even after timeout, the asynchronous action will keep running; its eventual result is just ignored. + +[[interactions]] +== Interactions with Other Strategies + +See xref:howto/multiple.adoc[How to Use Multiple Strategies] for an overview of how fault tolerance strategies are nested. + +If `@Fallback` is used with `@Timeout`, the fallback method or handler may be invoked if a `TimeoutException` is thrown, depending on the fallback configuration. + +If `@Retry` is used with `@Timeout`, each retry attempt is processed by the timeout as an independent invocation. +If `TimeoutException` is thrown, the execution may be retried, depending on how retry is configured. + +If `@CircuitBreaker` is used with `@Timeout`, the circuit breaker is checked before enforcing the timeout. +If enforcing the timeout results in `TimeoutException`, this may be counted as a failure, depending on how the circuit breaker is configured. + +If `@RateLimit` is used with `@Timeout`, the rate limit is enforced before enforcing the timeout. + +[[configuration]] +== Configuration + +There are 2 configuration options, corresponding to the 2 members of the `@Timeout` annotation. + +=== `value` + `unit` + +Type: `long` + `ChronoUnit` + +Default: `1000 millis`, or 1 second + +The timeout to enforce. + +[[metrics]] +== Metrics + +Timeout exposes the following metrics: + +[cols="1,5"] +|=== +| Name | `ft.timeout.calls.total` +| Type +a| * MP Metrics: `Counter` +* OpenTelemetry: `LongCounter` +* Micrometer: `Counter` +| Unit | None +| Description | The number of times the timeout logic was run. This is usually once per method call, but may be zero times if the circuit breaker or rate limit prevents execution or more than once if the method is retried. +| Tags +a| * `method` - the fully qualified method name +* `timedOut` = `[true\|false]` - whether the method call timed out +|=== + +[cols="1,5"] +|=== +| Name | `ft.timeout.executionDuration` +| Type +a| * MP Metrics: `Histogram` +* OpenTelemetry: `DoubleHistogram` with explicit bucket boundaries `[0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10]` +* Micrometer: `Timer` +| Unit +a| * MP Metrics: nanoseconds +* OpenTelemetry: seconds +* Micrometer: nanoseconds +| Description | Histogram of execution times for the method +| Tags +a| * `method` - the fully qualified method name +|=== + +See xref:reference/metrics.adoc[the Metrics reference guide] for general metrics information. diff --git a/content/docs/smallrye-fault-tolerance/latest/_attributes.adoc b/content/docs/smallrye-fault-tolerance/latest/_attributes.adoc new file mode 100644 index 000000000..231612638 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/latest/_attributes.adoc @@ -0,0 +1,24 @@ +:project-version: 7.0.0 +// Attributes shared by every page. smallrye.io overrides the ones it needs when it publishes. +// Pages in subdirectories set :version-root: (../ per level) before including this file. +ifndef::version-root[:version-root:] +:idprefix: +:idseparator: - +:sectanchors: +:partialsdir: {version-root}_partials +:examples-dir: {version-root}_examples +:smallrye-fault-tolerance: SmallRye Fault Tolerance +:smallrye-fault-tolerance-version: 7.0.0 +:microprofile-fault-tolerance: MicroProfile Fault Tolerance +:microprofile-fault-tolerance-version: 4.1.2 +:microprofile-fault-tolerance-url: https://download.eclipse.org/microprofile/microprofile-fault-tolerance-4.1/microprofile-fault-tolerance-spec-4.1.html +:vertx4-version: 4.5.25 +:generated-dir: {version-root}_generated + +// Added by smallrye.io (tools/docs/Docs.java sync) +:javadoc-version: 7.0.0 +:relfileprefix: /docs/smallrye-fault-tolerance/latest/ +:imagesdir: /docs/smallrye-fault-tolerance/latest +:docs-project: smallrye-fault-tolerance +:docs-version: latest +:docs-ref: 7.0.0 diff --git a/content/docs/smallrye-fault-tolerance/latest/_nav.yml b/content/docs/smallrye-fault-tolerance/latest/_nav.yml new file mode 100644 index 000000000..1c25b6497 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/latest/_nav.yml @@ -0,0 +1,80 @@ +title: SmallRye Fault Tolerance +nav: +- title: Overview + page: index +- title: How-to + items: + - title: Begin + page: howto/begin + - title: Supply Fallback Values + page: howto/fallback + - title: Limit Concurrency + page: howto/bulkhead + - title: Fail Fast on Recurring Failures + page: howto/circuit-breaker + - title: Watch for Timeouts + page: howto/timeout + - title: Retry on Failures + page: howto/retry + - title: Guard Asynchronous Methods + page: howto/asynchronous + - title: Limit Execution Rate + page: howto/rate-limit + - title: Use Multiple Strategies + page: howto/multiple +- title: Reference + items: + - title: Fallback + page: reference/fallback + - title: Bulkhead + page: reference/bulkhead + - title: Circuit Breaker + page: reference/circuit-breaker + - title: Timeout + page: reference/timeout + - title: Retry + page: reference/retry + - title: Asynchronous Execution + page: reference/asynchronous + - title: Rate Limit + page: reference/rate-limit + - title: Configuration + page: reference/config + - title: Metrics + page: reference/metrics + - title: Non-compatible Mode + page: reference/non-compat + - title: Programmatic API + page: reference/programmatic-api + - title: Reusable Fault Tolerance + page: reference/reusable +- title: Integration + items: + - title: Introduction + page: integration/intro + - title: Metrics + page: integration/metrics + - title: Thread Pool + page: integration/thread-pool + - title: Context Propagation + page: integration/context-propagation + - title: Event Loop + page: integration/event-loop + - title: Additional Asynchronous Types Integration Concerns + page: integration/async-types + - title: Kotlin Integration Concerns + page: integration/kotlin + - title: Programmatic API Integration Concerns + page: integration/programmatic-api +- title: Internals + items: + - title: Project Structure + page: internals/project-structure + - title: Instructions + page: internals/instructions + - title: Fault Tolerance Core + page: internals/core + - title: Configuration System + page: internals/config + - title: Logging + page: internals/logging diff --git a/content/docs/smallrye-fault-tolerance/latest/_partials/internals.adoc b/content/docs/smallrye-fault-tolerance/latest/_partials/internals.adoc new file mode 100644 index 000000000..6a5d9340f --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/latest/_partials/internals.adoc @@ -0,0 +1,3 @@ +WARNING: Note that everything explained here is an implementation detail. +API stability for these interfaces and classes is _not_ guaranteed! +This documentation is only present to provide implementation insight for people who have to debug issues or want to contribute. diff --git a/content/docs/smallrye-fault-tolerance/latest/_partials/non-compat.adoc b/content/docs/smallrye-fault-tolerance/latest/_partials/non-compat.adoc new file mode 100644 index 000000000..e087cdb10 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/latest/_partials/non-compat.adoc @@ -0,0 +1 @@ +IMPORTANT: This feature only works in the xref:reference/non-compat.adoc[non-compatible mode]. diff --git a/content/docs/smallrye-fault-tolerance/latest/_partials/srye-feature.adoc b/content/docs/smallrye-fault-tolerance/latest/_partials/srye-feature.adoc new file mode 100644 index 000000000..0f4a979cf --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/latest/_partials/srye-feature.adoc @@ -0,0 +1 @@ +IMPORTANT: This is an additional feature of {smallrye-fault-tolerance} and is not specified by {microprofile-fault-tolerance}. diff --git a/content/docs/smallrye-fault-tolerance/latest/_partials/time-unit-inconsistency.adoc b/content/docs/smallrye-fault-tolerance/latest/_partials/time-unit-inconsistency.adoc new file mode 100644 index 000000000..b742c482b --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/latest/_partials/time-unit-inconsistency.adoc @@ -0,0 +1,3 @@ +NOTE: Note the naming inconsistency between `{time-value-name}` and `{time-unit-bad-name}`. +It is common for the time unit property to be named after the time value property, e.g. `{time-unit-good-name}`. +Fixing this inconsistency would be a breaking change the {microprofile-fault-tolerance} specification is not willing to make. diff --git a/content/docs/smallrye-fault-tolerance/latest/howto/asynchronous.adoc b/content/docs/smallrye-fault-tolerance/latest/howto/asynchronous.adoc new file mode 100644 index 000000000..4c8394666 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/latest/howto/asynchronous.adoc @@ -0,0 +1,102 @@ += How to Guard Asynchronous Methods +:version-root: ../ +include::{version-root}_attributes.adoc[] + +{smallrye-fault-tolerance} allows offloading method calls to another thread, as well as applying asynchronous fault tolerance. + +Asynchronous fault tolerance means guarding not just the synchronous method execution, but the entire asynchronous operation started by the method call, until the result is delivered. +For example: + +[source,java] +---- +CompletionStage doSomething() { + return CompletableFuture.supplyAsync(() -> { + if (someCondition) { + return someValue; + } else { + throw someException; + } + }); +} +---- + +This method always returns a `CompletionStage` and it never throws an exception. +However, if `someCondition` does not hold, the `CompletionStage` returned from the method will complete exceptionally. + +Ordinary (non-asynchronous) fault tolerance: + +* never considers this operation failing, because the method returns a value and does not throw an exception; +* considers this operation finished when the method returns. + +Asynchronous fault tolerance, on the other hand: + +* defers its decision process to the moment when the returned `CompletionStage` completes, +* and considers this operation running until that happens. + +As a special case, if an asynchronous method in fact does throw an exception (it typically shouldn't), it is treated as if it returned a `CompletionStage` that is already completed exceptionally with the thrown exception. + +== `@Asynchronous` + +A method that returns `java.util.concurrent.Future` or `java.util.concurrent.CompletionStage` may be annotated `@Asynchronous`. +Such method is executed on another thread. +Additionally, if the return type is `CompletionStage`, asynchronous fault tolerance applies. + +[NOTE] +==== +Note that if an `@Asynchronous` method returns a `Future`, asynchronous fault tolerance does _not_ apply. +Only the synchronous method execution is guarded. + +Asynchronous fault tolerance only applies if the `@Asynchronous` method returns `CompletionStage`. +==== + +Use `@Asynchronous` if the method has blocking logic, but you don’t want to block the caller thread. +For example: + +[source,java] +---- +@ApplicationScoped +public class MyService { + @Retry + @Asynchronous // <1> + CompletionStage hello() { + ... blocking IO here ... + } +} +---- + +<1> Using the `@Asynchronous` annotation, because the method blocks, and it is necessary to offload its execution to another thread. + +== `@AsynchronousNonBlocking` + +include::{partialsdir}/srye-feature.adoc[] + +A method that returns `CompletionStage` may be annotated `@AsynchronousNonBlocking`. +In such case, the method is executed on the original thread, but asynchronous fault tolerance still applies. + +Use `@AsynchronousNonBlocking` if the method _does not_ have blocking logic, and you want the execution to stay on the caller thread. +For example: + +[source,java] +---- +@ApplicationScoped +public class MyService { + @Retry + @AsynchronousNonBlocking // <1> + CompletionStage hello() { + ... non-blocking IO here ... + } +} +---- + +<1> Using the `@AsynchronousNonBlocking` annotation, because the method does not block and offloading execution to another thread is not necessary. + +=== Non-compatible mode + +include::{partialsdir}/non-compat.adoc[] + +In the non-compatible mode, the `@AsynchronousNonBlocking` annotation is not necessary. +A method that returns `CompletionStage` and is not annotated `@Asynchronous` is automatically treated as `@AsynchronousNonBlocking`. + +== Links + +For more information, see xref:reference/asynchronous.adoc[the Asynchronous Execution reference guide]. diff --git a/content/docs/smallrye-fault-tolerance/latest/howto/begin.adoc b/content/docs/smallrye-fault-tolerance/latest/howto/begin.adoc new file mode 100644 index 000000000..3b5f0f3e7 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/latest/howto/begin.adoc @@ -0,0 +1,129 @@ += How to Begin +:version-root: ../ +include::{version-root}_attributes.adoc[] + +== Installation + +As an application developer, you don't have to "install" {smallrye-fault-tolerance} in any way. +Your chosen runtime, such as Quarkus or WildFly, already integrates {smallrye-fault-tolerance} to provide {microprofile-fault-tolerance}. +Please refer to the documentation of your selected runtime to understand how to enable MicroProfile Fault Tolerance. + +.Quarkus +**** +In case of Quarkus, it is enough to add a dependency on `io.quarkus:quarkus-smallrye-fault-tolerance`. + +See the https://quarkus.io/guides/smallrye-fault-tolerance[{smallrye-fault-tolerance} guide] in Quarkus documentation for more. +**** + +.WildFly +**** +In case of WildFly, the {microprofile-fault-tolerance} subsystem must be enabled. +It is not included in the default `standalone` or `standalone-full` profiles, but it is included in the `standalone-microprofile` profile. +It is easiest to just start WildFly in this profile. + +See the https://docs.wildfly.org/27/Admin_Guide.html#MicroProfile-Fault-Tolerance-SmallRye[{microprofile-fault-tolerance} chapter] in WildFly Admin Guide for more. +**** + +If you are a framework developer, interested in integrating {smallrye-fault-tolerance} in your framework or runtime, please refer to the xref:integration/intro.adoc[Integration] section. + +== Usage + +{microprofile-fault-tolerance} is built on top of CDI and interceptors. +This lets you guard method calls with selected fault tolerance strategies simply by annotating a CDI bean class or method with one of the fault tolerance annotations. +For example: + +[source,java] +---- +@ApplicationScoped // <1> +public class MyService { + @Retry // <2> + public String hello() { + ... + } +} +---- + +<1> `@ApplicationScoped` is a bean defining annotation. +It makes the `MyService` class a CDI bean. +<2> `@Retry` is a fault tolerance annotation. + +Here, if the `hello` method throws an exception, the invocation will be retried several times, until the method returns a value. + +The annotation may also be present on a class, in which case it applies to all business method in the class: + +[source,java] +---- +package com.example; + +@ApplicationScoped +@Retry // <1> +public class MyService { + public String hello() { // <2> + ... + } +} +---- +<1> `@Retry` is a fault tolerance annotation. +<2> The `hello()` method will use the retry strategy, because it is defined on the class. + +A class-level annotation may be overridden by explicitly annotating a method. + +TIP: CDI beginners often expect that this will work if you create an instance manually: `new MyService()`. +That is not the case. +You have to let the CDI container construct an instance for you and inject it: `@Inject MyService service`. +What you get injected is actually a proxy that implements the additional behaviors. + +The fault tolerance strategies present in {microprofile-fault-tolerance}, together with the corresponding annotations, are: + +* `@Asynchronous`: offload method execution to another thread +* `@Bulkhead`: limit concurrent invocations +* `@CircuitBreaker`: prevent invocations if previous invocations failed too often +* `@Fallback`: provide alternative result in case of a failure +* `@Retry`: retry several times in case of a failure +* `@Timeout`: fail if the invocation takes too long + +{smallrye-fault-tolerance} adds some more fault tolerance strategies and annotations: + +* `@ApplyGuard`: apply a programmatically-defined guard +* `@AsynchronousNonBlocking`: apply asynchronous fault tolerance without offloading method execution to another thread +* `@RateLimit`: limit rate of method invocations + +These are all described in the corresponding how-to and reference guides. +The {microprofile-fault-tolerance} features are also described in the {microprofile-fault-tolerance-url}[specification]. + +== Configuration + +The fault tolerance strategies can be configured using annotations, such as: + +[source,java] +---- +@ApplicationScoped +public class MyService { + @Retry(maxRetries = 10, retryOn = IOException.class) // <1> + public String hello() { + ... + } +} +---- + +<1> Retries will only be attempted if the thrown exception was `IOException`. +Other exceptions will be rethrown directly. +Also, the maximum number of retry attempts is changed to 10 (the default is 3). + +This is convenient, but changing such configuration requires recompilation. +For that reason, configuration using MicroProfile Config is also possible. +For example: + +[source,properties] +---- +smallrye.faulttolerance."com.example.MyService/hello".retry.max-retries=5 <1> + +# alternatively, a specification-defined property can be used +com.example.MyService/hello/Retry/maxRetries=5 <1> +---- +<1> Even though the `@Retry` annotation says that `maxRetries` should be 10, this configuration takes precedence. +The maximum number of retries will be 5. + +Since this example configuration does not modify the `retryOn` setting, the configuration provided in the annotation still applies. + +For more information about configuration, see xref:reference/config.adoc[the Configuration reference guide]. diff --git a/content/docs/smallrye-fault-tolerance/latest/howto/bulkhead.adoc b/content/docs/smallrye-fault-tolerance/latest/howto/bulkhead.adoc new file mode 100644 index 000000000..39326779c --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/latest/howto/bulkhead.adoc @@ -0,0 +1,33 @@ += How to Limit Concurrency +:version-root: ../ +include::{version-root}_attributes.adoc[] + +It is possible to prevent an operation from having too many concurrent executions using a _bulkhead_. + +NOTE: In general, the term _bulkhead_ refers to system compartmentalization, or the practice of preventing failures in one part of the system from spreading to other parts of the system. +In {microprofile-fault-tolerance}, the term refers to one specific instance of the general idea: a _concurrency limiter_. + +== `@Bulkhead` + +The `@Bulkhead` annotation specifies that a concurrency limit should be enforced for all calls of the method. + +[source,java] +---- +@ApplicationScoped +public class MyService { + @Bulkhead // <1> + public String hello() { + ... + } +} +---- +<1> Declares that `hello()` must not have too many concurrent executions. +Since there is no configuration, the default is at most 10 concurrent calls. + +If some callers attempt to invoke the method when the concurrency limit has already been reached, they will fail with `BulkheadException`. + +It is possible to specify the maximum allowed concurrency, as well as define a queue for excess invocations in case of xref:howto/asynchronous.adoc[asynchronous methods]. + +== Links + +For more information, see xref:reference/bulkhead.adoc[the Bulkhead reference guide]. diff --git a/content/docs/smallrye-fault-tolerance/latest/howto/circuit-breaker.adoc b/content/docs/smallrye-fault-tolerance/latest/howto/circuit-breaker.adoc new file mode 100644 index 000000000..0dcd618ec --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/latest/howto/circuit-breaker.adoc @@ -0,0 +1,33 @@ += How to Fail Fast on Recurring Failures +:version-root: ../ +include::{version-root}_attributes.adoc[] + +If an operation fails too often, it is possible to prevent executing that operation and fail fast using a _circuit breaker_. + +The circuit breaker starts _closed_, in which state it allows all invocations to proceed. +If too many invocations fail, the circuit breaker moves to _open_, in which state it prevents all invocations and fails fast. +After some time in _open_, the circuit breaker moves to _half-open_, in which state it determines whether it should continue failing fast (and move back to _open_) or allow invocations again (and move to _closed_). + +== `@CircuitBreaker` + +The `@CircuitBreaker` annotation specifies that method invocations should be prevented if the method failed too often recently. + +[source,java] +---- +@ApplicationScoped +public class MyService { + @CircuitBreaker // <1> + public String hello() { + ... + } +} +---- +<1> Declares that `hello()` must be guarded with a circuit breaker. + +If some callers attempt to invoke the method when the circuit breaker is _open_, they will fail with `CircuitBreakerOpenException`. + +It is possible to configure the number of recent invocations the circuit breaker should track, the failure rate, and so on. + +== Links + +For more information, see xref:reference/circuit-breaker.adoc[the Circuit Breaker reference guide]. diff --git a/content/docs/smallrye-fault-tolerance/latest/howto/fallback.adoc b/content/docs/smallrye-fault-tolerance/latest/howto/fallback.adoc new file mode 100644 index 000000000..a0fb8826d --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/latest/howto/fallback.adoc @@ -0,0 +1,62 @@ += How to Supply Fallback Values +:version-root: ../ +include::{version-root}_attributes.adoc[] + +If an operation fails, it is often possible to replace the failure with an alternative result: a _fallback_. + +== `@Fallback` + +=== Using a Fallback Method + +The `@Fallback` annotation allows specifying a method that should be called in case an exception is thrown. +The fallback method must have the same parameter types as the guarded method, and must return a type that is assignable to the guarded method's return type. + +[source,java] +---- +@ApplicationScoped +public class MyService { + @Fallback(fallbackMethod = "fallback") // <1> + public String hello() { + ... + } + + private String fallback() { + return "fallback"; + } +} +---- +<1> Declares that if `hello()` throws an exception, the `fallback()` method is called and its result is returned instead. + +In this particular case, if `hello()` throws an exception, that exception is swallowed and the string `"fallback"` is returned. +It is possible to specify that fallback should only apply to certain exception types or that it should be skipped for certain exception types. + +=== Using a Fallback Handler + +Alternatively, the `@Fallback` annotation allows specifying a fallback handler that should be called. + +[source,java] +---- +@ApplicationScoped +public class MyService { + @Fallback(MyFallback.class) // <1> + public String hello() { + ... + } +} +---- +<1> Declares that if `hello()` throws an exception, the `MyFallback` handler is used to obtain a fallback value. + +[source,java] +---- +public class MyFallback implements FallbackHandler { // <1> + String handle(ExecutionContext context) { // <2> + return "fallback"; + } +} +---- +<1> A fallback handler must implement the `FallbackHandler` interface. +<2> The `ExecutionContext` provides access to the guarded method, its parameter values, and the thrown exception. + +== Links + +For more information, see xref:reference/fallback.adoc[the Fallback reference guide]. diff --git a/content/docs/smallrye-fault-tolerance/latest/howto/multiple.adoc b/content/docs/smallrye-fault-tolerance/latest/howto/multiple.adoc new file mode 100644 index 000000000..7ee19a470 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/latest/howto/multiple.adoc @@ -0,0 +1,58 @@ += How to Use Multiple Strategies +:version-root: ../ +include::{version-root}_attributes.adoc[] + +It is possible to use multiple fault tolerance strategies to guard a single method. +For example, it is possible to use fallback together with retry and timeout: + +[source,java] +---- +@ApplicationScoped +public class MyService { + @Retry(maxRetries = 5) + @Fallback(fallbackMethod = "fallback") + @Timeout + public String hello() { + ... + } + + public String fallback() { + return "fallback"; + } +} +---- + +In such case, the fault tolerance strategies are nested in a pre-defined order: + +[source] +---- +Fallback( + Retry( + CircuitBreaker( + RateLimit( + Timeout( + Bulkhead( + ... the guarded method ... + ) + ) + ) + ) + ) +) +---- + +In the previous example, this means that timeout is nested inside retry, which in turn is nested inside fallback. +If the method times out (the default timeout is 1 second), it is retried (up to 5 times). +If no attempt succeeds, fallback is used. + +== Links + +Reference guides for all fault tolerance strategies explain in detail how they interact with one another: + +* xref:reference/asynchronous.adoc#interactions[@Asynchronous and @AsynchronousNonBlocking] +* xref:reference/bulkhead.adoc#interactions[@Bulkhead] +* xref:reference/circuit-breaker.adoc#interactions[@CircuitBreaker] +* xref:reference/fallback.adoc#interactions[@Fallback] +* xref:reference/rate-limit.adoc#interactions[@RateLimit] +* xref:reference/retry.adoc#interactions[@Retry] +* xref:reference/timeout.adoc#interactions[@Timeout] diff --git a/content/docs/smallrye-fault-tolerance/latest/howto/rate-limit.adoc b/content/docs/smallrye-fault-tolerance/latest/howto/rate-limit.adoc new file mode 100644 index 000000000..c818bfdfe --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/latest/howto/rate-limit.adoc @@ -0,0 +1,33 @@ += How to Limit Execution Rate +:version-root: ../ +include::{version-root}_attributes.adoc[] + +include::{partialsdir}/srye-feature.adoc[] + +It is possible to prevent an operation from being executed too often using a _rate limit_. + +== `@RateLimit` + +The `@RateLimit` annotation specifies that a rate limit should be enforced for all method calls. + +[source,java] +---- +@ApplicationScoped +public class MyService { + @RateLimit // <1> + public String hello() { + ... + } +} +---- +<1> Declares that `hello()` must not be called too often. +Since there is no configuration, the default is at most 100 calls per 1 minute. + +If the method is called too often, excess attempts will fail with `RateLimitException`. + +It is possible to specify the maximum allowed rate of execution (the rate limit), together with minimum spacing between invocations. +There are also several types of rate limiting to choose from. + +== Links + +For more information, see xref:reference/rate-limit.adoc[the Rate Limit reference guide]. diff --git a/content/docs/smallrye-fault-tolerance/latest/howto/retry.adoc b/content/docs/smallrye-fault-tolerance/latest/howto/retry.adoc new file mode 100644 index 000000000..865403c37 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/latest/howto/retry.adoc @@ -0,0 +1,33 @@ += How to Retry on Failures +:version-root: ../ +include::{version-root}_attributes.adoc[] + +If an operation fails, it is possible to _retry_ the operation in case the failure was just a temporary glitch. + +CAUTION: In case an operation fails due to the target system being overloaded, retrying will likely make the overload situation even worse. +Use sparingly, preferably with a xref:howto/bulkhead.adoc[concurrency limit] or a xref:howto/circuit-breaker.adoc[circuit breaker]. + +== `@Retry` + +The `@Retry` annotation specifies that a method should be called again in case a previous execution thrown an exception. + +[source,java] +---- +@ApplicationScoped +public class MyService { + @Retry // <1> + public String hello() { + ... + } +} +---- +<1> Declares that if `hello()` throws an exception, the exception is swallowed and `hello()` is called again. +Since there is no configuration, at most 3 retry attempts will be done before rethrowing the exception. + +It is possible to specify the maximum number of retry attempts, as well as the maximum duration for which retries should be attempted. +Further, it is possible to specify some delay between retry attempts, together with random jitter. +Finally, it is possible to specify that calls should be retried only for certain exception types or that retrying should be aborted for certain exception types. + +== Links + +For more information, see xref:reference/retry.adoc[the Retry reference guide]. diff --git a/content/docs/smallrye-fault-tolerance/latest/howto/timeout.adoc b/content/docs/smallrye-fault-tolerance/latest/howto/timeout.adoc new file mode 100644 index 000000000..c3e9fdf8b --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/latest/howto/timeout.adoc @@ -0,0 +1,28 @@ += How to Watch for Timeouts +:version-root: ../ +include::{version-root}_attributes.adoc[] + +It is possible to prevent an operation from taking too long using a _timeout_. + +== `@Timeout` + +The `@Timeout` annotation specifies that all calls of the method should fail if they take too long. + +[source,java] +---- +@ApplicationScoped +public class MyService { + @Timeout // <1> + public String hello() { + ... + } +} +---- +<1> Declares that `hello()` must not take too long. +Since there is no configuration, the default is at most 1 second. + +If the method call takes too long, the caller will get a `TimeoutException` instead. + +== Links + +For more information, see xref:reference/timeout.adoc[the Timeout reference guide]. diff --git a/content/docs/smallrye-fault-tolerance/latest/images/diagrams/mermaid-f40d3cbcabca.svg b/content/docs/smallrye-fault-tolerance/latest/images/diagrams/mermaid-f40d3cbcabca.svg new file mode 100644 index 000000000..a31aed359 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/latest/images/diagrams/mermaid-f40d3cbcabca.svg @@ -0,0 +1 @@ +

Recent invocations failed too often

Configured time elapsed

All probe invocations succeeded

Some probe invocations failed

[CLOSED] Allow invocations and track the number of successes and failures

[OPEN] Fail fast, prevent all invocations

[HALF-OPEN] Allow some probe invocations to proceed

\ No newline at end of file diff --git a/content/docs/smallrye-fault-tolerance/latest/index.adoc b/content/docs/smallrye-fault-tolerance/latest/index.adoc new file mode 100644 index 000000000..c8eccf252 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/latest/index.adoc @@ -0,0 +1,77 @@ += SmallRye Fault Tolerance Documentation +include::_attributes.adoc[] + +{smallrye-fault-tolerance} {smallrye-fault-tolerance-version} is an implementation of https://github.com/eclipse/microprofile-fault-tolerance/[Eclipse {microprofile-fault-tolerance}] {microprofile-fault-tolerance-version} with additional features not defined by the specification. + +It provides a declarative, annotation-based API for guarding method calls: + +[source,java] +---- +@ApplicationScoped +public class MyService { + @Retry // <1> + public String hello() { + ... + } +} +---- +<1> If `hello()` throws an exception, the invocation will be retried several times, until the method returns a value. + +It also provides a programmatic API to build reusable guards: + +[source,java] +---- +public class MyService { + private static final TypedGuard GUARD = TypedGuard.create(String.class) + .withFallback().handler(() -> "fallback").done() // <1> + .build(); + + public String hello() throws Exception { + return GUARD.call(() -> externalService.hello()); // <2> + } +} +---- +<1> If the guarded call throws an exception, the fallback value will be returned instead. +<2> The guarded call simply invokes some external service. + +CAUTION: The fault tolerance strategies provided by {smallrye-fault-tolerance} are best suited for guarding network interactions. +To handle ordinary exceptions, it is typically best to use the plain old `try` / `catch` / `finally` blocks. +Using {smallrye-fault-tolerance} as a replacement of exception handling is often a bad idea. + +NOTE: Some additional features may have experimental status, marked by the `@Experimental` annotation. +The programmatic API to build reusable guards, as mentioned above, is one example, but there are more. + +== Runtimes + +Runtimes that currently use {smallrye-fault-tolerance} include: + +* https://quarkus.io/[Quarkus] +* https://wildfly.org/[WildFly] + +== Navigation + +This documentation site has several sections, as you can see in the menu. + +=== How-to Guides + +How-tos are task-oriented guides that let you quickly start using some feature. +They provide a short overview, but not many details. + +TIP: If you're just starting with {smallrye-fault-tolerance}, start with the how-tos. + +=== References + +References are detailed guides that let you understand some feature in depth. +They also provide information about more advanced options or features. + +TIP: If the how-to guide doesn't answer a question you have, the reference guide should. + +=== Integration + +The articles here address concerns that runtimes integrating {smallrye-fault-tolerance} might have. +Useful for people who maintain {smallrye-fault-tolerance} integration into some bigger framework or platform, but not required for application developers. + +=== Internals + +A few random articles that describe some parts of the {smallrye-fault-tolerance} code base. +Useful for prospective contributors or generally curious users, but not required for application developers. diff --git a/content/docs/smallrye-fault-tolerance/latest/integration/async-types.adoc b/content/docs/smallrye-fault-tolerance/latest/integration/async-types.adoc new file mode 100644 index 000000000..f4706c68c --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/latest/integration/async-types.adoc @@ -0,0 +1,15 @@ += Additional Asynchronous Types Integration Concerns +:version-root: ../ +include::{version-root}_attributes.adoc[] + +This page describes integration concerns for xref:reference/asynchronous.adoc#async-types[Additional Asynchronous Types]. + +To enable support of additional asynchronous types, it is required that the support library is present. +That is: + +* https://smallrye.io/smallrye-mutiny/[Mutiny]: `io.smallrye:smallrye-fault-tolerance-mutiny` +* https://github.com/ReactiveX/RxJava/tree/3.x[RxJava 3]: `io.smallrye:smallrye-fault-tolerance-rxjava3` +* https://vertx.io/docs/vertx-core/java/#_future_results[Vert.x `Future`]: `io.smallrye:smallrye-fault-tolerance-vertx` + +These libraries include some service providers that {smallrye-fault-tolerance} will automatically load and use. +Therefore, no more integration is necessary. diff --git a/content/docs/smallrye-fault-tolerance/latest/integration/context-propagation.adoc b/content/docs/smallrye-fault-tolerance/latest/integration/context-propagation.adoc new file mode 100644 index 000000000..a39b7816d --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/latest/integration/context-propagation.adoc @@ -0,0 +1,8 @@ += Context Propagation +:version-root: ../ +include::{version-root}_attributes.adoc[] + +{smallrye-fault-tolerance} supports MicroProfile Context Propagation for `@Asynchronous` method invocations, if 2 conditions are satisfied: + +- the integrator provides a MicroProfile Context Propagation enabled thread pool (see xref:integration/thread-pool.adoc[Thread Pool]); +- the `io.smallrye:smallrye-fault-tolerance-context-propagation` artifact is present (it provides alternative implementations of some internal services, loaded using `ServiceLoader`). diff --git a/content/docs/smallrye-fault-tolerance/latest/integration/event-loop.adoc b/content/docs/smallrye-fault-tolerance/latest/integration/event-loop.adoc new file mode 100644 index 000000000..2cc5818ac --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/latest/integration/event-loop.adoc @@ -0,0 +1,37 @@ += Event Loop +:version-root: ../ +include::{version-root}_attributes.adoc[] + +{smallrye-fault-tolerance} provides an API to integrate with event loops used in non-blocking runtimes. + +WARNING: This integration API comes from the `core` module and so is treated as private. +If you intend to implement it, please let us know in the {smallrye-fault-tolerance} issue tracker. +We'd prefer all the implementations to live in the {smallrye-fault-tolerance} project. + +The integrator must implement the `EventLoop` interface, which lets {smallrye-fault-tolerance} submit ``Runnable``s to the event loop for execution. +Implementations are discovered through `ServiceLoader`, so the corresponding `META-INF/services/\...` file must also exist. + +At most one implementation of `EventLoop` may be present at runtime. +{smallrye-fault-tolerance} will use it to execute non-blocking guarded methods when the caller was originally on an event loop thread. +This is important in case of: + +- retries, when there's a delay between retry attempts (the delay must not block); +- bulkheads, when a task is queued (such task is executed when some other task finishes, which happens on an unknown thread). + +If no event loop integration is present, the tasks described above will be offloaded to the xref:integration/thread-pool.adoc[thread pool]. + +The event loop integration is also used in some other situations when guarding non-blocking methods: + +- fallbacks, when the original method fails (the fallback is executed on the original event loop); +- timeouts, when timeout occurs (the failure is signalled on the original event loop). + +== Vert.x + +{smallrye-fault-tolerance} includes an implementation of the event loop integration API for Vert.x. +This implementation is compiled and tested against Vert.x version {vertx-version}, but should also work fine with Vert.x 4.x. + +This implementation is present in the `io.smallrye:smallrye-fault-tolerance-vertx` artifact. +Including this artifact is enough to enable the Vert.x integration. + +NOTE: The Vert.x implementation is treated as public, but only as described here. +That is, the artifact should be treated as opaque, no guarantees about its internals are made. diff --git a/content/docs/smallrye-fault-tolerance/latest/integration/intro.adoc b/content/docs/smallrye-fault-tolerance/latest/integration/intro.adoc new file mode 100644 index 000000000..6f416fe4c --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/latest/integration/intro.adoc @@ -0,0 +1,40 @@ += Introduction +:version-root: ../ +include::{version-root}_attributes.adoc[] + +{smallrye-fault-tolerance} comprises the following main artifacts: + +* `io.smallrye:smallrye-fault-tolerance-api`: public API for the features provided on top of {microprofile-fault-tolerance}, including a programmatic API; +* `io.smallrye:smallrye-fault-tolerance-apiimpl`: implementation of the programmatic API; +* `io.smallrye:smallrye-fault-tolerance-core`: core implementations of fault tolerance strategies; +* `io.smallrye:smallrye-fault-tolerance`: CDI-based implementation of {microprofile-fault-tolerance} with {smallrye-fault-tolerance} extensions, including a programmatic API; +* `io.smallrye:smallrye-fault-tolerance-autoconfig-core`: runtime part of the {smallrye-fault-tolerance} configuration system. +** (There's also `io.smallrye:smallrye-fault-tolerance-autoconfig-processor`, but that is only used during {smallrye-fault-tolerance} build and isn't required at runtime.) + +These artifacts must be always present, together with the following dependencies: + +* Jakarta CDI, Annotations and Interceptors API; +* `org.eclipse.microprofile.fault-tolerance:microprofile-fault-tolerance-api`; +* `org.eclipse.microprofile.config:microprofile-config-api` and some implementation; +* `org.jboss.logging:jboss-logging`. + +Some other artifacts are provided to facilitate additional integrations. +These are described in the subsequent sections. + +Most of the integration is performed automatically by the CDI Portable Extension present in {smallrye-fault-tolerance}: `io.smallrye.faulttolerance.FaultToleranceExtension`. +Make sure the extension is active. +The remaining integration concerns are described in the following sections: + +* xref:integration/metrics.adoc[emitting metrics] for fault tolerance strategies; +* xref:integration/thread-pool.adoc[providing a thread pool] for `@Asynchronous` methods and other asynchronous tasks; +* xref:integration/context-propagation.adoc[enabling Context Propagation] for `@Asynchronous` methods; +* xref:integration/event-loop.adoc[integrating an event loop] for non-blocking method invocations; +* xref:integration/async-types.adoc[integrating additional types] for asynchronous invocations; +* xref:integration/kotlin.adoc[integrating Kotlin support] for `suspend` functions; +* xref:integration/programmatic-api.adoc[integrating the programmatic API] that {smallrye-fault-tolerance} provides. + +== Standalone Implementation of the Programmatic API + +A completely standalone implementation of the programmatic API exists in the `io.smallrye:smallrye-fault-tolerance-standalone` artifact. +This is supposed to be used in a non-CDI environment. +Runtimes that integrate {smallrye-fault-tolerance} typically do so using CDI, and so should ignore this artifact. diff --git a/content/docs/smallrye-fault-tolerance/latest/integration/kotlin.adoc b/content/docs/smallrye-fault-tolerance/latest/integration/kotlin.adoc new file mode 100644 index 000000000..98d9804f9 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/latest/integration/kotlin.adoc @@ -0,0 +1,11 @@ += Kotlin Integration Concerns +:version-root: ../ +include::{version-root}_attributes.adoc[] + +This page describes integration concerns for xref:reference/asynchronous.adoc#kotlin-suspend-functions[Kotlin `suspend` functions]. + +To enable support of Kotlin suspending functions, it is required that the support library is present. +Its coordinates are: `io.smallrye:smallrye-fault-tolerance-kotlin`. + +This library includes some service providers that {smallrye-fault-tolerance} will automatically load and use. +Therefore, no more integration is necessary. diff --git a/content/docs/smallrye-fault-tolerance/latest/integration/metrics.adoc b/content/docs/smallrye-fault-tolerance/latest/integration/metrics.adoc new file mode 100644 index 000000000..fda9de26f --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/latest/integration/metrics.adoc @@ -0,0 +1,66 @@ += Metrics +:version-root: ../ +include::{version-root}_attributes.adoc[] + +{smallrye-fault-tolerance} provides support for OpenTelemetry and Micrometer. +Alternatively, metrics may be completely disabled at the integration level. + +As usual, this integration is based on CDI. +{smallrye-fault-tolerance} includes an internal interface `MetricsProvider` and these implementations: + +* `io.smallrye.faulttolerance.metrics.OpenTelemetryProvider` +* `io.smallrye.faulttolerance.metrics.MicrometerProvider` +* `io.smallrye.faulttolerance.metrics.NoopProvider` + +There are 2 possible ways how to integrate metrics: + +* exactly 1 class from the list above is a bean; +* more than 1 class from the list above is a bean, in which case, `io.smallrye.faulttolerance.metrics.CompoundMetricsProvider` must also be a bean. + +NOTE: Only the _names_ of the classes listed above are treated as public. +That is, the classes should be treated as opaque, no guarantees about their internals are made. + +== Default Integration + +In case the integrator uses the CDI Portable Extension `FaultToleranceExtension` and lets the container create an instance, metrics presence is discovered automatically. +All present metrics systems are used. + +== Using a `FaultToleranceExtension` Constructor + +In case the integrator uses the CDI Portable Extension `FaultToleranceExtension` and creates its instance manually, they can use a constructor. + +In addition to a zero-parameter constructor, which is used in the default integration as described above, there are constructors that take a parameter of `MetricsIntegration` or `Set`. + +`MetricsIntegration` is an enum with these values: + +* `OPENTELEMETRY`: use OpenTelemetry (MicroProfile Telemetry) integration +* `MICROMETER`: use Micrometer integration +* `NOOP`: no metrics + +== Metrics Providers + +Metrics providers have additional requirements, as described below. + +=== OpenTelemetry + +If OpenTelemetry should be used, the integrator must ensure that the following artifact is present: + +* `io.opentelemetry:opentelemetry-api`. + +Further, a bean of type `io.opentelemetry.api.metrics.Meter` must exist. +This bean is used to emit the actual metrics. + +=== Micrometer + +If Micrometer should be used, the integrator must ensure that the following artifact is present: + +* `io.micrometer:micrometer-core`. + +Further, a bean of type `io.micrometer.core.instrument.MeterRegistry` must exist. +This bean is used to emit the actual metrics. + +=== Noop + +If the "noop" provider is used, metrics are completely disabled. + +No metrics API and implementation is required in this case. diff --git a/content/docs/smallrye-fault-tolerance/latest/integration/programmatic-api.adoc b/content/docs/smallrye-fault-tolerance/latest/integration/programmatic-api.adoc new file mode 100644 index 000000000..636973a75 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/latest/integration/programmatic-api.adoc @@ -0,0 +1,48 @@ += Programmatic API Integration Concerns +:version-root: ../ +include::{version-root}_attributes.adoc[] + +This page describes integration concerns of the xref:reference/programmatic-api.adoc[Programmatic API] of {smallrye-fault-tolerance}. + +== CDI Implementation + +Runtimes that provide the CDI implementation of the programmatic API don't have to do any extra integration work. +The existing integration is enough. + +== Standalone Implementation + +The standalone implementation exposes a single integration point: the `StandaloneFaultTolerance.configure()` static method. +This method must be called before fault tolerance is first used. +It accepts a custom implementation of the `Configuration` interface, which allows customizing: + +* `enabled()`: if `false`, all fault tolerance strategies except fallback and thread offload are disabled +* `executor()`: executor for thread offloads and other asynchronous tasks +* `metricsAdapter()`: adapter for metrics, see below + +When no `Configuration` is provided, sensible defaults are used: + +* fault tolerance is enabled unless system property `MP_Fault_Tolerance_NonFallback_Enabled` is set to `false` +* a thread pool obtained using `Executors.newCachedThreadPool()` is used as an executor +* no metrics are emitted + +Users of the standalone implementation that also use an event loop based library, such as Vert.x, may integrate the event loop support as described in xref:integration/event-loop.adoc[Event Loop]. + +=== Lifecycle + +The thread pool and all other internal resources are initialized lazily, on the first use. +The `StandaloneFaultTolerance.shutdown()` method may be used on application shutdown to shut down these internal resources. + +The executor is only shut down when no `Configuration` was provided and {smallrye-fault-tolerance} had to create an executor on its own. +If `Configuration` was provided, the executor shutdown is left to the integrator; it is _not_ attempted automatically. + +At the end of `StandaloneFaultTolerance.shutdown()`, the `Configuration.onShutdown()` method is called. + +After `StandaloneFaultTolerance.shutdown()`, it is not possible to reinitialize {smallrye-fault-tolerance} again. + +=== Metrics + +It is possible to integrate with OpenTelemetry or Micrometer. + +The `Configuration.metricsAdapter()` method must be implemented and return an instance of `io.smallrye.faulttolerance.standalone.OpenTelemetryAdapter` or `io.smallrye.faulttolerance.standalone.MicrometerAdapter`. +The constructor of `OpenTelemetryAdapter` accepts the `Meter` to which metrics shall be emitted. +The constructor of `MicrometerAdapter` accepts the `MeterRegistry` to which metrics shall be emitted. diff --git a/content/docs/smallrye-fault-tolerance/latest/integration/thread-pool.adoc b/content/docs/smallrye-fault-tolerance/latest/integration/thread-pool.adoc new file mode 100644 index 000000000..b223fd4fb --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/latest/integration/thread-pool.adoc @@ -0,0 +1,30 @@ += Thread Pool +:version-root: ../ +include::{version-root}_attributes.adoc[] + +{smallrye-fault-tolerance} expects integrators to provide a thread pool for running `@Asynchronous` methods and other asynchronous tasks. +This integration is based on CDI. + +The integrator should provide a CDI bean which implements `AsyncExecutorProvider`. +The bean should be `@Singleton`, must be marked as alternative and selected globally for the application. + +There is a default thread pool in case no integration is provided, but it shouldn't be used outside of tests or experiments. +This is provided by the `AsyncExecutorProvider` default implementation, `DefaultAsyncExecutorProvider`. + +Integrators that are themselves Jakarta EE application servers and don't want to expose their own thread pool can subclass `DefaultAsyncExecutorProvider`. +This at least allows customizing the `ThreadFactory`. + +== Configuration + +If the integrator doesn't provide a thread pool, the default thread pool is used as described above. +In such case, the following configuration properties are recognized: + +- `io.smallrye.faulttolerance.mainThreadPoolSize`: maximum number of threads in the thread pool. +Defaults to `100`. + +- `io.smallrye.faulttolerance.mainThreadPoolQueueSize`: size of the queue that the thread pool should use. +Can be `> 0`, in which case the queue will be bounded to given size. +Can be `0`, in which case there will be no queue, and the thread pool will reject excess tasks. +Can be `-1`, in which case the queue will be unbounded. +Values `< -1` are an error. +Defaults to `-1`. diff --git a/content/docs/smallrye-fault-tolerance/latest/internals/config.adoc b/content/docs/smallrye-fault-tolerance/latest/internals/config.adoc new file mode 100644 index 000000000..6e505c7b2 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/latest/internals/config.adoc @@ -0,0 +1,119 @@ += Configuration System +:version-root: ../ +include::{version-root}_attributes.adoc[] + +include::{partialsdir}/internals.adoc[] + +The xref:internals/core.adoc[core] fault tolerance strategies are configured simply by constructor parameters. +This is fine, since they are an internal implementation detail. +However, the constructor parameters correspond closely to the members of {microprofile-fault-tolerance} annotations. +The idea is that core fault tolerance strategies have no dependency on the {microprofile-fault-tolerance} API, but given a fault tolerance annotation, creating an instance of the corresponding strategy should be straightforward. + +On top of the core layer, {smallrye-fault-tolerance} implements {microprofile-fault-tolerance}, including a configuration system conforming to the specification. +This document briefly describes how the configuration system works. + +== `FaultToleranceOperation` + +The fault tolerance interceptor, which is the main entrypoint, looks up a `FaultToleranceOperation` instance for each class/method combination it has to process. +The `FaultToleranceOperation` holds a bean class, a descriptor of the guarded method, and configuration for each fault tolerance strategy applied to the method. +It also orchestrates configuration validation. +The interceptor uses a `FaultToleranceOperation` to instantiate all the core fault tolerance strategies for given guarded method. + +The `FaultToleranceOperation` instances are created early on, during application deployment, for all beans that use {microprofile-fault-tolerance}. +It may happen that a `FaultToleranceOperation` is created lazily, for special constructs such as runtime-built proxies, but that is largely irrelevant from the configuration perspective. + +== `FaultToleranceMethod` + +A `FaultToleranceOperation` is created from a `FaultToleranceMethod`. +A `FaultToleranceMethod` holds, again, a bean class, a descriptor of the guarded method, and the annotation instance for each annotation that is present on the guarded method or the class declaring the method. +It also knows what annotations have been declared directly on the method, and what annotations have been declared on the class. +This is because the {microprofile-fault-tolerance} configuration rules distinguish these two cases. + +A `FaultToleranceMethod` must be created in a CDI extension, because it represents the knowledge CDI has about the guarded method. +Note that CDI extensions may add, remove or modify annotations, so `FaultToleranceMethod` doesn't just hold the annotation instances as present in the class bytecode. + +.Quarkus +**** +In environments that bootstrap the CDI container during application build, such as Quarkus, the set of all ``FaultToleranceMethod``s in the application should be collected during build and transferred to runtime. +Then, at application runtime, ``FaultToleranceOperation``s should be created and validated. + +{smallrye-fault-tolerance} doesn't do anything like that, but is designed with Quarkus (and CDI Lite) in mind. +**** + +The main difference between `FaultToleranceMethod` and `FaultToleranceOperation` is that `FaultToleranceMethod` holds annotation instances, while `FaultToleranceOperation` holds configuration. + +== `@AutoConfig` + +As described above, `FaultToleranceMethod` holds annotation instances, while `FaultToleranceOperation` holds configuration. +Yet, `FaultToleranceOperation` exposes the annotation types (such as `@CircuitBreaker`) to anyone who needs the configuration. +The configuration consumers simply call the annotation methods (such as `requestVolumeThreshold()`), and configuration is handled behind the scenes, following the {microprofile-fault-tolerance} configuration rules. + +The infrastructure behind this is present in the `autoconfig` module. +This module allows creating simple configuration interfaces like this: + +[source,java] +---- +@AutoConfig +interface CircuitBreakerConfig extends CircuitBreaker, Config { + default void validate() { + ... + } +} +---- + +The config interface must: + +. be annotated with `@AutoConfig`; +. extend the annotation interface (`CircuitBreaker` in this case); +. extend the `Config` (or `ConfigDeclarativeOnly`) interface (from the `autoconfig/core` module); +. provide a `default` implementation of the `validate` method (defined in `Config`). + +For each such interface, an implementation class will be automatically generated by an annotation processor (implemented in the `autoconfig/processor` module). +In this case, it would be called `CircuitBreakerConfigImpl`. + +This implementation class has a `static` method called `create()` that accepts a `FaultToleranceMethod`. +It follows that for each guarded method, a new instance must be created. +If given `FaultToleranceMethod` doesn't contain an instance of particular annotation, `create()` simply returns `null`. + +The implementation class also overrides all methods from the annotation type. +Each annotation method is implemented to first consult MicroProfile Config, and only second to consult the annotation instance (which, as we described above, is present on the `FaultToleranceMethod`). + +The implementation class is supposed to be used just like the annotation type itself. +In other words, to obtain configured values, call the annotation methods on the config interface instance. +For example, to obtain the `requestVolumeThreshold` value, with respect to the {microprofile-fault-tolerance} configuration rules, we obtain an instance of `CircuitBreakerConfigImpl` and call `requestVolumeThreshold()`. + +The `FaultToleranceOperation` class holds instances of these config interfaces, and exposes them to configuration consumers. +It actually only exposes the annotation types, not the full config interface, but that is enough. + +== Putting it all together + +Let's recap how it all works. + +First, for each guarded method in the application, a `FaultToleranceMethod` instance is created, holding all the annotations that affect the method. + +Second, for each `FaultToleranceMethod`, a `FaultToleranceOperation` is created. +That, in turn, creates an instance of all relevant config interfaces. + +Finally, the fault tolerance interceptor looks up a `FaultToleranceOperation` and creates a chain of fault tolerance strategies. +For each fault tolerance strategy in the chain, the interceptor looks up configuration values from the corresponding config interface. + +The chain of fault tolerance strategies is created on the first invocation to a given guarded method and cached for all subsequent invocations. +That is how we satisfy the requirement that all stateful fault tolerance strategies are singletons, but that's a different story. + +== Supplement: Programmatic API + +The description above is slightly simplified for readability. +It is precise enough to describe how configuration works in the declarative, annotation-based API. + +The programmatic API itself supports configuration when used together with `@ApplyGuard`. +However, it doesn't support configuring _all_ strategies; some are built in a different manner and no configuration is possible. + +Therefore, the `FaultToleranceOperation` class described above is actually split into 2 classes: + +* `BasicFaultToleranceOperation`, which is shared for both configuration systems and holds all `Config` objects, +* its subclass `FaultToleranceOperation`, which only applies in case of the declarative API and holds all `ConfigDeclarativeOnly` objects. + +In addition to the description above, implementations of the `Config` interface (but not `ConfigDeclarativeOnly`) also include a `create()` method that takes a `String` identifier and a `Supplier` of the backing annotation instance. +This is supposed to be used to create an instance for configuring the programmatic API, because there's no `FaultToleranceMethod` in such case. +Also, a zero-configuration implementation of the `Config` interface is generated, called `...NoConfigImpl`. +This exists to be able to use the config interface even in the non-`@ApplyGuard` case, where no configuration is possible. diff --git a/content/docs/smallrye-fault-tolerance/latest/internals/core.adoc b/content/docs/smallrye-fault-tolerance/latest/internals/core.adoc new file mode 100644 index 000000000..271f108a9 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/latest/internals/core.adoc @@ -0,0 +1,123 @@ += Fault Tolerance Core +:version-root: ../ +include::{version-root}_attributes.adoc[] + +include::{partialsdir}/internals.adoc[] + +The core idea in {smallrye-fault-tolerance} is a `FaultToleranceStrategy`. +It is an interface that looks like this: + +[source,java] +---- +interface FaultToleranceStrategy { + Future apply(FaultToleranceContext ctx); +} +---- + +NOTE: The `Future` type here is _not_ a `java.util.concurrent.Future`. +It comes from {smallrye-fault-tolerance} and it could be described as a very bare-bones variant of `CompletableFuture`, except it's split into `Completer` and `Future`. +It is not supposed to be used outside of this project. + +The `FaultToleranceContext` is similar to a `Callable>`; it represents the method invocation guarded by this fault tolerance strategy. +The fault tolerance strategy does its work around `ctx.call()`. +It can catch exceptions, invoke `ctx.call()` multiple times, invoke something else, etc. +As an example, let's consider this strategy, applicable to methods that return a `String`: + +[source,java] +---- +public class MyStringFallback implements FaultToleranceStrategy { + @Override + public Future apply(FaultToleranceContext ctx) { + Completer completer = Completer.create(); + try { + ctx.call().then((value, error) -> { + if (error == null) { + completer.complete(value); + } else { + completer.complete("my string value"); + } + }); + } catch (Exception ignored) { + completer.complete("my string value"); + } + return completer.future(); + } +} +---- + +This is a very simple fallback mechanism, which returns a pre-defined value in case of an exception. + +In the {smallrye-fault-tolerance} codebase, you can find implementations of all the strategies required by {microprofile-fault-tolerance}: retry, fallback, timeout, circuit breaker or bulkhead. +Asynchronous invocation, delegated to a thread pool, is of course also supported. + +When multiple fault tolerance strategies are supposed to be used to guard one method, they form a chain. +Continuing with our simple example, adding the ability to chain with another strategy would look like this: + +[source,java] +---- +public class MyStringFallback implements FaultToleranceStrategy { + private final FaultToleranceStrategy delegate; + + public MyStringFallback(FaultToleranceStrategy delegate) { + this.delegate = delegate; + } + + @Override + public Future apply(FaultToleranceContext ctx) { + Completer completer = Completer.create(); + try { + delegate.apply(ctx).then((value, error) -> { + if (error == null) { + completer.complete(value); + } else { + completer.complete("my string value"); + } + }); + } catch (Exception ignored) { + completer.complete("my string value"); + } + return completer.future(); + } +} +---- + +We see that one strategy delegates to another, passing the `FaultToleranceContext` along. +In fact, all the implementations in {smallrye-fault-tolerance} are written like this: they expect to be used in a chain, so they take another `FaultToleranceStrategy` to which they delegate. +But if all strategies have this form, when is `ctx.call()` actually invoked? +Good question! +The ultimate `ctx.call()` invocation is done by a special fault tolerance strategy which is called, well, `Invocation`. + +As an example which uses real {microprofile-fault-tolerance} annotations, let's consider this method: + +[source,java] +---- +@Retry(...) +@Timeout(...) +@Fallback(...) +public void doSomething() { + ... +} +---- + +The chain of fault tolerance strategies will look roughly like this: + +[source] +---- +Fallback( + Retry( + Timeout( + Invocation( + // ctx.call() will happen here + // that will, in turn, invoke doSomething() + ) + ) + ) +) +---- + +The order in which the strategies are chained (or, in fact, nested) is specified by {microprofile-fault-tolerance}. + +Other strategies might also be present in more complex cases. +For example, if metrics are enabled, a special strategy is used that collects metrics. +If the method is `@Asynchronous`, a special strategy is used that offloads method execution to another thread. +Etc. diff --git a/content/docs/smallrye-fault-tolerance/latest/internals/instructions.adoc b/content/docs/smallrye-fault-tolerance/latest/internals/instructions.adoc new file mode 100644 index 000000000..2b275e995 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/latest/internals/instructions.adoc @@ -0,0 +1,21 @@ += Instructions +:version-root: ../ +include::{version-root}_attributes.adoc[] + +To compile this project and install the artifacts into local Maven repository, run: + +[source,bash] +---- +mvn clean install +---- + +This will also run all the tests and the TCK. + +You can use + +[source,bash] +---- +mvn clean install -DskipTests +---- + +to skip the tests and make the build a lot faster. diff --git a/content/docs/smallrye-fault-tolerance/latest/internals/logging.adoc b/content/docs/smallrye-fault-tolerance/latest/internals/logging.adoc new file mode 100644 index 000000000..bacfff5ec --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/latest/internals/logging.adoc @@ -0,0 +1,16 @@ += Logging +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Logging in {smallrye-fault-tolerance} is based on https://jboss-logging.github.io/jboss-logging-tools/[JBoss Logging Tools] (see also https://github.com/smallrye/smallrye/wiki/I18N-Messages-and-Logging[SmallRye instructions]). +It's an annotation processor that takes annotated interfaces and generates logger implementations with out of the box support for message codes and internationalization. +We use a project code of `SRFTL`. +Message codes are divided into ranges that are defined in the `message-ranges.txt` file in the root directory. + +In addition to logging, JBoss Logging Tools can also be used for creating exceptions, again, with out of the box support for message codes and internationalization. +Contrary to both documentation sites mentioned above, it _is_ possible to mix logging and exceptions in a single interface, as demonstrated by `CdiLogger`. +However, {smallrye-fault-tolerance} doesn't use this feature yet, with the exception of a few methods in `CdiLogger`. +This should be rectified in the future. + +Note that `DEBUG` and `TRACE` logging doesn't need message codes or internationalization, so we usually don't declare logger methods for them, unless the log message requires parameters. +We still declare the logger interface, let it extend `BasicLogger` and use the singleton instance to log plain ``String``s. diff --git a/content/docs/smallrye-fault-tolerance/latest/internals/project-structure.adoc b/content/docs/smallrye-fault-tolerance/latest/internals/project-structure.adoc new file mode 100644 index 000000000..aa14ffa82 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/latest/internals/project-structure.adoc @@ -0,0 +1,37 @@ += Project Structure +:version-root: ../ +include::{version-root}_attributes.adoc[] + +* `api`: {smallrye-fault-tolerance} API, additional to the {microprofile-fault-tolerance} API. + Treated as a public API. +* `implementation/core`: Implementation of core fault tolerance strategies. + Independent of the {microprofile-fault-tolerance} API or the {smallrye-fault-tolerance} API. + Treated as private API, no compatibility guaranteed. +* `implementation/apiimpl`: Implementation of the programmatic API. + Based on the `core` module. + Treated as private API, no compatibility guaranteed. +* `implementation/fault-tolerance`: Implementation of the {microprofile-fault-tolerance} API and the {smallrye-fault-tolerance} API. + Based on CDI and the `core` module. + The configuration system is based on the `autoconfig` module. +* `implementation/autoconfig/core`: Core classes of the {smallrye-fault-tolerance} configuration system. + Required at runtime. +* `implementation/autoconfig/processor`: An annotation processor that generates implementations of config interfaces. + Only required during {smallrye-fault-tolerance} build. +* `implementation/context-propagation`: Optional integration with MicroProfile Context Propagation. + See xref:integration/context-propagation.adoc[Context Propagation integration]. +* `implementation/kotlin`: Optional integration with Kotlin. + See xref:integration/kotlin.adoc[Kotlin Integration Concerns]. +* `implementation/mutiny`: Optional integration with Mutiny. + See xref:integration/async-types.adoc[Additional Asynchronous Types Integration Concerns]. +* `implementation/rxjava3`: Optional integration with RxJava 3. + See xref:integration/async-types.adoc[Additional Asynchronous Types Integration Concerns]. +* `implementation/standalone`: Standalone implementation of the {smallrye-fault-tolerance} programmatic API. + See xref:integration/programmatic-api.adoc[Programmatic API Integration Concerns]. +* `implementation/vertx`: Optional integration of the Vert.x event loop and Vert.x `Future`. + See xref:integration/event-loop.adoc[Event Loop integration] and xref:integration/async-types.adoc[Additional Asynchronous Types Integration Concerns]. +* `testsuite/basic`: Basic test suite. + Some parts of it overlap with the Eclipse MicroProfile Fault Tolerance TCK. +* `testsuite/integration`: Integration test suite. + Includes several Context Propagation and Vert.x tests. +* `testsuite/tck`: Runs the implementation against the Eclipse MicroProfile Fault Tolerance TCK. +* `doc`: This documentation. diff --git a/content/docs/smallrye-fault-tolerance/latest/reference/asynchronous.adoc b/content/docs/smallrye-fault-tolerance/latest/reference/asynchronous.adoc new file mode 100644 index 000000000..0937cf522 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/latest/reference/asynchronous.adoc @@ -0,0 +1,276 @@ += Asynchronous Execution +:version-root: ../ +include::{version-root}_attributes.adoc[] + +For introduction, see xref:howto/asynchronous.adoc[How to Guard Asynchronous Methods]. +The how-to guide also explains what _asynchronous fault tolerance_ is and when does it apply. + +== Description + +=== `@Asynchronous` + +This annotation may only be placed on methods that return `java.util.concurrent.Future` or java.util.concurrent.`CompletionStage`. +It may also be placed on a class, in which case all business methods must return `Future` or `CompletionStage`. + +The method call is offloaded to another thread. +The thread pool that is used for offloading method calls is provided by the runtime that integrates {smallrye-fault-tolerance}. + +=== `@AsynchronousNonBlocking` + +include::{partialsdir}/srye-feature.adoc[] + +In addition to the {microprofile-fault-tolerance} `@Asynchronous` annotation, {smallrye-fault-tolerance} offers an annotation for asynchronous non-blocking processing: `@AsynchronousNonBlocking`. + +This annotation may only be placed on methods that return `CompletionStage`, because the `Future` type cannot really be used for non-blocking processing. +It may also be placed on a class, in which case all business methods must return `CompletionStage`. + +As opposed to `@Asynchronous`, the `@AsynchronousNonBlocking` annotation does _not_ offload the method execution to another thread. +The method is allowed to proceed on the original thread, assuming that it returns quickly and all processing it does is performed in a non-blocking way. + +=== `@Asynchronous` and `@AsynchronousNonBlocking` Compared + +|=== +|Annotation |Method return type |Execution + +|`@Asynchronous` +|`Future` or `CompletionStage` +|offloaded to a thread pool + +|`@AsynchronousNonBlocking` +|`CompletionStage` +|proceeds on the original thread +|=== + +=== `@Asynchronous` and `@AsynchronousNonBlocking` Combined + +When these annotations are combined, an annotation on a method has priority over an annotation on a class. +For example: + +[source,java] +---- +@ApplicationScoped +@Asynchronous +public class MyService { + @Retry + CompletionStage hello() { // <1> + ... + } + + @Retry + @AsynchronousNonBlocking + CompletionStage helloBlocking() { // <2> + ... + } +} +---- + +<1> Treated as `@Asynchronous`, based on the class annotation. +<2> Treated as `@AsynchronousNonBlocking`, the method annotation has priority over the class annotation. + +It is an error to put both `@Asynchronous` and `@AsynchronousNonBlocking` on the same program element. + +[[recommendation]] +=== Recommendation + +Use the <>. +In this mode, methods returning `CompletionStage` are automatically treated as if they were `@AsynchronousNonBlocking`. +Use `@Asynchronous` to mark blocking methods for thread offload. + +If you can't use the non-compatible mode, use `@AsynchronousNonBlocking` or `@Asynchronous` to mark all asynchronous methods. + +IMPORTANT: In previous releases, {smallrye-fault-tolerance} recommended to use the `@Blocking` and `@NonBlocking` annotations. +Using these annotations *for fault tolerance purposes* is no longer supported, starting with {smallrye-fault-tolerance} 7.0.0. + +We also recommend avoiding `@Asynchronous` methods that return `Future`, because the only way to obtain the future value is blocking. + +[[interactions]] +== Interactions with Other Strategies + +See xref:howto/multiple.adoc[How to Use Multiple Strategies] for an overview of how fault tolerance strategies are nested. + +=== `@Asynchronous` + +`@Asynchronous` does not interact with other fault tolerance strategies, with the obvious exception that it moves execution to another thread. + +=== `@AsynchronousNonBlocking` + +If the guarded method uses `@Retry` and some delay between retries is configured, only the initial execution is guaranteed to occur on the original thread. +Subsequent attempts may be offloaded to an extra thread, so that the original thread is not blocked on the delay. + +If the guarded method uses `@Bulkhead`, the execution is _not_ guaranteed to occur on the original thread. +If the execution has to wait in the bulkhead queue, it may later end up on a different thread. + +If the original thread is an event loop thread and event loop integration is enabled, then the event loop is always used to execute the guarded method. +In such case, all retry attempts and queued bulkhead executions are guaranteed to happen on the original thread. + +[[configuration]] +== Configuration + +There is no configuration for `@Asynchronous` or `@AsynchronousNonBlocking`. + +[[metrics]] +== Metrics + +Asynchronous execution does not emit any metrics. + +See xref:reference/metrics.adoc[the Metrics reference guide] for general metrics information. + +== Extra Features + +[[async-types]] +=== Additional Asynchronous Types + +include::{partialsdir}/srye-feature.adoc[] + +{microprofile-fault-tolerance} supports asynchronous fault tolerance for methods that return `CompletionStage`. +(The `java.util.concurrent.Future` type is not truly asynchronous, so we won't take it into account here.) +{smallrye-fault-tolerance} adds support for additional asynchronous types: + +* Mutiny: `Uni` +* RxJava 3: `Single`, `Maybe`, `Completable` +* Vert.x: `io.vertx.core.Future` + +These types are treated just like `CompletionStage`, so everything that works for `CompletionStage` works for these types as well. +Stream-like types (`Multi`, `Observable`, `Flowable`) are not supported, because their semantics can't be easily expressed in terms of `CompletionStage`. + +For example: + +[source,java] +---- +@ApplicationScoped +public class MyService { + @Retry + @AsynchronousNonBlocking // <1> + Uni hello() { // <2> + ... + } +} +---- + +<1> Using the `@AsynchronousNonBlocking` annotation, because the method doesn't block and offloading execution to another thread is not necessary. +<2> Returning the `Uni` type from Mutiny. +This shows that whatever works for `CompletionStage` also works for the other async types. + +The implementation internally converts the async types to a `CompletionStage` and back. +This means that to be able to use any particular asynchronous type, the corresponding converter must be present. +{smallrye-fault-tolerance} provides support libraries for popular asynchronous types, and these support libraries include the corresponding converters. + +It is possible that the runtime you use already provides the correct integration. +Otherwise, add a dependency to your application: + +* https://smallrye.io/smallrye-mutiny/[Mutiny]: `io.smallrye:smallrye-fault-tolerance-mutiny` +* https://github.com/ReactiveX/RxJava/tree/3.x[RxJava 3]: `io.smallrye:smallrye-fault-tolerance-rxjava3` +* https://vertx.io/docs/vertx-core/java/#_future_results[Vert.x `Future`]: `io.smallrye:smallrye-fault-tolerance-vertx` + +.Quarkus +**** +In Quarkus, the Mutiny and Vert.x support libraries are present by default. +You can use fault tolerance on methods that return `Uni` (and Vert.x `Future`) out of the box. +**** + +[[kotlin-suspend-functions]] +=== Kotlin `suspend` Functions + +include::{partialsdir}/srye-feature.adoc[] + +{smallrye-fault-tolerance} includes support for Kotlin suspending functions. +They are treated as <>, even though the internal implementation is more complex than support for Mutiny or RxJava 3. + +For example: + +[source,kotlin] +---- +@ApplicationScoped +open class MyService { + @Retry(maxRetries = 2) + @Fallback(fallbackMethod = "helloFallback") + open suspend fun hello(): String { // <1> + delay(100) + throw IllegalArgumentException() + } + + private suspend fun helloFallback(): String { // <2> + delay(100) + return "hello" + } +} +---- + +<1> As a suspending function, this method can only be called from another suspending function. +It will be guarded by the retry and fallback strategies, as defined using the annotations. +<2> Similarly to fallback methods in Java, fallback methods in Kotlin must have the same signature as the guarded method. +Since the guarded method is suspending, the fallback method must be suspending. + +As mentioned above, suspending functions are treated as async types. +This means that for asynchronous fault tolerance to work correctly on suspending functions, they must be determined to be asynchronous. +That happens automatically in the <>, based on the method signature, but if you use strictly compatible mode, the `@Asynchronous` or `@AsynchronousNonBlocking` annotation must be present. +It is expected that most users will use the Kotlin support in the non-compatible mode, so the example above does not include any such annotation. + +To be able to use this, a support library must be present. +It is possible that the runtime you use already provides the correct integration. +Otherwise, add a dependency to your application: `io.smallrye:smallrye-fault-tolerance-kotlin`. + +.Quarkus +**** +In Quarkus, the Kotlin support library is present by default, if you use the Quarkus Kotlin support. +You can declare fault tolerance annotations on suspending methods out of the box. +**** + +==== Programmatic API + +Suspending functions are currently only supported in the declarative, annotation-based API, as shown in the example above. +The xref:reference/programmatic-api.adoc[Programmatic API] of {smallrye-fault-tolerance} does not support suspending functions, but other than that, it can of course be used from Kotlin through its Java interop. + +[[method-asynchrony]] +=== Determining Asynchrony from Method Signature + +include::{partialsdir}/srye-feature.adoc[] + +include::{partialsdir}/non-compat.adoc[] + +In the non-compatible mode, method asynchrony is determined solely from its signature. +That is, methods that + +* have some fault tolerance annotation (such as `@Retry`), +* return `CompletionStage` (or some other <>), + +always have asynchronous fault tolerance applied. + +For example: + +[source,java] +---- +@ApplicationScoped +public class MyService { + @Retry + CompletionStage hello() { // <1> + ... + } + + @Retry + Uni helloMutiny() { // <2> + ... + } + + @Retry + @Asynchronous + CompletionStage helloBlocking() { // <3> + ... + } +} +---- + +<1> Executed on the original thread, because the method returns `CompletionStage`. +It is as if the method was annotated `@AsynchronousNonBlocking`. +<2> Executed on the original thread, because the method returns an <>. +It is as if the method was annotated `@AsynchronousNonBlocking`. +<3> The explicit `@Asynchronous` annotation is honored. +The method is executed on a thread pool. + +Note that the existing annotations still work without a change, both in compatible and non-compatible mode. +That is, if a method (or class) is annotated `@Asynchronous`, execution will be offloaded to a thread pool. +If a method (or class) is annotated `@AsynchronousNonBlocking`, execution will happen on the original thread. + +Also note that this doesn't affect methods returning `java.util.concurrent.Future`. +You still have to annotate them `@Asynchronous` to make sure they are executed on a thread pool and are guarded properly. +As mentioned in the <>, we discourage using these methods, because the only way to obtain the future value is blocking. diff --git a/content/docs/smallrye-fault-tolerance/latest/reference/bulkhead.adoc b/content/docs/smallrye-fault-tolerance/latest/reference/bulkhead.adoc new file mode 100644 index 000000000..a671d958b --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/latest/reference/bulkhead.adoc @@ -0,0 +1,136 @@ += Bulkhead +:version-root: ../ +include::{version-root}_attributes.adoc[] + +For introduction, see xref:howto/bulkhead.adoc[How to Limit Concurrency]. + +== Description + +If a guarded method is called by too many callers concurrently, the excess attempts will fail with `BulkheadException`. + +If the guarded method is `@Asynchronous` or `@AsynchronousNonBlocking`, excess attempts are first put into a queue. +The queued attempts are executed later. +When the queue becomes full, excess attempts are rejected with `BulkheadException`. + +=== Lifecycle + +Bulkhead needs to maintain some state between invocations: the number of current executions and the queue of asynchronous executions. +This state is a singleton, irrespective of the lifecycle of the bean that uses the `@Bulkhead` annotation. + +More specifically, the bulkhead state is uniquely identified by the combination of the bean class (`java.lang.Class`) and the method object (`java.lang.reflect.Method`) representing the guarded method. + +For example, if there’s a guarded method `doWork` on a bean which is `@RequestScoped`, each request will have its own instance of the bean, but all invocations of `doWork` will share the same bulkhead state. + +[[interactions]] +== Interactions with Other Strategies + +See xref:howto/multiple.adoc[How to Use Multiple Strategies] for an overview of how fault tolerance strategies are nested. + +If `@Fallback` is used with `@Bulkhead`, the fallback method or handler may be invoked if a `BulkheadException` is thrown, depending on the fallback configuration. + +If `@Retry` is used with `@Bulkhead`, each retry attempt is processed by the bulkhead as an independent invocation. +If `BulkheadException` is thrown, the execution may be retried, depending on how retry is configured. + +If `@CircuitBreaker` is used with `@Bulkhead`, the circuit breaker is checked before enforcing the concurrency limit. +If concurrency limiting results in `BulkheadException`, this may be counted as a failure, depending on how the circuit breaker is configured. + +If `@RateLimit` is used with `@Bulkhead`, the rate limit is enforced before enforcing the concurrency limit. + +If `@Timeout` is used with `@Bulkhead`, the timeout watcher is started before enforcing the concurrency limit. + +[[configuration]] +== Configuration + +There are 2 configuration options, corresponding to the 2 members of the `@Bulkhead` annotation. + +=== `value` + +Type: `int` + +Default: `10` + +The concurrency limit. + +=== `waitingTaskQueue` + +Type: `int` + +Default: `10` + +The size of queue for excess attempts. +Only effective when the method is `@Asynchronous` or `@AsynchronousNonBlocking`. +See xref:reference/asynchronous.adoc[the Asynchronous Execution reference guide]. + +[[metrics]] +== Metrics + +Bulkhead exposes the following metrics: + +[cols="1,5"] +|=== +| Name | `ft.bulkhead.calls.total` +| Type +a| * OpenTelemetry: `LongCounter` +* Micrometer: `Counter` +| Unit | None +| Description | The number of times the bulkhead logic was run. This is usually once per method call, but may be zero times if the circuit breaker or rate limit prevented execution or more than once if the method call was retried. +| Tags +a| * `method` - the fully qualified method name +* `bulkheadResult` = `[accepted\|rejected]` - whether the bulkhead allowed the method call to run +|=== + +[cols="1,5"] +|=== +| Name | `ft.bulkhead.executionsRunning` +| Type +a| * OpenTelemetry: `LongUpDownCounter` +* Micrometer: `Gauge` +| Unit | None +| Description | Number of currently running executions. +| Tags +a| * `method` - the fully qualified method name +|=== + +[cols="1,5"] +|=== +| Name | `ft.bulkhead.executionsWaiting` +| Type +a| * OpenTelemetry: `LongUpDownCounter` +* Micrometer: `Gauge` +| Unit | None +| Description | Number of executions currently waiting in the queue. +| Tags +a| * `method` - the fully qualified method name +| Notes | Only added if the method is also `@Asynchronous` or `@AsynchronousNonBlocking` +|=== + +[cols="1,5"] +|=== +| Name | `ft.bulkhead.runningDuration` +| Type +a| * OpenTelemetry: `DoubleHistogram` with explicit bucket boundaries `[0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10]` +* Micrometer: `Timer` +| Unit +a| * OpenTelemetry: seconds +* Micrometer: nanoseconds +| Description | Histogram of the time that method executions spent running. +| Tags +a| * `method` - the fully qualified method name +|=== + +[cols="1,5"] +|=== +| Name | `ft.bulkhead.waitingDuration` +| Type +a| * OpenTelemetry: `DoubleHistogram` with explicit bucket boundaries `[0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10]` +* Micrometer: `Timer` +| Unit +a| * OpenTelemetry: seconds +* Micrometer: nanoseconds +| Description | Histogram of the time that method executions spent waiting in the queue. +| Tags +a| * `method` - the fully qualified method name +| Notes | Only added if the method is also `@Asynchronous` or `@AsynchronousNonBlocking` +|=== + +See xref:reference/metrics.adoc[the Metrics reference guide] for general metrics information. diff --git a/content/docs/smallrye-fault-tolerance/latest/reference/circuit-breaker.adoc b/content/docs/smallrye-fault-tolerance/latest/reference/circuit-breaker.adoc new file mode 100644 index 000000000..9b67acd68 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/latest/reference/circuit-breaker.adoc @@ -0,0 +1,282 @@ += Circuit Breaker +:version-root: ../ +include::{version-root}_attributes.adoc[] + +For introduction, see xref:howto/circuit-breaker.adoc[How to Fail Fast on Recurring Failures]. + +== Description + +The circuit breaker is a simple state machine: + +image::images/diagrams/mermaid-f40d3cbcabca.svg[circuit-breaker] + +The circuit breaker starts _closed_. +In this state, the circuit breaker maintains a rolling window of recent invocations. +For each invocation, the rolling window tracks whether it finished successfully or failed. + +The rolling window must be full to take any state transition decision. +For example, if the rolling window has size 10, a closed circuit breaker always allows at least 10 invocations. + +If the rolling window contains a number of failures higher than the configured ratio, a closed circuit breaker moves to _open_. +When the circuit breaker is open, invocations are not allowed to proceed. +Instead, the circuit breaker fails fast and throws `CircuitBreakerOpenException`. + +For example, if the rolling window has size 10 and the failure rate is 0.5, it means that 5 invocations out of the most recent 10 invocations must fail for the circuit breaker to move to _open_. + +After some time, an open circuit breaker moves to _half-open_ to determine whether failing fast is still appropriate. +A half-open circuit breaker allows some configured number of probe attempts to proceed. +If all of them succeed, the circuit breaker moves to _closed_ and invocations are allowed again. +If some probe invocations fail, the circuit breaker moves back to _open_ and invocations are prevented. + +Successes and failures are determined from the method result. +More specifically: + +. If the guarded method returns normally, it is treated as success. +. Otherwise, if the guarded method throws an exception whose type is assignable to any of the `skipOn` types (see <>), it is treated as success. +. Otherwise, if the guarded method throws an exception whose type is assignable to any of the `failOn` types (see <>), it is treated as failure. +. Otherwise, it is treated as success. + +=== Lifecycle + +Circuit breaker needs to maintain some state between invocations: the number of recent successful invocations, the number of recent failed invocations, and so on. +This state is a singleton, irrespective of the lifecycle of the bean that uses the `@CircuitBreaker` annotation. + +More specifically, the circuit breaker state is uniquely identified by the combination of the bean class (`java.lang.Class`) and the method object (`java.lang.reflect.Method`) representing the guarded method. + +For example, if there’s a guarded method `doWork` on a bean which is `@RequestScoped`, each request will have its own instance of the bean, but all invocations of `doWork` will share the same circuit breaker state. + +[[interactions]] +== Interactions with Other Strategies + +See xref:howto/multiple.adoc[How to Use Multiple Strategies] for an overview of how fault tolerance strategies are nested. + +If `@Fallback` is used with `@CircuitBreaker`, the fallback method or handler may be invoked if a `CircuitBreakerOpenException` is thrown, depending on the fallback configuration. + +If `@Retry` is used with `@CircuitBreaker`, each retry attempt is processed by the circuit breaker as an independent invocation. +If `CircuitBreakerOpenException` is thrown, the execution may be retried, depending on how retry is configured. + +[[configuration]] +== Configuration + +There are 7 configuration options, corresponding to the 7 members of the `@CircuitBreaker` annotation. + +=== `requestVolumeThreshold` + +Type: `int` + +Default: `20` + +The size of the rolling window. +That is, the number of recent consecutive invocations tracked by a _closed_ circuit breaker. + +=== `failureRatio` + +Type: `double` + +Default: `0.5` + +The ratio of failures in the rolling window that causes a _closed_ circuit breaker to move to _open_. + +=== `delay` + `delayUnit` + +Type: `long` + `ChronoUnit` + +Default: `5000 millis`, or 5 seconds + +The delay after which an _open_ circuit breaker moves to _half-open_. + +=== `successThreshold` + +Type: `int` + +Default: `1` + +The number of probe invocations allowed when the circuit breaker is _half-open_. +If they all succeed, the circuit breaker moves to _closed_, otherwise it moves back to _open_. + +[[fail-on]] +=== `failOn` + +Type: `Class[]` + +Default: `{Throwable.class}` + +Set of exception types that the circuit breaker considers failures. + +[[skip-on]] +=== `skipOn` + +Type: `Class[]` + +Default: `{}` + +Set of exception types that the circuit breaker considers success. +This configuration takes precedence over `failOn`. + +[[metrics]] +== Metrics + +Circuit breaker exposes the following metrics: + +[cols="1,5"] +|=== +| Name | `ft.circuitbreaker.calls.total` +| Type +a| * OpenTelemetry: `LongCounter` +* Micrometer: `Counter` +| Unit | None +| Description | The number of times the circuit breaker logic was run. This is usually once per method call, but may be more than once if the method call is retried. +| Tags +a| * `method` - the fully qualified method name +* `circuitBreakerResult` = `[success\|failure\|circuitBreakerOpen]` - the result of the method call, as considered by the circuit breaker +** `success` - the method ran and was successful +** `failure` - the method ran and failed +** `circuitBreakerOpen` - the method did not run because the circuit breaker was in open or half-open state +|=== + +[cols="1,5"] +|=== +| Name | `ft.circuitbreaker.state.total` +| Type +a| * OpenTelemetry: `LongCounter` +* Micrometer: `TimeGauge` +| Unit +a| * OpenTelemetry: nanoseconds +* Micrometer: nanoseconds +| Description | Amount of time the circuit breaker has spent in each state +| Tags +a| * `method` - the fully qualified method name +* `state` = `[open\|closed\|halfOpen]` - the circuit breaker state +| Notes | Although this metric is a `Gauge`, its value increases monotonically. +|=== + +[cols="1,5"] +|=== +| Name | `ft.circuitbreaker.opened.total` +| Type +a| * OpenTelemetry: `LongCounter` +* Micrometer: `Counter` +| Unit | None +| Description | Number of times the circuit breaker has moved from closed state to open state +| Tags +a| * `method` - the fully qualified method name +|=== + +[cols="1,5"] +|=== +| Name | `ft.circuitbreaker.state.current` +2+a| +include::{partialsdir}/srye-feature.adoc[] +| Type +a| * OpenTelemetry: `LongUpDownCounter` +* Micrometer: `Gauge` +| Unit | None +| Description | Whether the circuit breaker is currently in given state (`1`) or not (`0`) +| Tags +a| * `method` - the fully qualified method name +* `state` = `[open\|closed\|halfOpen]` - the circuit breaker state +|=== + +See xref:reference/metrics.adoc[the Metrics reference guide] for general metrics information. + +== Extra Features + +=== Circuit Breaker Maintenance + +include::{partialsdir}/srye-feature.adoc[] + +It is sometimes useful to see the circuit breaker status from within the application, or reset it to the initial state. +This is possible in two steps: + +. Give the circuit breaker a name by annotating the guarded method with `@CircuitBreakerName`: ++ +[source,java] +---- +@ApplicationScoped +public class MyService { + @CircuitBreaker + @CircuitBreakerName("hello-cb") // <1> + public String hello() { + ... + } +} +---- ++ +<1> The circuit breaker guarding the `MyService.hello` method is given a name `hello-cb`. +. Inject `CircuitBreakerMaintenance` and call its methods: ++ +[source,java] +---- +@ApplicationScoped +public class Example { + @Inject + CircuitBreakerMaintenance maintenance; + + public void test() { + System.out.println("Circuit breaker state: " + + maintenance.currentState("hello-cb")); // <1> + maintenance.resetAll(); // <2> + } +} +---- ++ +<1> Obtains current circuit breaker state. +<2> Resets all circuit breakers to the initial state. + +The `CircuitBreakerMaintenance` interface provides 4 methods: + +. `currentState(name)`: returns current state of given circuit breaker. +The return type `CircuitBreakerState` is an `enum` with 3 values: `CLOSED`, `OPEN`, `HALF_OPEN`. +. `onStateChange(name, callback)`: registers a callback that will be called when given circuit breaker changes state. +. `reset(name)`: resets given circuit breaker to the initial state. +. `resetAll()`: resets all circuit breakers in the application to the initial state. + +See the javadoc of those methods for more information. + +[[inspecting-exception-cause-chains]] +=== Inspecting Exception Cause Chains + +include::{partialsdir}/srye-feature.adoc[] + +include::{partialsdir}/non-compat.adoc[] + +The `@CircuitBreaker` annotation can specify that certain exceptions should be treated as failures (`failOn`) and others as successes (`skipOn`). +The specification limits this to inspecting the actual exception that was thrown. +However, in many cases, exceptions are wrapped and the exception the user wants to decide on is only present in the cause chain. + +For that reason, in the non-compatible mode, if the actual thrown exception isn't known failure or known success, {smallrye-fault-tolerance} inspects the cause chain. +To be specific, in case a `@CircuitBreaker` method throws an exception, the decision process is: + +1. If the `skipOn` exceptions are not default and the exception is assignable to one of the `skipOn` exceptions, the circuit breaker treats it as a success. +2. Otherwise, if the `failOn` exceptions are not default and the exception is assignable to one of the `failOn` exceptions, the circuit breaker treats it as a failure. +3. Otherwise, if the exception is assignable to one of the `skipOn` exceptions or its cause chain contains an exception assignable to one of the `skipOn` exceptions, the circuit breaker treats it as a success. +4. Otherwise, if the exception is assignable to one of the `failOn` exceptions or its cause chain contains an exception assignable to one of the `failOn` exceptions, the circuit breaker treats it as a failure. +5. Otherwise, the exception is treated as a success. + +For example: + +[source,java] +---- +@CircuitBreaker(requestVolumeThreshold = 10, + skipOn = ExpectedOutcomeException.class, // <1> + failOn = IOException.class) // <2> +public Result doSomething() { + ... +} +---- + +<1> If `doSomething` throws an `ExpectedOutcomeException`, or a `WrapperException` whose cause is `ExpectedOutcomeException`, the circuit breaker treats it as a success. +<2> If `doSomething` throws an `IOException`, or a `WrapperException` whose cause is `IOException`, the circuit breaker treats it as a failure. + +[source,java] +---- +@CircuitBreaker(requestVolumeThreshold = 10, + skipOn = ExpectedOutcomeException.class) // <1> <2> +public Result doSomething() { + ... +} +---- + +<1> If `doSomething` throws an `ExpectedOutcomeException`, or a `WrapperException` whose cause is `ExpectedOutcomeException`, the circuit breaker treats it as a success. +<2> There's no `failOn`, so the 2nd step in the algorithm above is skipped. +This is what turns the `WrapperException` whose cause is `ExpectedOutcomeException` into a success. diff --git a/content/docs/smallrye-fault-tolerance/latest/reference/config.adoc b/content/docs/smallrye-fault-tolerance/latest/reference/config.adoc new file mode 100644 index 000000000..7a9288a12 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/latest/reference/config.adoc @@ -0,0 +1,207 @@ += Configuration +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The fault tolerance strategies are primarily configured using annotations. +This is convenient, but changing such configuration requires recompilation. +For that reason, configuration using MicroProfile Config is also possible. + +NOTE: The configuration system of {microprofile-fault-tolerance}, as described in this guide, also applies to annotations defined by {smallrye-fault-tolerance}. + +== Annotation on Method + +The annotation may be present on a method: + +[source,java] +---- +package com.example; + +@ApplicationScoped +public class MyService { + @Retry(maxRetries = 10, retryOn = IOException.class) // <1> + public String hello() { + ... + } +} +---- + +<1> Retries will only be attempted if the thrown exception was `IOException`. +Other exceptions will be rethrown directly. +Also, the maximum number of retry attempts is changed to 10 (the default is 3). + +To change the `maxRetries` property using MicroProfile Config, set: + +[source,properties] +---- +smallrye.faulttolerance."com.example.MyService/hello".retry.max-retries=5 + +# alternatively, a specification-defined property can be used +com.example.MyService/hello/Retry/maxRetries=5 +---- + +== Annotation on Class + +The annotation may be present on a class, in which case it applies to all business method in the class: + +[source,java] +---- +package com.example; + +@ApplicationScoped +@Retry(maxRetries = 10, retryOn = IOException.class) // <1> +public class MyService { + public String hello() { + ... + } +} +---- +<1> The `hello()` method will use the `@Retry` strategy, as configured in this annotation. + +A class-level annotation may be overridden by explicitly annotating a method. + +The MicroProfile Config configuration property doesn't include the method name in this case: + +[source,properties] +---- +smallrye.faulttolerance."com.example.MyService".retry.max-retries=5 + +# alternatively, a specification-defined property can be used +com.example.MyService/Retry/maxRetries=5 +---- + +[IMPORTANT] +==== +Attempting to configure some property for a method or a class which does not have the annotation has no effect. + +* If the annotation is present on a method, the configuration property must target the method. +* If the annotation is present on a class, the configuration property must target the class. +==== + +== Global Configuration + +It is also possible to reconfigure some property for all annotations in the application. +For example: + +[source,properties] +---- +smallrye.faulttolerance.global.retry.max-retries=5 + +# alternatively, a specification-defined property can be used +Retry/maxRetries=5 +---- + +All `@Retry` annotations in the application will be considered to have `maxRetries` set to `5`. + +== Configuration Priority + +The most specific configuration has priority over more general configuration. + +For example: + +[source,java] +---- +package com.example; + +@ApplicationScoped +@Retry(maxRetries = 5) +public class MyService { + public String simpleHello() { + ... + } + + @Retry(maxRetries = 10) + public String complexHello() { + ... + } +} +---- + +The following configuration: + +[source,properties] +---- +smallrye.faulttolerance.global.retry.max-retries=30 +smallrye.faulttolerance."com.example.MyService".retry.max-retries=15 +smallrye.faulttolerance."com.example.MyService/complexHello".retry.max-retries=20 + +# alternatively, specification-defined properties can be used +Retry/maxRetries=30 +com.example.MyService/Retry/maxRetries=15 +com.example.MyService/complexHello/Retry/maxRetries=20 +---- + +Has the following effects: + +* `MyService.complexHello()` has `maxRetries` set to `20`. +* `MyService.simpleHello()` has `maxRetries` set to `15`. +* Other `@Retry` methods in the application have `maxRetries` set to `30`. + +== Disabling Fault Tolerance + +It is possible to disable certain fault tolerance annotation on some method, if the annotation is present on the method: + +[source,properties] +---- +smallrye.faulttolerance."com.example.MyService/hello".retry.enabled=false + +# alternatively, a specification-defined property can be used +com.example.MyService/hello/Retry/enabled=false +---- + +Or on some class, if the annotation is present on the class: + +[source,properties] +---- +smallrye.faulttolerance."com.example.MyService".retry.enabled=5 + +# alternatively, a specification-defined property can be used +com.example.MyService/Retry/enabled=false +---- + +Or globally: + +[source,properties] +---- +smallrye.faulttolerance.global.retry.enabled=false + +# alternatively, a specification-defined property can be used +Retry/enabled=false +---- + +It is also possible to disable all fault tolerance completely: + +[source,properties] +---- +smallrye.faulttolerance.enabled=false + +# alternatively, a specification-defined property can be used +MP_Fault_Tolerance_NonFallback_Enabled=false +---- + +This will leave only fallbacks enabled, all other annotations will be disabled. + +== {smallrye-fault-tolerance} Configuration Properties + +As demonstrated in the examples above, {smallrye-fault-tolerance} provides its own configuration properties, in addition to the specification-defined properties. +The specification-defined properties can of course be used, but the {smallrye-fault-tolerance} configuration properties have higher priority. + +The mapping is relatively straightforward: + +- `///` moves to `smallrye.faulttolerance."/"..` +- `//` moves to `smallrye.faulttolerance.""..` +- `/` moves to `smallrye.faulttolerance.global..` + +All the `` and `` parts are changed from camel case (`BeforeRetry`, `methodName`) to kebab case (`before-retry`, `method-name`). +Two annotation members are special cased to improve consistency: + +- `Retry/durationUnit` moves to `retry.max-duration-unit`, because the value property is called `maxDuration` (`max-duration`) +- `Retry/jitterDelayUnit` moves to `retry.jitter-unit`, because the value property is called `jitter` + +Further: + +- `MP_Fault_Tolerance_NonFallback_Enabled` moves to `smallrye.faulttolerance.enabled` +- `MP_Fault_Tolerance_Metrics_Enabled` moves to `smallrye.faulttolerance.metrics.enabled` + +== Links + +Configuration is described in detail in the {microprofile-fault-tolerance-url}#configuration[{microprofile-fault-tolerance} specification]. diff --git a/content/docs/smallrye-fault-tolerance/latest/reference/fallback.adoc b/content/docs/smallrye-fault-tolerance/latest/reference/fallback.adoc new file mode 100644 index 000000000..af6316c15 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/latest/reference/fallback.adoc @@ -0,0 +1,302 @@ += Fallback +:version-root: ../ +include::{version-root}_attributes.adoc[] + +For introduction, see xref:howto/fallback.adoc[How to Supply Fallback Values]. + +== Description + +If a guarded method throws an exception, fallback will prevent that exception from being propagated to the caller. +Instead, a fallback method or fallback handler is called and their result is returned. + +More specifically: + +. If the guarded method returns normally, its return value is returned. +. Otherwise, if the guarded method throws an exception whose type is assignable to any of the `skipOn` types (see <>), the exception is rethrown. +. Otherwise, if the guarded method throws an exception whose type is assignable to any of the `applyOn` types (see <>), the fallback value is obtained and returned. +. Otherwise, the exception is rethrown. + +[[interactions]] +== Interactions with Other Strategies + +See xref:howto/multiple.adoc[How to Use Multiple Strategies] for an overview of how fault tolerance strategies are nested. + +If `@Fallback` is used, the fallback method or handler may be invoked if other fault tolerance strategies throw an exception, depending on the fallback configuration. + +[[configuration]] +== Configuration + +There are 4 configuration options, corresponding to the 4 members of the `@Fallback` annotation. + +Exactly one of `value` or `fallbackMethod` must be set explicitly, they don't have a useful default value. + +=== `value` + +Type: `Class>` + +Default: empty + +Class of the fallback handler implementation that should be used to obtain the fallback value. +When this is set, the `fallbackMethod` must be left empty. + +The fallback handler class must implement the `FallbackHandler` interface. +The type argument to `FallbackHandler` must be assignable to the return type of the guarded method. +For example, if the guarded method returns `Number`, the fallback handler may implement `FallbackHandler` or `FallbackHandler`. + +=== `fallbackMethod` + +Type: `String` + +Default: empty + +Name of the fallback method that should be used to obtain the fallback value. +When this is set, the `value` must be left empty. + +The fallback method must be declared directly on the class where the guarded method is declared, or must be inherited from any of the class's supertypes. +The fallback method must have the same parameter types as the guarded method. +The fallback method must have a return type that is assignable to the return type of the guarded method. +For example, if the guarded method returns `Number`, the fallback method may return `Number` or `Integer`. + +[[apply-on]] +=== `applyOn` + +Type: `Class[]` + +Default: `{Throwable.class}` + +Set of exception types to which fallback applies. + +[[skip-on]] +=== `skipOn` + +Type: `Class[]` + +Default: `{}` + +Set of exception types to which fallback does _not_ apply. +This configuration takes precedence over `applyOn`. + +[[metrics]] +== Metrics + +Fallback does not emit any metrics on its own. + +See xref:reference/metrics.adoc[the Metrics reference guide] for general metrics information. +Fallback is included in the overall method invocation metric described there. + +== Extra Features + +[[fallback-methods-with-exception-parameter]] +=== Fallback Methods with Exception Parameter + +include::{partialsdir}/srye-feature.adoc[] + +include::{partialsdir}/non-compat.adoc[] + +{smallrye-fault-tolerance} provides access to the causing exception in a fallback method. + +A fallback method, as defind by the {microprofile-fault-tolerance} specification, must have the same parameters as the guarded method. +{smallrye-fault-tolerance} permits defining one additional parameter, at the end of the parameter list, which must be of an exception type. +If such parameter is defined, the exception that caused the fallback will be supplied in it. + +For example: + +[source,java] +---- +@ApplicationScoped +public class MyService { + @Fallback(fallbackMethod = "fallback") + public String doSomething(String param) { + ... + } + + public String fallback(String param, IllegalArgumentException cause) { // <1> + ... + } +} +---- + +<1> The fallback method matches the guarded method signature, except for one additional parameter at the end. + +All rules of {microprofile-fault-tolerance} specification related to looking up fallback methods still apply. +That is, the return types must match, the parameter types must match (with this one exception), etc. + +If the thrown exception is not assignable to the exception parameter type, it is rethrown as if no fallback was declared. +In the previous example, if `IllegalStateException` was thrown, the fallback method would not be called, as `IllegalStateException` is not a subtype of `IllegalArgumentException`. + +If the guarded method has a vararg parameter and you want to declare a fallback method with an exception parameter, simply replace the vararg syntax with an array type: + +[source,java] +---- +@ApplicationScoped +public class MyService { + @Fallback(fallbackMethod = "fallback") + public String doSomething(String... params) { + ... + } + + public String fallback(String[] params, IllegalArgumentException cause) { + ... + } +} +---- + +==== Multiple Fallback Methods with Exception Parameter + +It is possible to declare multiple overloads of the fallback method, each having different type of the exception parameter: + +[source,java] +---- +@ApplicationScoped +public class MyService { + @Fallback(fallbackMethod = "fallback") + public String doSomething(String param) { + ... + } + + public String fallback(String param, IllegalArgumentException cause) { + ... + } + + public String fallback(String param, RuntimeException cause) { + ... + } +} +---- + +In that case, which fallback method is called depends on the type of thrown exception. +The method that declares a most-specific supertype of the actual exception is selected. + +In the previous example, if `IllegalArgumentException` was thrown by `doSomething`, the first fallback method would be called. +If `IllegalStateException` was thrown, the second fallback method would be called. + +If the thrown exception is not assignable to the exception parameter type of any fallback method, it is rethrown as if no fallback was declared. + +==== Fallback Methods with and without Exception Parameter + +It is possible to declare the fallback method with and without an exception parameter at the same time: + +[source,java] +---- +@ApplicationScoped +public class MyService { + @Fallback(fallbackMethod = "fallback") + public String doSomething(String param) { + ... + } + + public String fallback(String param, IllegalArgumentException cause) { + ... + } + + public String fallback(String param, RuntimeException cause) { + ... + } + + public String fallback(String param) { + ... + } +} +---- + +The fallback methods with an exception parameter have precedence. +The fallback method without an exception parameter is only called if the thrown exception is not assignable to any declared exception parameter. + +==== Interactions with `applyOn` / `skipOn` + +The presence or absence of a fallback method with specific exception parameter may seem related to the usage of `applyOn` / `skipOn` on the `@Fallback` annotation, but in fact, it is not. +These features are completely independent. + +Simply put, the `applyOn` / `skipOn` configuration is always evaluated first. +A fallback method is only selected and invoked when this configuration indicates that a fallback should apply. + +If `@Fallback` is configured to skip `IllegalStateException` and `IllegalStateException` is thrown, no fallback method is invoked. +That applies even if a fallback method with a matching exception parameter exists. + +For example: + +[source,java] +---- +@ApplicationScoped +public class MyService { + @Fallback(fallbackMethod = "fallback", skipOn = IllegalStateException.class) + public String doSomething(String param) { + ... + } + + public String fallback(String param, IllegalArgumentException cause) { + ... + } + + public String fallback(String param, RuntimeException cause) { + ... + } + + public String fallback(String param) { + ... + } +} +---- + +In this case: + +- if `doSomething` throws `IllegalArgumentException`, the first fallback method is called; +- if `doSomething` throws `IllegalStateException`, no fallback method is called, because this exception type is skipped; +- if `doSomething` throws any other `RuntimeException`, the second fallback method is called; +- if `doSomething` throws any other exception, the last fallback method is called. + +[[inspecting-exception-cause-chains]] +=== Inspecting Exception Cause Chains + +include::{partialsdir}/srye-feature.adoc[] + +include::{partialsdir}/non-compat.adoc[] + +The `@Fallback` annotation can specify that certain exceptions should be treated as failures (`applyOn`) and others as successes (`skipOn`). +The specification limits this to inspecting the actual exception that was thrown. +However, in many cases, exceptions are wrapped and the exception the user wants to decide on is only present in the cause chain. + +For that reason, in the non-compatible mode, if the actual thrown exception isn't known failure or known success, {smallrye-fault-tolerance} inspects the cause chain. +To be specific, in case a `@Fallback` method throws an exception, the decision process is: + +1. If the `skipOn` exceptions are not default and the exception is assignable to one of the `skipOn` exceptions, fallback is skipped and the exception is rethrown. +2. Otherwise, if the `applyOn` exceptions are not default and the exception is assignable to one of the `applyOn` exceptions, fallback is applied. +3. Otherwise, if the exception is assignable to one of the `skipOn` exceptions or its cause chain contains an exception assignable to one of the `skipOn` exceptions, fallback is skipped and the exception is rethrown. +4. Otherwise, if the exception is assignable to one of the `applyOn` exceptions or its cause chain contains an exception assignable to one of the `applyOn` exceptions, fallback is applied. +5. Otherwise, the exception is rethrown. + +For example: + +[source,java] +---- +@Fallback(fallbackMethod = "fallback", + skipOn = ExpectedOutcomeException.class, // <1> + applyOn = IOException.class) // <2> +public Result doSomething() { + ... +} + +public Result fallback() { + ... +} +---- + +<1> If `doSomething` throws an `ExpectedOutcomeException`, or a `WrapperException` whose cause is `ExpectedOutcomeException`, fallback is skipped and the exception is thrown. +<2> If `doSomething` throws an `IOException`, or a `WrapperException` whose cause is `IOException`, fallback is applied. + +[source,java] +---- +@Fallback(fallbackMethod = "fallback", + skipOn = ExpectedOutcomeException.class) // <1> <2> +public Result doSomething() { + ... +} + +public Result fallback() { + ... +} +---- + +<1> If `doSomething` throws an `ExpectedOutcomeException`, or a `WrapperException` whose cause is `ExpectedOutcomeException`, fallback is skipped and the exception is thrown. +<2> There's no `applyOn`, so the 2nd step in the algorithm above is skipped. +This is what turns the `WrapperException` whose cause is `ExpectedOutcomeException` into a skipped fallback. diff --git a/content/docs/smallrye-fault-tolerance/latest/reference/metrics.adoc b/content/docs/smallrye-fault-tolerance/latest/reference/metrics.adoc new file mode 100644 index 000000000..09300ea27 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/latest/reference/metrics.adoc @@ -0,0 +1,109 @@ += Metrics +:version-root: ../ +include::{version-root}_attributes.adoc[] + +{smallrye-fault-tolerance} exposes metrics, as {microprofile-fault-tolerance-url}#_integration_with_microprofile_metrics_and_microprofile_telemetry[specified] by {microprofile-fault-tolerance}. + +[[general]] +== General Metrics + +For all methods guarded with some fault tolerance strategy, the following metric is emitted: + +[cols="1,5"] +|=== +| Name | `ft.invocations.total` +| Type +a| * OpenTelemetry: `LongCounter` +* Micrometer: `Counter` +| Unit | None +| Description | The number of times the method was called. +| Tags +a| * `method` - the fully qualified method name +* `result` = `[valueReturned\|exceptionThrown]` - whether the invocation returned a value or threw an exception +* `fallback` = `[applied\|notApplied\|notDefined]` - `applied` if fallback was used, `notApplied` if a fallback is configured but was not used, `notDefined` if a fallback is not configured +|=== + +For xref:reference/asynchronous.adoc[asynchronous methods], "value returned" means that the `CompletionStage` completed successfully, while "exception thrown" means that the `CompletionStage` completed exceptionally. + +== Specific Metrics + +In addition to the general metrics described above, most fault tolerance strategies emit their own metrics. +Those are described in the reference guides: + +* xref:reference/bulkhead.adoc#metrics[Bulkhead] +* xref:reference/circuit-breaker.adoc#metrics[Circuit Breaker] +* xref:reference/rate-limit.adoc#metrics[Rate Limit] +* xref:reference/retry.adoc#metrics[Retry] +* xref:reference/timeout.adoc#metrics[Timeout] + +== Timer Metrics + +For task scheduling purposes (e.g. watching timeouts or delaying retries), {smallrye-fault-tolerance} maintains one thread called the _timer thread_. +Most of the time, it is kept sleeping (parked), it only wakes up (unparks) when necessary to submit tasks to executors. + +The behavior of the timer thread can be observed through the following metrics: + +[cols="1,5"] +|=== +| Name | `ft.timer.scheduled` +| Type +a| * OpenTelemetry: `LongUpDownCounter` +* Micrometer: `Gauge` +| Unit | None +| Description | The number of tasks that are currently scheduled (for future execution) on the timer. +| Tags +a| * `id` - the ID of the timer, to distinguish multiple timers in a multi-application environment +|=== + +== Micrometer Support + +In addition to the OpenTelemetry support (as specified by {microprofile-fault-tolerance}), {smallrye-fault-tolerance} also provides support for https://micrometer.io/[Micrometer]. +The set of metrics emitted to Micrometer is the same, using the same metric names and tags. + +Note that distribution summaries in Micrometer, including timers, do not emit quantiles by default. +Micrometer recommends that libraries should not configure them out of the box, so if you need them, you should use a `MeterFilter`. + +The following implementation makes sure Micrometer emits some common quantiles for all fault tolerance metrics: + +[source,java] +---- +static final MeterFilter ENABLE_HISTOGRAMS = new MeterFilter() { + @Override + public DistributionStatisticConfig configure(Meter.Id id, DistributionStatisticConfig config) { + if (id.getName().startsWith("ft.")) { + return DistributionStatisticConfig.builder() + .percentiles(0.5, 0.75, 0.9, 0.95, 0.99, 0.999) + .build() + .merge(config); + } + return config; + } +}; +---- + +== Disabling Metrics + +It is possible to completely disable fault tolerance metrics using MicroProfile Config: + +[source,properties] +---- +smallrye.faulttolerance.metrics.enabled=false + +# alternatively, a specification-defined property can be used +MP_Fault_Tolerance_Metrics_Enabled=false +---- + +It is also possible to disable individual metric providers: + +[source,properties] +---- +# to disable OpenTelemetry Metrics +smallrye.faulttolerance.opentelemetry.disabled=true + +# to disable Micrometer +smallrye.faulttolerance.micrometer.disabled=true +---- + +Note that setting `smallrye.faulttolerance.*.disabled` to `false` does not mean the provider is enabled unconditionally. +When that provider is not discovered or selected by the integrator, it cannot be enabled in any way. +These properties are only meant for disabling an otherwise enabled metrics provider; not the other way around. diff --git a/content/docs/smallrye-fault-tolerance/latest/reference/non-compat.adoc b/content/docs/smallrye-fault-tolerance/latest/reference/non-compat.adoc new file mode 100644 index 000000000..20fcf0b64 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/latest/reference/non-compat.adoc @@ -0,0 +1,36 @@ += Non-compatible Mode +:version-root: ../ +include::{version-root}_attributes.adoc[] + +include::{partialsdir}/srye-feature.adoc[] + +{smallrye-fault-tolerance} offers a mode where certain features are improved beyond specification, as described below. +This mode is *not compatible* with the {microprofile-fault-tolerance} specification (and doesn't necessarily pass the entire TCK). + +This mode is disabled by default. +To enable, set the configuration property `smallrye.faulttolerance.mp-compatibility` to `false`. + +.Quarkus +**** +In Quarkus, the non-compatible mode is enabled by default. +To restore compatibility, add the following to your `application.properties`: + +[source,properties] +---- +smallrye.faulttolerance.mp-compatibility=true +---- + +Note that the non-compatible mode is available since {smallrye-fault-tolerance} 5.2.0 and Quarkus 2.1.0.Final. +Previous versions are always compatible. +**** + +== Improvements + +The improvements enabled in the non-compatible mode are: + +* xref:reference/asynchronous.adoc#method-asynchrony[Determining asynchrony from method signatures] +* Inspecting exception cause chains +** in xref:reference/fallback.adoc#inspecting-exception-cause-chains[fallback] +** in xref:reference/circuit-breaker.adoc#inspecting-exception-cause-chains[circuit breaker] +** in xref:reference/retry.adoc#inspecting-exception-cause-chains[retry] +* xref:reference/fallback.adoc#fallback-methods-with-exception-parameter[Fallback methods with exception parameter] diff --git a/content/docs/smallrye-fault-tolerance/latest/reference/programmatic-api.adoc b/content/docs/smallrye-fault-tolerance/latest/reference/programmatic-api.adoc new file mode 100644 index 000000000..2260df49e --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/latest/reference/programmatic-api.adoc @@ -0,0 +1,414 @@ += Programmatic API +:version-root: ../ +include::{version-root}_attributes.adoc[] + +include::{partialsdir}/srye-feature.adoc[] + +In addition to the declarative, annotation-based API of {microprofile-fault-tolerance}, {smallrye-fault-tolerance} also offers a programmatic API for advanced scenarios. +This API is present in the `io.smallrye:smallrye-fault-tolerance-api` artifact, just like all other additional APIs {smallrye-fault-tolerance} provides. + +== Installation + +If you use {smallrye-fault-tolerance} as part of a runtime that implements {microprofile-fault-tolerance}, you don't have to do anything. +The programmatic API is ready to use. +In this documentation, we'll call this the _CDI implementation_ of the programmatic API, because it's integrated with the {microprofile-fault-tolerance} implementation, which is naturally based on CDI. + +.Quarkus +**** +In Quarkus, the {smallrye-fault-tolerance} API is brought in automatically, as a transitive dependency of the Quarkus extension for {smallrye-fault-tolerance}. +That is, you don't need to do anything to be able to use the programmatic API. +**** + +.WildFly +**** +In WildFly, the {smallrye-fault-tolerance} API is not readily available to deployments. +If you want to use it, you need to add a module dependency to the deployment using `jboss-deployment-structure.xml`. + +Note that at the time of this writing, the {smallrye-fault-tolerance} module in WildFly is considered private. +If you do add a module dependency on it, to be able to use the {smallrye-fault-tolerance} API, you may be stepping out of the WildFly support scope. +**** + +In addition to the CDI implementation, {smallrye-fault-tolerance} also offers a _standalone implementation_ that is meant to be used outside of any runtime. +This implementation does not need CDI or anything else. +If you want to use {smallrye-fault-tolerance} in a standalone fashion, just add a dependency on `io.smallrye:smallrye-fault-tolerance-standalone`. +The API is brought in transitively. + +== Usage + +The entrypoints to the programmatic API are the `Guard` and `TypedGuard` interfaces. + +These interfaces represent a configured set of fault tolerance strategies. +Their configuration, order of application and behavior in general corresponds to the declarative API, so if you know that, you'll feel right at home. +If not, the javadoc has all the information you need (though it often points to the annotation-based API for more information). + +The interfaces are very similar, there are only 2 differences: + +. `Guard` requires specifying the return type of the guarded operation for each `call()` or `get()` (or `adapt*`) method call. + `TypedGuard`, on the other hand, requires specifying the return type once, when creating an instance using `create()`. + Therefore, `Guard` may be used for guarding many different types, while `TypedGuard` may only be used to guard single type. +. `TypedGuard` allows defining a fallback. + `Guard` does not. + +In the following text, we'll only talk about `Guard`, but it also applies to `TypedGuard` without a change (except of the differences mentioned above). + +To create an instance of `Guard`, you can use the `create` static method. +It returns a builder which has to be used to add and configure all the fault tolerance strategies that should apply. +There is no external configuration, so all configuration properties have to be set explicitly, using the builder methods. +If you don't set a configuration property, it will default to the same value the annotation-based API uses. + +.Disabling Fault Tolerance +**** +There's one exception to the "no external configuration" rule. + +The CDI implementation looks for the `smallrye.faulttolerance.enabled` / `MP_Fault_Tolerance_NonFallback_Enabled` configuration properties in MicroProfile Config. +The standalone implementation looks for system properties with the same name, or obtains the value from custom configuration (as xref:integration/programmatic-api.adoc[described] in the integration section). + +If at least one of these properties exists and is set to `false`, only the fallback and thread offload fault tolerance strategies will be applied. +Everything else will be ignored. + +Note that this is somewhat different to the declarative, annotation-based API, where only fallback is retained and the `@Asynchronous` strategy is skipped as well. +Since this significantly changes execution semantics, the programmatic API will apply thread offload even if fault tolerance is disabled. + +Similarly to the declarative API, implementations of the programmatic API also read this property only once, when the `Guard` API is first used. +It is _not_ read again later. +**** + +Let's take a look at a simple example: + +[source,java] +---- +public class MyService { + private static final Guard GUARD = Guard.create() + .withRetry().maxRetries(3).done() + .build(); + + public String hello() throws Exception { + return GUARD.call(() -> externalService.hello(), String.class); // <1> + } +} +---- + +<1> Here, we call `externalService.hello()` and guard the call with the previously configured set of fault tolerance strategies. + The `call` method used here takes the `Callable` type, which represent the guarded action. + The `get` method works just like `call`, but accepts a `Supplier`. + +The previous example shows how to apply fault tolerance to synchronous actions. +{smallrye-fault-tolerance} naturally also supports guarding asynchronous actions, using the `java.util.concurrent.CompletionStage` type. +Unlike the declarative API, the programmatic API doesn't support asynchronous actions that return the `java.util.concurrent.Future` type. + +[source,java] +---- +public class MyService { + private static final Guard GUARD = Guard.create() + .withBulkhead().done() + .withThreadOffload(true) // <1> + .build(); + + public CompletionStage hello() throws Exception { + return GUARD.call(() -> externalService.hello(), + new TypeLiteral>() {}); // <2> + } +} +---- + +<1> The thread offload here only applies to asynchronous actions. + If you use the same `GUARD` to guard a synchronous action, no thread offload will apply. +<2> Note that here, we use the `TypeLiteral` class (from CDI) to specify the type of the guarded action. + +Asynchronous actions may be blocking or non-blocking. +In the example above, we assume the `externalService.hello()` call is blocking, so we set thread offload to `true`. +{smallrye-fault-tolerance} will automatically move the actual execution of the action to another thread. + +If we didn't configure `withThreadOffload`, however, the execution of an asynchronous action would continue on the original thread. +This is often desired for non-blocking actions, which are very common in modern reactive architectures. + +Also note that in this example, we configured multiple fault tolerance strategies: bulkhead and thread offload. +When that happens, the fault tolerance strategies are ordered according to the {microprofile-fault-tolerance} specification, just like in the declarative API. +Order of all the `with*` method invocations doesn’t matter. + +=== Single-Action Usage + +The `Guard` API is general and permits guarding multiple different actions using the same set of fault tolerance strategies. +Often, we need to guard just a single action, although possibly several times. + +For such use case, the `Guard` instance may be adapted to a `Callable` or `Supplier` using the `adapt*` methods: + +[source,java] +---- +public class MyService { + private static final Callable guard = Guard.create() + .withTimeout().duration(5, ChronoUnit.SECONDS).done() + .build() // <1> + .adaptCallable(() -> externalService.hello(), String.class); // <2> + + public String hello() throws Exception { + return callable.call(); // <3> + } + +} +---- + +<1> Create a `Guard` object that can guard arbitrary actions. +<2> Adapt the general `Guard` instance to a `Callable` that guards the `externalService.hello()` invocation. + Similar method exists that accepts and returns a `Supplier`: `adaptSupplier`. +<3> You can do whatever you wish with the adapted `Callable`. + Here, we just call it once, which isn't very interesting, but it could possibly be called multiple times, passed to other methods etc. + +=== Synchronous vs. Asynchronous + +The `Guard` and `TypedGuard` interfaces both decide whether the guarded action is synchronous or asynchronous based on the action type. +This is given to `Guard` when calling or adapting the action, and to `TypedGuard` when creating it. + +If the type is asynchronous (such as `CompletionStage`), the asynchronous behavior will also be guarded; the action is only considered complete when the `CompletionStage` actually completes. +If the type is not asynchronous, only the synchronous behavior will be guarded; the action is considered complete when the method returns. + +To enable support of additional asynchronous types, it is enough to include the support libraries, as described in xref:reference/asynchronous.adoc#async-types[Additional Asynchronous Types]. + +==== Mutiny Support + +For example, when the Mutiny support library `io.smallrye:smallrye-fault-tolerance-mutiny` is present, both `Guard` and `TypedGuard` will recognize `Uni` as an asynchronous type and guard it properly. +Guarding a `Multi` is not supported. + +For example: + +[source,java] +---- +public class MyService { + private final Supplier> GUARD = TypedGuard.create( + new TypeLiteral>() {}) + .withTimeout().duration(5, ChronoUnit.SECONDS).done() + .withFallback().handler(() -> Uni.createFrom().item("fallback")).done() + .build() + .adaptSupplier(() -> externalService.hello()); // <1> + + public Uni hello() { + return guard.get(); + } +} +---- + +<1> The call to `externalService.hello()` is supposed to return `Uni`. + +Note that the `Uni` type is lazy, so the action itself won't execute until the guarded `Uni` is subscribed to. + +.Quarkus +**** +In Quarkus, the Mutiny support library is present by default. +You can guard `Uni`-returning actions out of the box. +**** + +=== Stateful Fault Tolerance Strategies + +The bulkhead, circuit breaker and rate limit strategies are stateful. +That is, they hold some state required for their correct functioning, such as the number of current executions for bulkhead, the rolling window of successes/failures for circuit breaker, or the time window for rate limit. +If you use these strategies, you have to consider their lifecycle. + +The {smallrye-fault-tolerance} programmatic API makes such reasoning pretty straightforward. +Each `Guard` object has its own instance of each fault tolerance strategy, including the stateful strategies. +If you use a single `Guard` object for guarding multiple different actions, all those actions will be guarded by the same bulkhead, circuit breaker and/or rate limit. +If, on the other hand, you use different `Guard` objects for guarding different actions, each action will be guarded by its own bulkhead, circuit breaker and/or rate limit. + +If you call the `adapt*` methods on the same `Guard` multiple times, the resulting `Callable` or `Supplier` objects will guard the underlying action using the original `Guard` instance, so stateful strategies will be shared. + +=== Circuit Breaker Maintenance + +The `CircuitBreakerMaintenance` API, accessed through `CircuitBreakerMaintenance.get()` or by injection in the CDI implementation, can be used to manipulate all named circuit breakers. +A circuit breaker is given a name by calling `withCircuitBreaker().name("\...")` on the fault tolerance builder, or using the `@CircuitBreakerName` annotation in the declarative API. + +Additionally, `CircuitBreakerMaintenance.resetAll()` will also reset all unnamed circuit breakers declared using the `@CicruitBreaker` annotation. +For this to work, all unnamed circuit breakers have to be remembered. +This is safe in case of the declarative, annotation-based API, because the number of such declared circuit breakers is fixed. +At the same time, this would _not_ be safe to do for all unnamed circuit breakers created using the programmatic API, as their number is potentially unbounded. +(In other words, remembering all unnamed circuit breakers created using the programmatic API would easily lead to a memory leak.) + +Therefore, all circuit breakers created using the programmatic API must be given a name when `CircuitBreakerMaintenance` is supposed to affect them. +Note that duplicate names are not permitted and lead to an error, so lifecycle of the circuit breaker must be carefully considered. + +=== Event Listeners + +The programmatic API has one feature that the declarative API doesn't have: ability to observe certain events. +For example, when configuring a circuit breaker, it is possible to register a callback for circuit breaker state changes or for a situation when an open circuit breaker prevents an invocation. +When configuring a timeout, it is possible to register a callback for when the invocation times out, etc. etc. +For example: + +[source,java] +---- +private static final Guard GUARD = Guard.create() + .withTimeout().duration(5, ChronoUnit.SECONDS).onTimeout(() -> ...).done() // <1> + .build(); +---- + +<1> The `onTimeout` method takes a `Runnable` that will later be executed whenever an invocation guarded by `GUARD` times out. + +All event listeners registered like this must run quickly and must not throw exceptions. + +== Configuration + +As mentioned above, except of `smallrye.faulttolerance.enabled` / `MP_Fault_Tolerance_NonFallback_Enabled`, there is no support for external configuration of fault tolerance strategies. +This may change in the future, though possibly only in the CDI implementation. + +== Metrics + +The programmatic API is integrated with metrics. +All metrics, as described xref:reference/metrics.adoc[in the Metrics reference guide] and the linked guides, are supported. +The only difference is the value of the `method` tag. +With the programmatic API, the `method` tag will be set to the _description_ of the guarded operation, provided on the `Guard` builder. + +[source,java] +---- +private static final Guard GUARD = Guard.create() + .withDescription("hello") // <1> + .withRetry().maxRetries(3).done() + .build(); +---- + +<1> A description of `hello` is set, it will be used as a value of the `method` tag in all metrics. + +It is possible to create multiple `Guard` objects with the same description. +In this case, it won't be possible to distinguish the different `Guard` objects in metrics; their values will be aggregated. + +If no description is provided, metrics are not emitted. + +== Differences to the Specification + +`Guard` and `TypedGuard` have the following differences to standard MicroProfile Fault Tolerance: + +* asynchronous actions of type `java.util.concurrent.Future` are not supported; +* the fallback, circuit breaker and retry strategies always inspect the cause chain of exceptions, following the behavior of SmallRye Fault Tolerance in the non-compatible mode. + +== Kotlin `suspend` Functions + +The `Guard` and `TypedGuard` APIs do not support Kotlin `suspend` functions at the moment. + +== Integration Concerns + +Integration concerns, which are particularly interesting for users of the standalone implementation, are xref:integration/programmatic-api.adoc[described] in the integration section. + +[[migration_from_faulttolerance]] +== Migration from `FaultTolerance` + +The 1st version of the programmatic API had the `FaultTolerance` interface. +That interface was removed in {smallrye-fault-tolerance} 7.0. +The replacement are the `Guard` and `TypedGuard` types. + +When migrating, one first has to decide which type to use as a replacement. +The `TypedGuard` is much closer to the `FaultTolerance` type, so that is the easiest way to migrate. +There are some differences still: + +- the `create()` static method now takes a parameter that expresses the guarded action type, either as a `Class` or as a `TypeLiteral`; +- there are no `cast()` and `castAsync()` methods, because `TypedGuard` only guards actions of a single type; +- there is no support for guarding or adapting ``Runnable``s, only ``Callable``s and ``Supplier``s are supported. + As a replacement for `Runnable`, a `Supplier` (or `Supplier>` etc.) can be used. + +For example, these usages of `FaultTolerance`: + +[source,java] +---- +static final FaultTolerance FT1 = FaultTolerance.create() + .withDescription("ft1") + .withRetry().maxRetries(3).done() + .withFallback().handler(() -> "fallback").done() + .build(); + +static final FaultTolerance> FT2 = FaultTolerance.createAsync() + .withDescription("ft2") + .withBulkhead().limit(5).queueSize(100).done() + .withTimeout().duration(3, ChronoUnit.SECONDS).done() + .build(); +---- + +can be rewritten to: + +[source,java] +---- +static final TypedGuard GUARD1 = TypedGuard.create(String.class) + .withDescription("ft1") + .withRetry().maxRetries(3).done() + .withFallback().handler(() -> "fallback").done() + .build(); + +static final TypedGuard> GUARD2 = TypedGuard.create( + new TypeLiteral>() {}) + .withDescription("ft2") + .withBulkhead().limit(5).queueSize(100).done() + .withTimeout().duration(3, ChronoUnit.SECONDS).done() + .build(); +---- + +After creating a `TypedGuard`, there's no change in how it's used. +The methods `call()`, `get()`, `adaptCallable()` and `adaptSupplier()` have the same signatures as before. + +`TypedGuard` is the only possible replacement if you need fallback. +If you don't need fallback, and especially if you need to guard actions of multiple types, `Guard` is a better choice. + +The differences are: + +- `Guard` has no type parameter (is not generic) and does not allow defining fallback; +- there are no `cast()` and `castAsync()` methods, because they are no longer needed; +- there is no support for guarding or adapting ``Runnable``s, only ``Callable``s and ``Supplier``s are supported. + As a replacement for `Runnable`, a `Supplier` (or `Supplier>` etc.) can be used. + +For example, these usages of `FaultTolerance`: + +[source,java] +---- +static final FaultTolerance FT1 = FaultTolerance.create() + .withDescription("ft1") + .withRetry().maxRetries(3).done() + .withCircuitBreaker().done() + .build(); + +static final FaultTolerance> FT2 = FaultTolerance.createAsync() + .withDescription("ft2") + .withBulkhead().limit(5).queueSize(100).done() + .withTimeout().duration(3, ChronoUnit.SECONDS).done() + .build(); +---- + +can be rewritten to: + +[source,java] +---- +static final Guard GUARD1 = Guard.create() + .withDescription("ft1") + .withRetry().maxRetries(3).done() + .withCircuitBreaker().done() + .build(); + +static final Guard GUARD2 = Guard.create() + .withDescription("ft2") + .withBulkhead().limit(5).queueSize(100).done() + .withTimeout().duration(3, ChronoUnit.SECONDS).done() + .build(); +---- + +Now, as mentioned above, `Guard` is used slightly differently than `FaultTolerance`. +The methods `call()`, `get()`, `adaptCallable()` and `adaptSupplier()` take an extra parameter that expresses the type of the guarded action, again either as a `Class` or as a `TypeLiteral`. + +For example, these usages of `FaultTolerance`: + +[source,java] +---- +String result1 = FT1.call(() -> externalService.hello()); +CompletionStage result2 = FT2.get(() -> externalService.helloAsync()); + +Callable callable = FT1.adaptCallable( + () -> externalService.hello()); +Supplier> supplier = FT2.adaptSupplier( + () -> externalService.helloAsync()); +---- + +can be rewritten to: + +[source,java] +---- +String result1 = GUARD1.call(() -> externalService.hello(), String.class); +CompletionStage result2 = GUARD2.get(() -> externalService.helloAsync(), + new TypeLiteral>() {}); + +Callable callable = GUARD1.adaptCallable( + () -> externalService.hello(), String.class); +Supplier> supplier = GUARD2.adaptSupplier( + () -> externalService.helloAsync(), + new TypeLiteral>() {}); +---- diff --git a/content/docs/smallrye-fault-tolerance/latest/reference/rate-limit.adoc b/content/docs/smallrye-fault-tolerance/latest/reference/rate-limit.adoc new file mode 100644 index 000000000..abb823f0f --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/latest/reference/rate-limit.adoc @@ -0,0 +1,148 @@ += Rate Limit +:version-root: ../ +include::{version-root}_attributes.adoc[] + +For introduction, see xref:howto/rate-limit.adoc[How to Limit Execution Rate]. + +include::{partialsdir}/srye-feature.adoc[] + +== Description + +Rate limit enforces a maximum number of permitted invocations in a time window of some length. +For example, with a rate limit, one can make sure that a method may only be called 50 times per minute. +Invocations that would exceed the limit are rejected with an exception of type `RateLimitException`. + +Additionally, it is possible to define minimum spacing between invocations. +For example, with minimum spacing of 1 second, if a second invocation happens 500 millis after the first, it is rejected even if the limit would not be exceeded yet. + +Rate limit is superficially similar to a bulkhead (concurrency limit), but is in fact quite different. +Bulkhead limits the number of executions happening concurrently at any point in time. +Rate limit limits the number of executions in a time window of some length, without considering concurrency. + +A method or a class can be annotated with `@RateLimit`, which means the method or the methods in the class will apply the rate limit strategy. +The previous example with 50 maximum invocations per minute and minimum spacing of 1 second would look like this: + +[source,java] +---- +@RateLimit(value = 50, + window = 1, windowUnit = ChronoUnit.MINUTES, + minSpacing = 1, minSpacingUnit = ChronoUnit.SECONDS) +public void doSomething() { + ... +} +---- + +The `RateLimitException` class has a method `getRetryAfterMillis()` which returns the number of milliseconds after which the user may retry the rejected invocation. +Retrying sooner is guaranteed to be rejected again. +Note that this information is accurate only at the time the invocation is rejected. +It may be invalidated by any subsequent or concurrent invocations, so there is no guarantee that a retry attempt after the given number of milliseconds will in fact be permitted. + +=== Lifecycle + +Rate limit needs to maintain some state between invocations: the number of recent invocations, the time stamp of last invocation, and so on. +This state is a singleton, irrespective of the lifecycle of the bean that uses the `@RateLimit` annotation. + +More specifically, the rate limit state is uniquely identified by the combination of the bean class (`java.lang.Class`) and the method object (`java.lang.reflect.Method`) representing the guarded method. + +For example, if there’s a guarded method `doWork` on a bean which is `@RequestScoped`, each request will have its own instance of the bean, but all invocations of `doWork` will share the same rate limit state. + +[[interactions]] +== Interactions with Other Strategies + +See xref:howto/multiple.adoc[How to Use Multiple Strategies] for an overview of how fault tolerance strategies are nested. + +If `@Fallback` is used with `@RateLimit`, the fallback method or handler may be invoked if a `RateLimitException` is thrown, depending on the fallback configuration. + +If `@Retry` is used with `@RateLimit`, each retry attempt is processed by the rate limit as an independent invocation. +If `RateLimitException` is thrown, the execution may be retried, depending on how retry is configured. + +If `@CircuitBreaker` is used with `@RateLimit`, the circuit breaker is checked before enforcing the rate limit. +If rate limiting results in `RateLimitException`, this may be counted as a failure, depending on how the circuit breaker is configured. + +[[configuration]] +== Configuration + +There are 6 configuration options, corresponding to the 6 members of the `@RateLimit` annotation. + +=== `value` + +Type: `int` + +Default: `100` + +The limit of maximum invocations to be permitted in the time window. + +=== `window` + `windowUnit` + +Type: `long` + `ChronoUnit` + +Default: `1 second` + +The length of the time window. + +=== `minSpacing` + `minSpacingUnit` + +Type: `long` + `ChronoUnit` + +Default: `0 seconds` + +The minimum spacing between two consecutive invocations. + +=== `type` + +Type: `RateLimitType` + +Default: `RateLimitType.FIXED` + +The type of time window used for rate limiting. +There are 3 types of time windows used for rate limiting: _fixed_, _rolling_ and _smooth_. + +Fixed time windows are a result of dividing time into non-overlapping intervals of given length. +The invocation limit is enforced for each interval independently. +This means that short bursts of invocations occuring near the time window boundaries may temporarily exceed the configured rate limit. +This kind of rate limiting is also called _fixed window_ rate limiting. + +Rolling time windows enforce the limit continuously, instead of dividing time into independent intervals. +The invocation limit is enforced for all possible time intervals of given length, regardless of overlap. +This is more precise, but requires more memory and may be slower. +This kind of rate limiting is also called _sliding log_ rate limiting. + +Smooth time windows enforce a uniform distribution of invocations under a rate calculated from given time window length and given limit. +If recent rate of invocations is under the limit, a subsequent burst of invocations is allowed during a shorter time span, but the calculated rate is never exceeded. +This kind of rate limiting is also called _token bucket_ or _leaky bucket (as a meter)_ rate limiting, with the additional property that all work units are considered to have the same size. + +With fixed and rolling time windows, rejected invocations always count towards the limit, so if a caller continuously invokes the guarded method faster than the configuration allows, all invocations are rejected until the caller slows down. +With smooth time windows, rejected invocations do not count towards the recent rate of invocations. + +Example usage: + +[source,java] +---- +@RateLimit(value = 50, + window = 1, windowUnit = ChronoUnit.MINUTES, + minSpacing = 1, minSpacingUnit = ChronoUnit.SECONDS, + type = RateLimitType.ROLLING) +public void doSomething() { + ... +} +---- + +[[metrics]] +== Metrics + +Rate limit exposes the following metrics: + +[cols="1,5"] +|=== +| Name | `ft.ratelimit.calls.total` +| Type +a| * OpenTelemetry: `LongCounter` +* Micrometer: `Counter` +| Unit | None +| Description | The number of times the rate limit logic was run. This is usually once per method call, but may be zero times if the circuit breaker prevented execution or more than once if the method call was retried. +| Tags +a| * `method` - the fully qualified method name +* `rateLimitResult` = `[permitted\|rejected]` - whether the rate limit permitted the method call +|=== + +See xref:reference/metrics.adoc[the Metrics reference guide] for general metrics information. diff --git a/content/docs/smallrye-fault-tolerance/latest/reference/retry.adoc b/content/docs/smallrye-fault-tolerance/latest/reference/retry.adoc new file mode 100644 index 000000000..0c3bf8dc4 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/latest/reference/retry.adoc @@ -0,0 +1,350 @@ += Retry +:version-root: ../ +include::{version-root}_attributes.adoc[] + +For introduction, see xref:howto/retry.adoc[How to Retry on Failures]. + +== Description + +If a guarded method throws an exception, retry will swallow that exception and call the method again. + +More specifically: + +. If the guarded method returns normally, its return value is returned. +. Otherwise, if the guarded method throws an exception whose type is assignable to any of the `abortOn` types (see <>), retrying is aborted and the exception is rethrown. +. Otherwise, if the guarded method throws an exception whose type is assignable to any of the `retryOn` types (see <>), the method call is retried. +. Otherwise, the exception is rethrown. + +[[interactions]] +== Interactions with Other Strategies + +See xref:howto/multiple.adoc[How to Use Multiple Strategies] for an overview of how fault tolerance strategies are nested. + +If `@Fallback` is used with `@Retry`, the fallback method or handler may be invoked if all retry attempts failed, depending on the fallback configuration. + +[[configuration]] +== Configuration + +There are 9 configuration options, corresponding to the 9 members of the `@Retry` annotation. + +=== `maxRetries` + +Type: `int` + +Default: `3` + +The maximum number of retries. +`-1` means the maximum number of retries is unlimited. + +=== `delay` + `delayUnit` + +Type: `long` + `ChronoUnit` + +Default: `0 millis` + +The delay between retry attempts. + +=== `maxDuration` + `durationUnit` + +Type: `long` + `ChronoUnit` + +Default: `180_000 millis`, or 3 minutes + +The maximum duration for which retries will be attempted. + +:time-value-name: maxDuration +:time-unit-bad-name: durationUnit +:time-unit-good-name: maxDurationUnit +include::{partialsdir}/time-unit-inconsistency.adoc[] + +=== `jitter` + `jitterDelayUnit` + +Type: `long` + `ChronoUnit` + +Default: `200 millis` + +The maximum jitter to adjust the delay between retries. +If set, the delay before each retry attempt will be computed as follows: + +. Generate random number from the closed interval `[-jitter, jitter]`. +. Add the generated random number to the configured `delay`. +. If the result is by any chance negative, use `0`. + +:time-value-name: jitter +:time-unit-bad-name: jitterDelayUnit +:time-unit-good-name: jitterUnit +include::{partialsdir}/time-unit-inconsistency.adoc[] + +[[retry-on]] +=== `retryOn` + +Type: `Class[]` + +Default: `{Exception.class}` + +Set of exception types that trigger a retry. + +[[abort-on]] +=== `abortOn` + +Type: `Class[]` + +Default: `{}` + +Set of exception types that abort retrying. +This configuration takes precedence over `retryOn`. + +[[metrics]] +== Metrics + +Retry exposes the following metrics: + +[cols="1,5"] +|=== +| Name | `ft.retry.calls.total` +| Type +a| * OpenTelemetry: `LongCounter` +* Micrometer: `Counter` +| Unit | None +| Description | The number of times the retry logic was run. This will always be once per method call. +| Tags +a| * `method` - the fully qualified method name +* `retried` = `[true\|false]` - whether any retries occurred +* `retryResult` = `[valueReturned\|exceptionNotRetryable\|maxRetriesReached\|maxDurationReached]` - the reason that last attempt to call the method was not retried +|=== + +[cols="1,5"] +|=== +| Name | `ft.retry.retries.total` +| Type +a| * OpenTelemetry: `LongCounter` +* Micrometer: `Counter` +| Unit | None +| Description | The number of times the method was retried +| Tags +a| * `method` - the fully qualified method name +|=== + +See xref:reference/metrics.adoc[the Metrics reference guide] for general metrics information. + +== Extra Features + +=== Backoff Strategies for `@Retry` + +include::{partialsdir}/srye-feature.adoc[] + +When retrying failed operations, it is often useful to make a delay between retry attempts. +This delay is also called "backoff". +The `@Retry` annotation in {microprofile-fault-tolerance} supports a single backoff strategy: constant. +That is, the delay between all retry attempts is identical (except for a random jitter). + +{smallrye-fault-tolerance} offers 3 annotations to specify a different backoff strategy: + +* `@ExponentialBackoff` +* `@FibonacciBackoff` +* `@CustomBackoff` + +One of these annotations may be present on any program element (method or class) that also has the `@Retry` annotation. +For example: + +[source,java] +---- +package com.example; + +@ApplicationScoped +public class MyService { + @Retry + @ExponentialBackoff + public void hello() { + ... + } +} +---- + +It is an error to add a backoff annotation to a program element that doesn't have `@Retry` (e.g. add `@Retry` on a class and `@ExponentialBackoff` on a method). +It is also an error to add more than one of these annotations to the same program element. + +When any one of these annotations is present, it modifies the behavior specified by the `@Retry` annotation. +The new behavior is as follows: + +For `@ExponentialBackoff`, the delays between retry attempts grow exponentially, using a defined `factor`. +By default, the `factor` is 2, so each delay is 2 * the previous delay. +For example, if the initial delay (specified by `@Retry`) is 1 second, then the second delay is 2 seconds, third delay is 4 seconds, fourth delay is 8 seconds etc. +It is possible to define a `maxDelay`, so that this growth has a limit. + +For `@FibonacciBackoff`, the delays between retry attempts grow per the Fibonacci sequence. +For example, if the initial delay (specified by `@Retry`) is 1 second, then the second delay is 2 seconds, third delay is 3 seconds, fourth delay is 5 seconds etc. +It is possible to define a `maxDelay`, so that this growth has a limit. + +Both `@ExponentialBackoff` and `@FibonacciBackoff` also apply jitter, exactly like plain `@Retry`. + +Also, since `@Retry` has a default `maxDuration` of 3 minutes and default `maxRetries` of 3, both `@ExponentialBackoff` and `@FibonacciBackoff` define a `maxDelay` of 1 minute. +If we redefine `maxRetries` to a much higher value, and the guarded method keeps failing, the delay would eventually become higher than 1 minute. +In that case, it will be limited to 1 minute. +Of course, `maxDelay` can be configured. +If set to `0`, there's no limit, and the delays will grow without bounds. + +For `@CustomBackoff`, computing the delays between retry attempts is delegated to a specified implementation of `CustomBackoffStrategy`. +This is an advanced option. + +For more information about these backoff strategies, see the javadoc of the annotations. + +=== Predicate-Based `@Retry` + +include::{partialsdir}/srye-feature.adoc[] + +By default, an operation is retried only if it fails and the exception is assignable to one of the classes defined in `@Retry.retryOn` (and not assignable to any of the classes defined in `@Retry.abortOn`). +With the `@RetryWhen` annotation, it is possible to instead define a custom predicate for the exception, as well as a predicate for the result. + +The `@RetryWhen` annotation may be present on any program element (method or class) that also has the `@Retry` annotation. +For example: + +[source,java] +---- +package com.example; + +@ApplicationScoped +public class MyService { + @Retry + @RetryWhen( + result = IsNull.class, // <1> + exception = IsRuntimeException.class // <2> + ) + public String hello() { + ... + } + + public static final class IsNull implements Predicate { + @Override + public boolean test(Object o) { + return o == null; + } + } + + public static final class IsRuntimeException implements Predicate { + @Override + public boolean test(Throwable throwable) { + return throwable instanceof RuntimeException; + } + } +} +---- + +<1> When the method returns `null`, it will be retried. +<2> When the method throws a `RuntimeException`, it will be retried. + +All other results are considered expected and are not retried. + +It is an error to specify `@RetryWhen.exception` if `@Retry.retryOn` / `@Retry.abortOn` is specified. +Specifying `@RetryWhen.result` together with `@Retry.retryOn` / `@Retry.abortOn` is possible, although perhaps not the best idea. + +It is an error to add a `@RetryWhen` annotation to a program element that doesn't have `@Retry` (e.g. add `@Retry` on a class and `@RetryWhen` on a method). + +For more information about `@RetryWhen`, see the javadoc of the annotation. + +=== Before Retry Actions + +include::{partialsdir}/srye-feature.adoc[] + +The `@BeforeRetry` annotation supports specifying an action that should be performed before any retry. +The action will _not_ be performed before the initial attempt. + +The `@BeforeRetry` annotation may be present on any program element (method or class) that also has the `@Retry` annotation. +For example: + +[source,java] +---- +package com.example; + +@ApplicationScoped +public class MyService { + @Retry + @BeforeRetry(methodName = "beforeRetry") // <1> + public String hello() { + ... + } + + public void beforeRetry() { // <2> + ... + } +} +---- + +<1> Specifies that before retrying, the `beforeRetry` method should be called. +<2> The method must return `void` and have no parameters. + +Alternatively, it is possible to specify a class that is an implementation of the `BeforeRetryHandler` interface: + +[source,java] +---- +package com.example; + +@ApplicationScoped +public class MyService { + @Retry + @BeforeRetry(MyBeforeRetry.class) // <1> + public String hello() { + ... + } + + public static class MyBeforeRetry implements BeforeRetryHandler { + public void handle(ExecutionContext context) { + ... + } + } +} +---- + +<1> Specifies that the `MyBeforeRetry` handler should be called before retrying. + +It is only possible to specify one option; specifying both `value` and `methodName` leads to a deployment problem. + +Note that the before retry action must fast and non-blocking (i.e., it must not do any IO or long-running computations) and must not throw an exception. + +[[inspecting-exception-cause-chains]] +=== Inspecting Exception Cause Chains + +include::{partialsdir}/srye-feature.adoc[] + +include::{partialsdir}/non-compat.adoc[] + +The `@Retry` annotation can specify that certain exceptions should be treated as failures (`retryOn`) and others as successes (`abortOn`). +The specification limits this to inspecting the actual exception that was thrown. +However, in many cases, exceptions are wrapped and the exception the user wants to decide on is only present in the cause chain. + +For that reason, in the non-compatible mode, if the actual thrown exception isn't known failure or known success, {smallrye-fault-tolerance} inspects the cause chain. +To be specific, in case a `@Retry` method throws an exception, the decision process is: + +1. If the `abortOn` exceptions are not default and the exception is assignable to one of the `abortOn` exceptions, retry is aborted and the exception is rethrown. +2. Otherwise, if the `retryOn` exceptions are not default and the exception is assignable to one of the `retryOn` exceptions, retry is attempted. +3. Otherwise, if the exception is assignable to one of the `abortOn` exceptions or its cause chain contains an exception assignable to one of the `abortOn` exceptions, retry is aborted and the exception is rethrown. +4. Otherwise, if the exception is assignable to one of the `retryOn` exceptions or its cause chain contains an exception assignable to one of the `retryOn` exceptions, retry is attempted. +5. Otherwise, the exception is rethrown. + +For example: + +[source,java] +---- +@Retry(maxRetries = 5, + abortOn = ExpectedOutcomeException.class, // <1> + retryOn = IOException.class) // <2> +public Result doSomething() { + ... +} +---- + +<1> If `doSomething` throws an `ExpectedOutcomeException`, or a `WrapperException` whose cause is `ExpectedOutcomeException`, retry is aborted and the exception is thrown. +<2> If `doSomething` throws an `IOException`, or a `WrapperException` whose cause is `IOException`, retry is attempted. + +[source,java] +---- +@Retry(maxRetries = 5, + abortOn = ExpectedOutcomeException.class) // <1> <2> +public Result doSomething() { + ... +} +---- + +<1> If `doSomething` throws an `ExpectedOutcomeException`, or a `WrapperException` whose cause is `ExpectedOutcomeException`, retry is aborted and the exception is thrown. +<2> There's no `retryOn`, so the 2nd step in the algorithm above is skipped. +This is what turns the `WrapperException` whose cause is `ExpectedOutcomeException` into an aborted retry. diff --git a/content/docs/smallrye-fault-tolerance/latest/reference/reusable.adoc b/content/docs/smallrye-fault-tolerance/latest/reference/reusable.adoc new file mode 100644 index 000000000..9e7d97d04 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/latest/reference/reusable.adoc @@ -0,0 +1,244 @@ += Reusable Fault Tolerance +:version-root: ../ +include::{version-root}_attributes.adoc[] + +include::{partialsdir}/srye-feature.adoc[] + +The declarative, annotation-based API of {microprofile-fault-tolerance} doesn't allow sharing configuration of fault tolerance strategies across multiple classes. +In a single class, the configuration may be shared across all methods by putting the annotations on the class instead of individual methods, but even then, _stateful_ fault tolerance strategies are not shared. +Each method has its own bulkhead, circuit breaker and/or rate limit, which is often not what you want. + +The xref:reference/programmatic-api.adoc[programmatic API] of {smallrye-fault-tolerance} allows using a single `Guard` or `TypedGuard` object to guard multiple disparate actions, which allows reuse and state sharing. +It is possible to use a programmatically constructed `Guard` or `TypedGuard` object declaratively, using the `@ApplyGuard` annotation. + +To be able to do that, we need a bean of type `Guard` (or `TypedGuard`) with the `@Identifier` qualifier: + +[source,java] +---- +@ApplicationScoped +public class PreconfiguredFaultTolerance { + @Produces + @Identifier("my-fault-tolerance") + public static final Guard GUARD = Guard.create() + .withRetry().maxRetries(2).done() + .withTimeout().done() + .build(); +} +---- + +See the xref:reference/programmatic-api.adoc[programmatic API] documentation for more information about creating the `Guard` or `TypedGuard` instance. + +It is customary to create the bean by declaring a `static` producer field, just like in the previous example. + +Once we have that, we can apply `my-fault-tolerance` to any method: + +[source,java] +---- +@ApplicationScoped +public class MyService { + @ApplyGuard("my-fault-tolerance") + public String doSomething() { + ... + } + + @ApplyGuard("my-fault-tolerance") + public CompletionStage doSomethingElse() { + ... + } +} +---- + +== Defining Fallback + +Note that it is not possible to define a fallback on `Guard`, because fallback is tied to the action type. +It is possible to define a fallback on `TypedGuard`, because it can only be used to guard methods with a single return type, equal to the type the `TypedGuard` was created with. + +However, the `@ApplyGuard` annotation pays attention to the `@Fallback` annotation. +If `@Fallback` is defined, it is used both by `Guard` and `TypedGuard` instances, and it overrides the possible fallback defined on the `TypedGuard`. + +== Defining Thread Offload + +Both `Guard` and `TypedGuard` allow enabling or disabling thread offload. +This is ignored by `@ApplyGuard` if the annotated method is asynchronous as determined from the method signature and possible annotations (`@Asynchronous` and `@AsynchronousNonBlocking`). +See xref:reference/asynchronous.adoc[] for more information about how that determination works. + +The thread offload configuration of `Guard` or `TypedGuard` is only honored when the method cannot be determined to be asynchronous, but it still declares an asynchronous return type. + +.Compatible Mode +**** +In the compatible mode, methods are determined to be asynchronous when they are annotated `@Asynchronous` or `@AsynchronousNonBlocking` and they declare an asynchronous return type. +For such methods, the `Guard` / `TypedGuard` thread offload configuration does not apply. + +For methods that declare an asynchronous return type but are not annotated with `@Asynchronous` or `@AsynchronousNonBlocking`, the configuration on `Guard` / `TypedGuard` is honored. +**** + +.Non-compatible Mode +**** +In the non-compatible mode, methods are determined to be asynchronous whenever they declare an asynchronous return type. +The `@Asynchronous` and `@AsynchronousNonBlocking` only affect whether the invocation of that method is offloaded to an extra thread. +If none of these annotations is present, no thread offload happens. + +In other words, in non-compatible mode, the `Guard` / `TypedGuard` thread offload configuration never applies. + +See xref:reference/non-compat.adoc[] for more information about the non-compatible mode. +**** + +== Configuration + +Even though the programmatic API of `Guard` and `TypedGuard` does not support configuration, the declarative API of `@ApplyGuard` does. + +The configuration closely resembles xref:reference/config.adoc[the configuration of the declarative API], where instead of the `` or `/`, you use the identifier that's used in the `@Identifier` qualifier and in the `@ApplyGuard` annotation. + +CAUTION: When configuring the `Guard` or `TypedGuard` using the following configuration properties, it is recommended to never use the `[Typed]Guard` programmatically. +The reason is that configuration is applied on the first invocation, and if that invocation is programmatic, the configuration key is unknown. + +For example, let's assume the following preconfigured `Guard` object: + +[source,java] +---- +@ApplicationScoped +public class PreconfiguredFaultTolerance { + @Produces + @Identifier("my-fault-tolerance") + public static final Guard GUARD = Guard.create() + .withRetry().maxRetries(2).done() + .withTimeout().done() + .build(); +} +---- + +This `Guard` supports retry and timeout; any attempt to configure other strategies will be ignored. + +To reconfigure the number of maximum retries, the following configuration properties may be used: + +[source,properties] +---- +smallrye.faulttolerance."my-fault-tolerance".retry.max-retries=5 + +# alternatively, a specification-defined property can be used +my-fault-tolerance/Retry/maxRetries=5 +---- + +Global configuration that applies to all usages of retry is also respected by `@ApplyGuard`: + +[source,properties] +---- +smallrye.faulttolerance.global.retry.max-retries=5 + +# alternatively, a specification-defined property can be used +Retry/maxRetries=5 +---- + +Note that the configuration keys follow the configuration of the declarative API, even if the methods in the programmatic API builders are named differently. +For example, to set the rate limit in the programmatic API, the `RateLimitBuilder` uses a method called `limit()`, while the corresponding annotation member of `@RateLimit` is named `value()`. +The configuration key in both cases is `rate-limit.value` (or `RateLimit/value`). + +=== Disabling Fault Tolerance + +Configuration properties for disabling individual strategies also work: + +[source,properties] +---- +smallrye.faulttolerance."my-fault-tolerance".retry.enabled=false + +# alternatively, a specification-defined property can be used +my-fault-tolerance/Retry/enabled=false +---- + +Or globally: + +[source,properties] +---- +smallrye.faulttolerance.global.retry.enabled=false + +# alternatively, a specification-defined property can be used +Retry/enabled=false +---- + +=== Supported Strategies + +The following fault tolerance strategies, when defined on a `Guard` (or `TypedGuard`) and used through `@ApplyGuard`, may be configured: + +* bulkhead (except of `BulkheadBuilder.enableSynchronousQueueing()`) +* circuit breaker +* rate limit +* retry, including exponential backoff and Fibonacci backoff +* timeout + +In the programmatic API, the following strategies are not built declaratively, and so their configuration is ignored: + +* fallback +* circuit breaker name (cannot be configured in the declarative API either) +* certain parts of retry: custom backoff, before retry action and retry predicates +* thread offload + +== Metrics + +Methods annotated `@ApplyGuard` gather metrics similarly to methods annotated with {microprofile-fault-tolerance} annotations. +That is, each method gets its own metrics, with the `method` tag being `.`. + +At the same time, state is still shared. +All methods annotated `@ApplyGuard` share the same bulkhead, circuit breaker and/or rate limit. + +If the `Guard` or `TypedGuard` object used for `@ApplyGuard` is also used xref:reference/programmatic-api.adoc[programmatically], that usage is coalesced in metrics (where the `method` tag is set to the description). + +== Differences to the Specification + +`@ApplyGuard` has the same differences to standard MicroProfile Fault Tolerance as `Guard` / `TypedGuard`: + +* asynchronous actions of type `java.util.concurrent.Future` are not supported; +* the fallback, circuit breaker and retry strategies always inspect the cause chain of exceptions, following the behavior of SmallRye Fault Tolerance in the non-compatible mode. + +== Kotlin `suspend` Functions + +Even though the programmatic API of `Guard` and `TypedGuard` does not support Kotlin `suspend` functions, the declarative API of `@ApplyGuard` does. +When the guard is a `Guard`, no restrictions apply. + +When the guard is a `TypedGuard`, however, its type must be a synchronous return type of the `suspend` function. +For example, when the `suspend` function is declared to return a `String` asynchronously: + +[source,kotlin] +---- +@ApplyGuard("my-fault-tolerance") +@Fallback(fallbackMethod = "fallback") +suspend fun hello(): String { + delay(100) + throw IllegalArgumentException() +} +---- + +The `TypedGuard` must be declared to guard actions of type `String`: + +[source,kotlin] +---- +@Produces +@Identifier("my-fault-tolerance") +val GUARD = TypedGuard.create(String::class.java) + .withRetry().maxRetries(2).done() + .withFallback().handler(Supplier { "fallback" }).done() + .build() +---- + +This means that a possible fallback declared on the `TypedGuard` must be synchronous; it cannot be a `suspend` lambda. + +The `@Fallback` method, if declared, must have a matching signature and so must be a `suspend` function: + +[source,kotlin] +---- +suspend fun fallback(): String { + delay(100) + return "fallback" +} +---- + +[[migration_from_applyfaulttolerance]] +== Migration from `@ApplyFaultTolerance` + +The 1st version of the programmatic API had the `@ApplyFaultTolerance` annotation. +That annotation was removed in {smallrye-fault-tolerance} 7.0. + +To migrate, replace `@ApplyFaultTolerance` with `@ApplyGuard` and change the `FaultTolerance<>` producers to produce `Guard` or `TypedGuard<>`. +See xref:reference/programmatic-api.adoc#migration_from_faulttolerance[the programmatic API migration guide] for more details about that. + +Note that it is not possible to define both `Guard` and `TypedGuard<>` with the same identifier; that leads to a deployment problem. +Therefore, for each producer of `FaultTolerance<>`, you have to decide whether the replacement should be `Guard` or `TypedGuard<>`. diff --git a/content/docs/smallrye-fault-tolerance/latest/reference/timeout.adoc b/content/docs/smallrye-fault-tolerance/latest/reference/timeout.adoc new file mode 100644 index 000000000..183009bd7 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/latest/reference/timeout.adoc @@ -0,0 +1,78 @@ += Timeout +:version-root: ../ +include::{version-root}_attributes.adoc[] + +For introduction, see xref:howto/timeout.adoc[How to Watch for Timeouts]. + +== Description + +If a guarded method takes too long, the caller will get a `TimeoutException`. + +If the guarded method is not `@Asynchronous` or `@AsynchronousNonBlocking`, the thread executing the method is interrupted. +Note that interruption requires cooperation from the executing code (for example, it must not swallow `InterruptedException`). +In other words, it is not guaranteed that interrupting the method will actually make it stop. + +If the guarded method is `@Asynchronous` or `@AsynchronousNonBlocking`, timeout is always signalled correctly, but there is no cancellation. +Even after timeout, the asynchronous action will keep running; its eventual result is just ignored. + +[[interactions]] +== Interactions with Other Strategies + +See xref:howto/multiple.adoc[How to Use Multiple Strategies] for an overview of how fault tolerance strategies are nested. + +If `@Fallback` is used with `@Timeout`, the fallback method or handler may be invoked if a `TimeoutException` is thrown, depending on the fallback configuration. + +If `@Retry` is used with `@Timeout`, each retry attempt is processed by the timeout as an independent invocation. +If `TimeoutException` is thrown, the execution may be retried, depending on how retry is configured. + +If `@CircuitBreaker` is used with `@Timeout`, the circuit breaker is checked before enforcing the timeout. +If enforcing the timeout results in `TimeoutException`, this may be counted as a failure, depending on how the circuit breaker is configured. + +If `@RateLimit` is used with `@Timeout`, the rate limit is enforced before enforcing the timeout. + +[[configuration]] +== Configuration + +There are 2 configuration options, corresponding to the 2 members of the `@Timeout` annotation. + +=== `value` + `unit` + +Type: `long` + `ChronoUnit` + +Default: `1000 millis`, or 1 second + +The timeout to enforce. + +[[metrics]] +== Metrics + +Timeout exposes the following metrics: + +[cols="1,5"] +|=== +| Name | `ft.timeout.calls.total` +| Type +a| * OpenTelemetry: `LongCounter` +* Micrometer: `Counter` +| Unit | None +| Description | The number of times the timeout logic was run. This is usually once per method call, but may be zero times if the circuit breaker or rate limit prevents execution or more than once if the method is retried. +| Tags +a| * `method` - the fully qualified method name +* `timedOut` = `[true\|false]` - whether the method call timed out +|=== + +[cols="1,5"] +|=== +| Name | `ft.timeout.executionDuration` +| Type +a| * OpenTelemetry: `DoubleHistogram` with explicit bucket boundaries `[0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10]` +* Micrometer: `Timer` +| Unit +a| * OpenTelemetry: seconds +* Micrometer: nanoseconds +| Description | Histogram of execution times for the method +| Tags +a| * `method` - the fully qualified method name +|=== + +See xref:reference/metrics.adoc[the Metrics reference guide] for general metrics information. diff --git a/content/docs/smallrye-fault-tolerance/main/_attributes.adoc b/content/docs/smallrye-fault-tolerance/main/_attributes.adoc new file mode 100644 index 000000000..3dacb848d --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/main/_attributes.adoc @@ -0,0 +1,24 @@ +:project-version: 7.0.0 +// Attributes shared by every page. smallrye.io overrides the ones it needs when it publishes. +// Pages in subdirectories set :version-root: (../ per level) before including this file. +ifndef::version-root[:version-root:] +:idprefix: +:idseparator: - +:sectanchors: +:partialsdir: {version-root}_partials +:examples-dir: {version-root}_examples +:smallrye-fault-tolerance: SmallRye Fault Tolerance +:smallrye-fault-tolerance-version: 7.0.0 +:microprofile-fault-tolerance: MicroProfile Fault Tolerance +:microprofile-fault-tolerance-version: 4.1.2 +:microprofile-fault-tolerance-url: https://download.eclipse.org/microprofile/microprofile-fault-tolerance-4.1/microprofile-fault-tolerance-spec-4.1.html +:vertx4-version: 4.5.25 +:generated-dir: {version-root}_generated + +// Added by smallrye.io (tools/docs/Docs.java sync) +:javadoc-version: 7.0.0 +:relfileprefix: /docs/smallrye-fault-tolerance/main/ +:imagesdir: /docs/smallrye-fault-tolerance/main +:docs-project: smallrye-fault-tolerance +:docs-version: main +:docs-ref: main diff --git a/content/docs/smallrye-fault-tolerance/main/_nav.yml b/content/docs/smallrye-fault-tolerance/main/_nav.yml new file mode 100644 index 000000000..1c25b6497 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/main/_nav.yml @@ -0,0 +1,80 @@ +title: SmallRye Fault Tolerance +nav: +- title: Overview + page: index +- title: How-to + items: + - title: Begin + page: howto/begin + - title: Supply Fallback Values + page: howto/fallback + - title: Limit Concurrency + page: howto/bulkhead + - title: Fail Fast on Recurring Failures + page: howto/circuit-breaker + - title: Watch for Timeouts + page: howto/timeout + - title: Retry on Failures + page: howto/retry + - title: Guard Asynchronous Methods + page: howto/asynchronous + - title: Limit Execution Rate + page: howto/rate-limit + - title: Use Multiple Strategies + page: howto/multiple +- title: Reference + items: + - title: Fallback + page: reference/fallback + - title: Bulkhead + page: reference/bulkhead + - title: Circuit Breaker + page: reference/circuit-breaker + - title: Timeout + page: reference/timeout + - title: Retry + page: reference/retry + - title: Asynchronous Execution + page: reference/asynchronous + - title: Rate Limit + page: reference/rate-limit + - title: Configuration + page: reference/config + - title: Metrics + page: reference/metrics + - title: Non-compatible Mode + page: reference/non-compat + - title: Programmatic API + page: reference/programmatic-api + - title: Reusable Fault Tolerance + page: reference/reusable +- title: Integration + items: + - title: Introduction + page: integration/intro + - title: Metrics + page: integration/metrics + - title: Thread Pool + page: integration/thread-pool + - title: Context Propagation + page: integration/context-propagation + - title: Event Loop + page: integration/event-loop + - title: Additional Asynchronous Types Integration Concerns + page: integration/async-types + - title: Kotlin Integration Concerns + page: integration/kotlin + - title: Programmatic API Integration Concerns + page: integration/programmatic-api +- title: Internals + items: + - title: Project Structure + page: internals/project-structure + - title: Instructions + page: internals/instructions + - title: Fault Tolerance Core + page: internals/core + - title: Configuration System + page: internals/config + - title: Logging + page: internals/logging diff --git a/content/docs/smallrye-fault-tolerance/main/_partials/internals.adoc b/content/docs/smallrye-fault-tolerance/main/_partials/internals.adoc new file mode 100644 index 000000000..6a5d9340f --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/main/_partials/internals.adoc @@ -0,0 +1,3 @@ +WARNING: Note that everything explained here is an implementation detail. +API stability for these interfaces and classes is _not_ guaranteed! +This documentation is only present to provide implementation insight for people who have to debug issues or want to contribute. diff --git a/content/docs/smallrye-fault-tolerance/main/_partials/non-compat.adoc b/content/docs/smallrye-fault-tolerance/main/_partials/non-compat.adoc new file mode 100644 index 000000000..e087cdb10 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/main/_partials/non-compat.adoc @@ -0,0 +1 @@ +IMPORTANT: This feature only works in the xref:reference/non-compat.adoc[non-compatible mode]. diff --git a/content/docs/smallrye-fault-tolerance/main/_partials/srye-feature.adoc b/content/docs/smallrye-fault-tolerance/main/_partials/srye-feature.adoc new file mode 100644 index 000000000..0f4a979cf --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/main/_partials/srye-feature.adoc @@ -0,0 +1 @@ +IMPORTANT: This is an additional feature of {smallrye-fault-tolerance} and is not specified by {microprofile-fault-tolerance}. diff --git a/content/docs/smallrye-fault-tolerance/main/_partials/time-unit-inconsistency.adoc b/content/docs/smallrye-fault-tolerance/main/_partials/time-unit-inconsistency.adoc new file mode 100644 index 000000000..b742c482b --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/main/_partials/time-unit-inconsistency.adoc @@ -0,0 +1,3 @@ +NOTE: Note the naming inconsistency between `{time-value-name}` and `{time-unit-bad-name}`. +It is common for the time unit property to be named after the time value property, e.g. `{time-unit-good-name}`. +Fixing this inconsistency would be a breaking change the {microprofile-fault-tolerance} specification is not willing to make. diff --git a/content/docs/smallrye-fault-tolerance/main/howto/asynchronous.adoc b/content/docs/smallrye-fault-tolerance/main/howto/asynchronous.adoc new file mode 100644 index 000000000..4c8394666 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/main/howto/asynchronous.adoc @@ -0,0 +1,102 @@ += How to Guard Asynchronous Methods +:version-root: ../ +include::{version-root}_attributes.adoc[] + +{smallrye-fault-tolerance} allows offloading method calls to another thread, as well as applying asynchronous fault tolerance. + +Asynchronous fault tolerance means guarding not just the synchronous method execution, but the entire asynchronous operation started by the method call, until the result is delivered. +For example: + +[source,java] +---- +CompletionStage doSomething() { + return CompletableFuture.supplyAsync(() -> { + if (someCondition) { + return someValue; + } else { + throw someException; + } + }); +} +---- + +This method always returns a `CompletionStage` and it never throws an exception. +However, if `someCondition` does not hold, the `CompletionStage` returned from the method will complete exceptionally. + +Ordinary (non-asynchronous) fault tolerance: + +* never considers this operation failing, because the method returns a value and does not throw an exception; +* considers this operation finished when the method returns. + +Asynchronous fault tolerance, on the other hand: + +* defers its decision process to the moment when the returned `CompletionStage` completes, +* and considers this operation running until that happens. + +As a special case, if an asynchronous method in fact does throw an exception (it typically shouldn't), it is treated as if it returned a `CompletionStage` that is already completed exceptionally with the thrown exception. + +== `@Asynchronous` + +A method that returns `java.util.concurrent.Future` or `java.util.concurrent.CompletionStage` may be annotated `@Asynchronous`. +Such method is executed on another thread. +Additionally, if the return type is `CompletionStage`, asynchronous fault tolerance applies. + +[NOTE] +==== +Note that if an `@Asynchronous` method returns a `Future`, asynchronous fault tolerance does _not_ apply. +Only the synchronous method execution is guarded. + +Asynchronous fault tolerance only applies if the `@Asynchronous` method returns `CompletionStage`. +==== + +Use `@Asynchronous` if the method has blocking logic, but you don’t want to block the caller thread. +For example: + +[source,java] +---- +@ApplicationScoped +public class MyService { + @Retry + @Asynchronous // <1> + CompletionStage hello() { + ... blocking IO here ... + } +} +---- + +<1> Using the `@Asynchronous` annotation, because the method blocks, and it is necessary to offload its execution to another thread. + +== `@AsynchronousNonBlocking` + +include::{partialsdir}/srye-feature.adoc[] + +A method that returns `CompletionStage` may be annotated `@AsynchronousNonBlocking`. +In such case, the method is executed on the original thread, but asynchronous fault tolerance still applies. + +Use `@AsynchronousNonBlocking` if the method _does not_ have blocking logic, and you want the execution to stay on the caller thread. +For example: + +[source,java] +---- +@ApplicationScoped +public class MyService { + @Retry + @AsynchronousNonBlocking // <1> + CompletionStage hello() { + ... non-blocking IO here ... + } +} +---- + +<1> Using the `@AsynchronousNonBlocking` annotation, because the method does not block and offloading execution to another thread is not necessary. + +=== Non-compatible mode + +include::{partialsdir}/non-compat.adoc[] + +In the non-compatible mode, the `@AsynchronousNonBlocking` annotation is not necessary. +A method that returns `CompletionStage` and is not annotated `@Asynchronous` is automatically treated as `@AsynchronousNonBlocking`. + +== Links + +For more information, see xref:reference/asynchronous.adoc[the Asynchronous Execution reference guide]. diff --git a/content/docs/smallrye-fault-tolerance/main/howto/begin.adoc b/content/docs/smallrye-fault-tolerance/main/howto/begin.adoc new file mode 100644 index 000000000..3b5f0f3e7 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/main/howto/begin.adoc @@ -0,0 +1,129 @@ += How to Begin +:version-root: ../ +include::{version-root}_attributes.adoc[] + +== Installation + +As an application developer, you don't have to "install" {smallrye-fault-tolerance} in any way. +Your chosen runtime, such as Quarkus or WildFly, already integrates {smallrye-fault-tolerance} to provide {microprofile-fault-tolerance}. +Please refer to the documentation of your selected runtime to understand how to enable MicroProfile Fault Tolerance. + +.Quarkus +**** +In case of Quarkus, it is enough to add a dependency on `io.quarkus:quarkus-smallrye-fault-tolerance`. + +See the https://quarkus.io/guides/smallrye-fault-tolerance[{smallrye-fault-tolerance} guide] in Quarkus documentation for more. +**** + +.WildFly +**** +In case of WildFly, the {microprofile-fault-tolerance} subsystem must be enabled. +It is not included in the default `standalone` or `standalone-full` profiles, but it is included in the `standalone-microprofile` profile. +It is easiest to just start WildFly in this profile. + +See the https://docs.wildfly.org/27/Admin_Guide.html#MicroProfile-Fault-Tolerance-SmallRye[{microprofile-fault-tolerance} chapter] in WildFly Admin Guide for more. +**** + +If you are a framework developer, interested in integrating {smallrye-fault-tolerance} in your framework or runtime, please refer to the xref:integration/intro.adoc[Integration] section. + +== Usage + +{microprofile-fault-tolerance} is built on top of CDI and interceptors. +This lets you guard method calls with selected fault tolerance strategies simply by annotating a CDI bean class or method with one of the fault tolerance annotations. +For example: + +[source,java] +---- +@ApplicationScoped // <1> +public class MyService { + @Retry // <2> + public String hello() { + ... + } +} +---- + +<1> `@ApplicationScoped` is a bean defining annotation. +It makes the `MyService` class a CDI bean. +<2> `@Retry` is a fault tolerance annotation. + +Here, if the `hello` method throws an exception, the invocation will be retried several times, until the method returns a value. + +The annotation may also be present on a class, in which case it applies to all business method in the class: + +[source,java] +---- +package com.example; + +@ApplicationScoped +@Retry // <1> +public class MyService { + public String hello() { // <2> + ... + } +} +---- +<1> `@Retry` is a fault tolerance annotation. +<2> The `hello()` method will use the retry strategy, because it is defined on the class. + +A class-level annotation may be overridden by explicitly annotating a method. + +TIP: CDI beginners often expect that this will work if you create an instance manually: `new MyService()`. +That is not the case. +You have to let the CDI container construct an instance for you and inject it: `@Inject MyService service`. +What you get injected is actually a proxy that implements the additional behaviors. + +The fault tolerance strategies present in {microprofile-fault-tolerance}, together with the corresponding annotations, are: + +* `@Asynchronous`: offload method execution to another thread +* `@Bulkhead`: limit concurrent invocations +* `@CircuitBreaker`: prevent invocations if previous invocations failed too often +* `@Fallback`: provide alternative result in case of a failure +* `@Retry`: retry several times in case of a failure +* `@Timeout`: fail if the invocation takes too long + +{smallrye-fault-tolerance} adds some more fault tolerance strategies and annotations: + +* `@ApplyGuard`: apply a programmatically-defined guard +* `@AsynchronousNonBlocking`: apply asynchronous fault tolerance without offloading method execution to another thread +* `@RateLimit`: limit rate of method invocations + +These are all described in the corresponding how-to and reference guides. +The {microprofile-fault-tolerance} features are also described in the {microprofile-fault-tolerance-url}[specification]. + +== Configuration + +The fault tolerance strategies can be configured using annotations, such as: + +[source,java] +---- +@ApplicationScoped +public class MyService { + @Retry(maxRetries = 10, retryOn = IOException.class) // <1> + public String hello() { + ... + } +} +---- + +<1> Retries will only be attempted if the thrown exception was `IOException`. +Other exceptions will be rethrown directly. +Also, the maximum number of retry attempts is changed to 10 (the default is 3). + +This is convenient, but changing such configuration requires recompilation. +For that reason, configuration using MicroProfile Config is also possible. +For example: + +[source,properties] +---- +smallrye.faulttolerance."com.example.MyService/hello".retry.max-retries=5 <1> + +# alternatively, a specification-defined property can be used +com.example.MyService/hello/Retry/maxRetries=5 <1> +---- +<1> Even though the `@Retry` annotation says that `maxRetries` should be 10, this configuration takes precedence. +The maximum number of retries will be 5. + +Since this example configuration does not modify the `retryOn` setting, the configuration provided in the annotation still applies. + +For more information about configuration, see xref:reference/config.adoc[the Configuration reference guide]. diff --git a/content/docs/smallrye-fault-tolerance/main/howto/bulkhead.adoc b/content/docs/smallrye-fault-tolerance/main/howto/bulkhead.adoc new file mode 100644 index 000000000..39326779c --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/main/howto/bulkhead.adoc @@ -0,0 +1,33 @@ += How to Limit Concurrency +:version-root: ../ +include::{version-root}_attributes.adoc[] + +It is possible to prevent an operation from having too many concurrent executions using a _bulkhead_. + +NOTE: In general, the term _bulkhead_ refers to system compartmentalization, or the practice of preventing failures in one part of the system from spreading to other parts of the system. +In {microprofile-fault-tolerance}, the term refers to one specific instance of the general idea: a _concurrency limiter_. + +== `@Bulkhead` + +The `@Bulkhead` annotation specifies that a concurrency limit should be enforced for all calls of the method. + +[source,java] +---- +@ApplicationScoped +public class MyService { + @Bulkhead // <1> + public String hello() { + ... + } +} +---- +<1> Declares that `hello()` must not have too many concurrent executions. +Since there is no configuration, the default is at most 10 concurrent calls. + +If some callers attempt to invoke the method when the concurrency limit has already been reached, they will fail with `BulkheadException`. + +It is possible to specify the maximum allowed concurrency, as well as define a queue for excess invocations in case of xref:howto/asynchronous.adoc[asynchronous methods]. + +== Links + +For more information, see xref:reference/bulkhead.adoc[the Bulkhead reference guide]. diff --git a/content/docs/smallrye-fault-tolerance/main/howto/circuit-breaker.adoc b/content/docs/smallrye-fault-tolerance/main/howto/circuit-breaker.adoc new file mode 100644 index 000000000..0dcd618ec --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/main/howto/circuit-breaker.adoc @@ -0,0 +1,33 @@ += How to Fail Fast on Recurring Failures +:version-root: ../ +include::{version-root}_attributes.adoc[] + +If an operation fails too often, it is possible to prevent executing that operation and fail fast using a _circuit breaker_. + +The circuit breaker starts _closed_, in which state it allows all invocations to proceed. +If too many invocations fail, the circuit breaker moves to _open_, in which state it prevents all invocations and fails fast. +After some time in _open_, the circuit breaker moves to _half-open_, in which state it determines whether it should continue failing fast (and move back to _open_) or allow invocations again (and move to _closed_). + +== `@CircuitBreaker` + +The `@CircuitBreaker` annotation specifies that method invocations should be prevented if the method failed too often recently. + +[source,java] +---- +@ApplicationScoped +public class MyService { + @CircuitBreaker // <1> + public String hello() { + ... + } +} +---- +<1> Declares that `hello()` must be guarded with a circuit breaker. + +If some callers attempt to invoke the method when the circuit breaker is _open_, they will fail with `CircuitBreakerOpenException`. + +It is possible to configure the number of recent invocations the circuit breaker should track, the failure rate, and so on. + +== Links + +For more information, see xref:reference/circuit-breaker.adoc[the Circuit Breaker reference guide]. diff --git a/content/docs/smallrye-fault-tolerance/main/howto/fallback.adoc b/content/docs/smallrye-fault-tolerance/main/howto/fallback.adoc new file mode 100644 index 000000000..a0fb8826d --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/main/howto/fallback.adoc @@ -0,0 +1,62 @@ += How to Supply Fallback Values +:version-root: ../ +include::{version-root}_attributes.adoc[] + +If an operation fails, it is often possible to replace the failure with an alternative result: a _fallback_. + +== `@Fallback` + +=== Using a Fallback Method + +The `@Fallback` annotation allows specifying a method that should be called in case an exception is thrown. +The fallback method must have the same parameter types as the guarded method, and must return a type that is assignable to the guarded method's return type. + +[source,java] +---- +@ApplicationScoped +public class MyService { + @Fallback(fallbackMethod = "fallback") // <1> + public String hello() { + ... + } + + private String fallback() { + return "fallback"; + } +} +---- +<1> Declares that if `hello()` throws an exception, the `fallback()` method is called and its result is returned instead. + +In this particular case, if `hello()` throws an exception, that exception is swallowed and the string `"fallback"` is returned. +It is possible to specify that fallback should only apply to certain exception types or that it should be skipped for certain exception types. + +=== Using a Fallback Handler + +Alternatively, the `@Fallback` annotation allows specifying a fallback handler that should be called. + +[source,java] +---- +@ApplicationScoped +public class MyService { + @Fallback(MyFallback.class) // <1> + public String hello() { + ... + } +} +---- +<1> Declares that if `hello()` throws an exception, the `MyFallback` handler is used to obtain a fallback value. + +[source,java] +---- +public class MyFallback implements FallbackHandler { // <1> + String handle(ExecutionContext context) { // <2> + return "fallback"; + } +} +---- +<1> A fallback handler must implement the `FallbackHandler` interface. +<2> The `ExecutionContext` provides access to the guarded method, its parameter values, and the thrown exception. + +== Links + +For more information, see xref:reference/fallback.adoc[the Fallback reference guide]. diff --git a/content/docs/smallrye-fault-tolerance/main/howto/multiple.adoc b/content/docs/smallrye-fault-tolerance/main/howto/multiple.adoc new file mode 100644 index 000000000..7ee19a470 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/main/howto/multiple.adoc @@ -0,0 +1,58 @@ += How to Use Multiple Strategies +:version-root: ../ +include::{version-root}_attributes.adoc[] + +It is possible to use multiple fault tolerance strategies to guard a single method. +For example, it is possible to use fallback together with retry and timeout: + +[source,java] +---- +@ApplicationScoped +public class MyService { + @Retry(maxRetries = 5) + @Fallback(fallbackMethod = "fallback") + @Timeout + public String hello() { + ... + } + + public String fallback() { + return "fallback"; + } +} +---- + +In such case, the fault tolerance strategies are nested in a pre-defined order: + +[source] +---- +Fallback( + Retry( + CircuitBreaker( + RateLimit( + Timeout( + Bulkhead( + ... the guarded method ... + ) + ) + ) + ) + ) +) +---- + +In the previous example, this means that timeout is nested inside retry, which in turn is nested inside fallback. +If the method times out (the default timeout is 1 second), it is retried (up to 5 times). +If no attempt succeeds, fallback is used. + +== Links + +Reference guides for all fault tolerance strategies explain in detail how they interact with one another: + +* xref:reference/asynchronous.adoc#interactions[@Asynchronous and @AsynchronousNonBlocking] +* xref:reference/bulkhead.adoc#interactions[@Bulkhead] +* xref:reference/circuit-breaker.adoc#interactions[@CircuitBreaker] +* xref:reference/fallback.adoc#interactions[@Fallback] +* xref:reference/rate-limit.adoc#interactions[@RateLimit] +* xref:reference/retry.adoc#interactions[@Retry] +* xref:reference/timeout.adoc#interactions[@Timeout] diff --git a/content/docs/smallrye-fault-tolerance/main/howto/rate-limit.adoc b/content/docs/smallrye-fault-tolerance/main/howto/rate-limit.adoc new file mode 100644 index 000000000..c818bfdfe --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/main/howto/rate-limit.adoc @@ -0,0 +1,33 @@ += How to Limit Execution Rate +:version-root: ../ +include::{version-root}_attributes.adoc[] + +include::{partialsdir}/srye-feature.adoc[] + +It is possible to prevent an operation from being executed too often using a _rate limit_. + +== `@RateLimit` + +The `@RateLimit` annotation specifies that a rate limit should be enforced for all method calls. + +[source,java] +---- +@ApplicationScoped +public class MyService { + @RateLimit // <1> + public String hello() { + ... + } +} +---- +<1> Declares that `hello()` must not be called too often. +Since there is no configuration, the default is at most 100 calls per 1 minute. + +If the method is called too often, excess attempts will fail with `RateLimitException`. + +It is possible to specify the maximum allowed rate of execution (the rate limit), together with minimum spacing between invocations. +There are also several types of rate limiting to choose from. + +== Links + +For more information, see xref:reference/rate-limit.adoc[the Rate Limit reference guide]. diff --git a/content/docs/smallrye-fault-tolerance/main/howto/retry.adoc b/content/docs/smallrye-fault-tolerance/main/howto/retry.adoc new file mode 100644 index 000000000..865403c37 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/main/howto/retry.adoc @@ -0,0 +1,33 @@ += How to Retry on Failures +:version-root: ../ +include::{version-root}_attributes.adoc[] + +If an operation fails, it is possible to _retry_ the operation in case the failure was just a temporary glitch. + +CAUTION: In case an operation fails due to the target system being overloaded, retrying will likely make the overload situation even worse. +Use sparingly, preferably with a xref:howto/bulkhead.adoc[concurrency limit] or a xref:howto/circuit-breaker.adoc[circuit breaker]. + +== `@Retry` + +The `@Retry` annotation specifies that a method should be called again in case a previous execution thrown an exception. + +[source,java] +---- +@ApplicationScoped +public class MyService { + @Retry // <1> + public String hello() { + ... + } +} +---- +<1> Declares that if `hello()` throws an exception, the exception is swallowed and `hello()` is called again. +Since there is no configuration, at most 3 retry attempts will be done before rethrowing the exception. + +It is possible to specify the maximum number of retry attempts, as well as the maximum duration for which retries should be attempted. +Further, it is possible to specify some delay between retry attempts, together with random jitter. +Finally, it is possible to specify that calls should be retried only for certain exception types or that retrying should be aborted for certain exception types. + +== Links + +For more information, see xref:reference/retry.adoc[the Retry reference guide]. diff --git a/content/docs/smallrye-fault-tolerance/main/howto/timeout.adoc b/content/docs/smallrye-fault-tolerance/main/howto/timeout.adoc new file mode 100644 index 000000000..c3e9fdf8b --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/main/howto/timeout.adoc @@ -0,0 +1,28 @@ += How to Watch for Timeouts +:version-root: ../ +include::{version-root}_attributes.adoc[] + +It is possible to prevent an operation from taking too long using a _timeout_. + +== `@Timeout` + +The `@Timeout` annotation specifies that all calls of the method should fail if they take too long. + +[source,java] +---- +@ApplicationScoped +public class MyService { + @Timeout // <1> + public String hello() { + ... + } +} +---- +<1> Declares that `hello()` must not take too long. +Since there is no configuration, the default is at most 1 second. + +If the method call takes too long, the caller will get a `TimeoutException` instead. + +== Links + +For more information, see xref:reference/timeout.adoc[the Timeout reference guide]. diff --git a/content/docs/smallrye-fault-tolerance/main/images/diagrams/mermaid-f40d3cbcabca.svg b/content/docs/smallrye-fault-tolerance/main/images/diagrams/mermaid-f40d3cbcabca.svg new file mode 100644 index 000000000..a31aed359 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/main/images/diagrams/mermaid-f40d3cbcabca.svg @@ -0,0 +1 @@ +

Recent invocations failed too often

Configured time elapsed

All probe invocations succeeded

Some probe invocations failed

[CLOSED] Allow invocations and track the number of successes and failures

[OPEN] Fail fast, prevent all invocations

[HALF-OPEN] Allow some probe invocations to proceed

\ No newline at end of file diff --git a/content/docs/smallrye-fault-tolerance/main/index.adoc b/content/docs/smallrye-fault-tolerance/main/index.adoc new file mode 100644 index 000000000..c8eccf252 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/main/index.adoc @@ -0,0 +1,77 @@ += SmallRye Fault Tolerance Documentation +include::_attributes.adoc[] + +{smallrye-fault-tolerance} {smallrye-fault-tolerance-version} is an implementation of https://github.com/eclipse/microprofile-fault-tolerance/[Eclipse {microprofile-fault-tolerance}] {microprofile-fault-tolerance-version} with additional features not defined by the specification. + +It provides a declarative, annotation-based API for guarding method calls: + +[source,java] +---- +@ApplicationScoped +public class MyService { + @Retry // <1> + public String hello() { + ... + } +} +---- +<1> If `hello()` throws an exception, the invocation will be retried several times, until the method returns a value. + +It also provides a programmatic API to build reusable guards: + +[source,java] +---- +public class MyService { + private static final TypedGuard GUARD = TypedGuard.create(String.class) + .withFallback().handler(() -> "fallback").done() // <1> + .build(); + + public String hello() throws Exception { + return GUARD.call(() -> externalService.hello()); // <2> + } +} +---- +<1> If the guarded call throws an exception, the fallback value will be returned instead. +<2> The guarded call simply invokes some external service. + +CAUTION: The fault tolerance strategies provided by {smallrye-fault-tolerance} are best suited for guarding network interactions. +To handle ordinary exceptions, it is typically best to use the plain old `try` / `catch` / `finally` blocks. +Using {smallrye-fault-tolerance} as a replacement of exception handling is often a bad idea. + +NOTE: Some additional features may have experimental status, marked by the `@Experimental` annotation. +The programmatic API to build reusable guards, as mentioned above, is one example, but there are more. + +== Runtimes + +Runtimes that currently use {smallrye-fault-tolerance} include: + +* https://quarkus.io/[Quarkus] +* https://wildfly.org/[WildFly] + +== Navigation + +This documentation site has several sections, as you can see in the menu. + +=== How-to Guides + +How-tos are task-oriented guides that let you quickly start using some feature. +They provide a short overview, but not many details. + +TIP: If you're just starting with {smallrye-fault-tolerance}, start with the how-tos. + +=== References + +References are detailed guides that let you understand some feature in depth. +They also provide information about more advanced options or features. + +TIP: If the how-to guide doesn't answer a question you have, the reference guide should. + +=== Integration + +The articles here address concerns that runtimes integrating {smallrye-fault-tolerance} might have. +Useful for people who maintain {smallrye-fault-tolerance} integration into some bigger framework or platform, but not required for application developers. + +=== Internals + +A few random articles that describe some parts of the {smallrye-fault-tolerance} code base. +Useful for prospective contributors or generally curious users, but not required for application developers. diff --git a/content/docs/smallrye-fault-tolerance/main/integration/async-types.adoc b/content/docs/smallrye-fault-tolerance/main/integration/async-types.adoc new file mode 100644 index 000000000..f4706c68c --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/main/integration/async-types.adoc @@ -0,0 +1,15 @@ += Additional Asynchronous Types Integration Concerns +:version-root: ../ +include::{version-root}_attributes.adoc[] + +This page describes integration concerns for xref:reference/asynchronous.adoc#async-types[Additional Asynchronous Types]. + +To enable support of additional asynchronous types, it is required that the support library is present. +That is: + +* https://smallrye.io/smallrye-mutiny/[Mutiny]: `io.smallrye:smallrye-fault-tolerance-mutiny` +* https://github.com/ReactiveX/RxJava/tree/3.x[RxJava 3]: `io.smallrye:smallrye-fault-tolerance-rxjava3` +* https://vertx.io/docs/vertx-core/java/#_future_results[Vert.x `Future`]: `io.smallrye:smallrye-fault-tolerance-vertx` + +These libraries include some service providers that {smallrye-fault-tolerance} will automatically load and use. +Therefore, no more integration is necessary. diff --git a/content/docs/smallrye-fault-tolerance/main/integration/context-propagation.adoc b/content/docs/smallrye-fault-tolerance/main/integration/context-propagation.adoc new file mode 100644 index 000000000..a39b7816d --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/main/integration/context-propagation.adoc @@ -0,0 +1,8 @@ += Context Propagation +:version-root: ../ +include::{version-root}_attributes.adoc[] + +{smallrye-fault-tolerance} supports MicroProfile Context Propagation for `@Asynchronous` method invocations, if 2 conditions are satisfied: + +- the integrator provides a MicroProfile Context Propagation enabled thread pool (see xref:integration/thread-pool.adoc[Thread Pool]); +- the `io.smallrye:smallrye-fault-tolerance-context-propagation` artifact is present (it provides alternative implementations of some internal services, loaded using `ServiceLoader`). diff --git a/content/docs/smallrye-fault-tolerance/main/integration/event-loop.adoc b/content/docs/smallrye-fault-tolerance/main/integration/event-loop.adoc new file mode 100644 index 000000000..2cc5818ac --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/main/integration/event-loop.adoc @@ -0,0 +1,37 @@ += Event Loop +:version-root: ../ +include::{version-root}_attributes.adoc[] + +{smallrye-fault-tolerance} provides an API to integrate with event loops used in non-blocking runtimes. + +WARNING: This integration API comes from the `core` module and so is treated as private. +If you intend to implement it, please let us know in the {smallrye-fault-tolerance} issue tracker. +We'd prefer all the implementations to live in the {smallrye-fault-tolerance} project. + +The integrator must implement the `EventLoop` interface, which lets {smallrye-fault-tolerance} submit ``Runnable``s to the event loop for execution. +Implementations are discovered through `ServiceLoader`, so the corresponding `META-INF/services/\...` file must also exist. + +At most one implementation of `EventLoop` may be present at runtime. +{smallrye-fault-tolerance} will use it to execute non-blocking guarded methods when the caller was originally on an event loop thread. +This is important in case of: + +- retries, when there's a delay between retry attempts (the delay must not block); +- bulkheads, when a task is queued (such task is executed when some other task finishes, which happens on an unknown thread). + +If no event loop integration is present, the tasks described above will be offloaded to the xref:integration/thread-pool.adoc[thread pool]. + +The event loop integration is also used in some other situations when guarding non-blocking methods: + +- fallbacks, when the original method fails (the fallback is executed on the original event loop); +- timeouts, when timeout occurs (the failure is signalled on the original event loop). + +== Vert.x + +{smallrye-fault-tolerance} includes an implementation of the event loop integration API for Vert.x. +This implementation is compiled and tested against Vert.x version {vertx-version}, but should also work fine with Vert.x 4.x. + +This implementation is present in the `io.smallrye:smallrye-fault-tolerance-vertx` artifact. +Including this artifact is enough to enable the Vert.x integration. + +NOTE: The Vert.x implementation is treated as public, but only as described here. +That is, the artifact should be treated as opaque, no guarantees about its internals are made. diff --git a/content/docs/smallrye-fault-tolerance/main/integration/intro.adoc b/content/docs/smallrye-fault-tolerance/main/integration/intro.adoc new file mode 100644 index 000000000..6f416fe4c --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/main/integration/intro.adoc @@ -0,0 +1,40 @@ += Introduction +:version-root: ../ +include::{version-root}_attributes.adoc[] + +{smallrye-fault-tolerance} comprises the following main artifacts: + +* `io.smallrye:smallrye-fault-tolerance-api`: public API for the features provided on top of {microprofile-fault-tolerance}, including a programmatic API; +* `io.smallrye:smallrye-fault-tolerance-apiimpl`: implementation of the programmatic API; +* `io.smallrye:smallrye-fault-tolerance-core`: core implementations of fault tolerance strategies; +* `io.smallrye:smallrye-fault-tolerance`: CDI-based implementation of {microprofile-fault-tolerance} with {smallrye-fault-tolerance} extensions, including a programmatic API; +* `io.smallrye:smallrye-fault-tolerance-autoconfig-core`: runtime part of the {smallrye-fault-tolerance} configuration system. +** (There's also `io.smallrye:smallrye-fault-tolerance-autoconfig-processor`, but that is only used during {smallrye-fault-tolerance} build and isn't required at runtime.) + +These artifacts must be always present, together with the following dependencies: + +* Jakarta CDI, Annotations and Interceptors API; +* `org.eclipse.microprofile.fault-tolerance:microprofile-fault-tolerance-api`; +* `org.eclipse.microprofile.config:microprofile-config-api` and some implementation; +* `org.jboss.logging:jboss-logging`. + +Some other artifacts are provided to facilitate additional integrations. +These are described in the subsequent sections. + +Most of the integration is performed automatically by the CDI Portable Extension present in {smallrye-fault-tolerance}: `io.smallrye.faulttolerance.FaultToleranceExtension`. +Make sure the extension is active. +The remaining integration concerns are described in the following sections: + +* xref:integration/metrics.adoc[emitting metrics] for fault tolerance strategies; +* xref:integration/thread-pool.adoc[providing a thread pool] for `@Asynchronous` methods and other asynchronous tasks; +* xref:integration/context-propagation.adoc[enabling Context Propagation] for `@Asynchronous` methods; +* xref:integration/event-loop.adoc[integrating an event loop] for non-blocking method invocations; +* xref:integration/async-types.adoc[integrating additional types] for asynchronous invocations; +* xref:integration/kotlin.adoc[integrating Kotlin support] for `suspend` functions; +* xref:integration/programmatic-api.adoc[integrating the programmatic API] that {smallrye-fault-tolerance} provides. + +== Standalone Implementation of the Programmatic API + +A completely standalone implementation of the programmatic API exists in the `io.smallrye:smallrye-fault-tolerance-standalone` artifact. +This is supposed to be used in a non-CDI environment. +Runtimes that integrate {smallrye-fault-tolerance} typically do so using CDI, and so should ignore this artifact. diff --git a/content/docs/smallrye-fault-tolerance/main/integration/kotlin.adoc b/content/docs/smallrye-fault-tolerance/main/integration/kotlin.adoc new file mode 100644 index 000000000..98d9804f9 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/main/integration/kotlin.adoc @@ -0,0 +1,11 @@ += Kotlin Integration Concerns +:version-root: ../ +include::{version-root}_attributes.adoc[] + +This page describes integration concerns for xref:reference/asynchronous.adoc#kotlin-suspend-functions[Kotlin `suspend` functions]. + +To enable support of Kotlin suspending functions, it is required that the support library is present. +Its coordinates are: `io.smallrye:smallrye-fault-tolerance-kotlin`. + +This library includes some service providers that {smallrye-fault-tolerance} will automatically load and use. +Therefore, no more integration is necessary. diff --git a/content/docs/smallrye-fault-tolerance/main/integration/metrics.adoc b/content/docs/smallrye-fault-tolerance/main/integration/metrics.adoc new file mode 100644 index 000000000..fda9de26f --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/main/integration/metrics.adoc @@ -0,0 +1,66 @@ += Metrics +:version-root: ../ +include::{version-root}_attributes.adoc[] + +{smallrye-fault-tolerance} provides support for OpenTelemetry and Micrometer. +Alternatively, metrics may be completely disabled at the integration level. + +As usual, this integration is based on CDI. +{smallrye-fault-tolerance} includes an internal interface `MetricsProvider` and these implementations: + +* `io.smallrye.faulttolerance.metrics.OpenTelemetryProvider` +* `io.smallrye.faulttolerance.metrics.MicrometerProvider` +* `io.smallrye.faulttolerance.metrics.NoopProvider` + +There are 2 possible ways how to integrate metrics: + +* exactly 1 class from the list above is a bean; +* more than 1 class from the list above is a bean, in which case, `io.smallrye.faulttolerance.metrics.CompoundMetricsProvider` must also be a bean. + +NOTE: Only the _names_ of the classes listed above are treated as public. +That is, the classes should be treated as opaque, no guarantees about their internals are made. + +== Default Integration + +In case the integrator uses the CDI Portable Extension `FaultToleranceExtension` and lets the container create an instance, metrics presence is discovered automatically. +All present metrics systems are used. + +== Using a `FaultToleranceExtension` Constructor + +In case the integrator uses the CDI Portable Extension `FaultToleranceExtension` and creates its instance manually, they can use a constructor. + +In addition to a zero-parameter constructor, which is used in the default integration as described above, there are constructors that take a parameter of `MetricsIntegration` or `Set`. + +`MetricsIntegration` is an enum with these values: + +* `OPENTELEMETRY`: use OpenTelemetry (MicroProfile Telemetry) integration +* `MICROMETER`: use Micrometer integration +* `NOOP`: no metrics + +== Metrics Providers + +Metrics providers have additional requirements, as described below. + +=== OpenTelemetry + +If OpenTelemetry should be used, the integrator must ensure that the following artifact is present: + +* `io.opentelemetry:opentelemetry-api`. + +Further, a bean of type `io.opentelemetry.api.metrics.Meter` must exist. +This bean is used to emit the actual metrics. + +=== Micrometer + +If Micrometer should be used, the integrator must ensure that the following artifact is present: + +* `io.micrometer:micrometer-core`. + +Further, a bean of type `io.micrometer.core.instrument.MeterRegistry` must exist. +This bean is used to emit the actual metrics. + +=== Noop + +If the "noop" provider is used, metrics are completely disabled. + +No metrics API and implementation is required in this case. diff --git a/content/docs/smallrye-fault-tolerance/main/integration/programmatic-api.adoc b/content/docs/smallrye-fault-tolerance/main/integration/programmatic-api.adoc new file mode 100644 index 000000000..636973a75 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/main/integration/programmatic-api.adoc @@ -0,0 +1,48 @@ += Programmatic API Integration Concerns +:version-root: ../ +include::{version-root}_attributes.adoc[] + +This page describes integration concerns of the xref:reference/programmatic-api.adoc[Programmatic API] of {smallrye-fault-tolerance}. + +== CDI Implementation + +Runtimes that provide the CDI implementation of the programmatic API don't have to do any extra integration work. +The existing integration is enough. + +== Standalone Implementation + +The standalone implementation exposes a single integration point: the `StandaloneFaultTolerance.configure()` static method. +This method must be called before fault tolerance is first used. +It accepts a custom implementation of the `Configuration` interface, which allows customizing: + +* `enabled()`: if `false`, all fault tolerance strategies except fallback and thread offload are disabled +* `executor()`: executor for thread offloads and other asynchronous tasks +* `metricsAdapter()`: adapter for metrics, see below + +When no `Configuration` is provided, sensible defaults are used: + +* fault tolerance is enabled unless system property `MP_Fault_Tolerance_NonFallback_Enabled` is set to `false` +* a thread pool obtained using `Executors.newCachedThreadPool()` is used as an executor +* no metrics are emitted + +Users of the standalone implementation that also use an event loop based library, such as Vert.x, may integrate the event loop support as described in xref:integration/event-loop.adoc[Event Loop]. + +=== Lifecycle + +The thread pool and all other internal resources are initialized lazily, on the first use. +The `StandaloneFaultTolerance.shutdown()` method may be used on application shutdown to shut down these internal resources. + +The executor is only shut down when no `Configuration` was provided and {smallrye-fault-tolerance} had to create an executor on its own. +If `Configuration` was provided, the executor shutdown is left to the integrator; it is _not_ attempted automatically. + +At the end of `StandaloneFaultTolerance.shutdown()`, the `Configuration.onShutdown()` method is called. + +After `StandaloneFaultTolerance.shutdown()`, it is not possible to reinitialize {smallrye-fault-tolerance} again. + +=== Metrics + +It is possible to integrate with OpenTelemetry or Micrometer. + +The `Configuration.metricsAdapter()` method must be implemented and return an instance of `io.smallrye.faulttolerance.standalone.OpenTelemetryAdapter` or `io.smallrye.faulttolerance.standalone.MicrometerAdapter`. +The constructor of `OpenTelemetryAdapter` accepts the `Meter` to which metrics shall be emitted. +The constructor of `MicrometerAdapter` accepts the `MeterRegistry` to which metrics shall be emitted. diff --git a/content/docs/smallrye-fault-tolerance/main/integration/thread-pool.adoc b/content/docs/smallrye-fault-tolerance/main/integration/thread-pool.adoc new file mode 100644 index 000000000..b223fd4fb --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/main/integration/thread-pool.adoc @@ -0,0 +1,30 @@ += Thread Pool +:version-root: ../ +include::{version-root}_attributes.adoc[] + +{smallrye-fault-tolerance} expects integrators to provide a thread pool for running `@Asynchronous` methods and other asynchronous tasks. +This integration is based on CDI. + +The integrator should provide a CDI bean which implements `AsyncExecutorProvider`. +The bean should be `@Singleton`, must be marked as alternative and selected globally for the application. + +There is a default thread pool in case no integration is provided, but it shouldn't be used outside of tests or experiments. +This is provided by the `AsyncExecutorProvider` default implementation, `DefaultAsyncExecutorProvider`. + +Integrators that are themselves Jakarta EE application servers and don't want to expose their own thread pool can subclass `DefaultAsyncExecutorProvider`. +This at least allows customizing the `ThreadFactory`. + +== Configuration + +If the integrator doesn't provide a thread pool, the default thread pool is used as described above. +In such case, the following configuration properties are recognized: + +- `io.smallrye.faulttolerance.mainThreadPoolSize`: maximum number of threads in the thread pool. +Defaults to `100`. + +- `io.smallrye.faulttolerance.mainThreadPoolQueueSize`: size of the queue that the thread pool should use. +Can be `> 0`, in which case the queue will be bounded to given size. +Can be `0`, in which case there will be no queue, and the thread pool will reject excess tasks. +Can be `-1`, in which case the queue will be unbounded. +Values `< -1` are an error. +Defaults to `-1`. diff --git a/content/docs/smallrye-fault-tolerance/main/internals/config.adoc b/content/docs/smallrye-fault-tolerance/main/internals/config.adoc new file mode 100644 index 000000000..6e505c7b2 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/main/internals/config.adoc @@ -0,0 +1,119 @@ += Configuration System +:version-root: ../ +include::{version-root}_attributes.adoc[] + +include::{partialsdir}/internals.adoc[] + +The xref:internals/core.adoc[core] fault tolerance strategies are configured simply by constructor parameters. +This is fine, since they are an internal implementation detail. +However, the constructor parameters correspond closely to the members of {microprofile-fault-tolerance} annotations. +The idea is that core fault tolerance strategies have no dependency on the {microprofile-fault-tolerance} API, but given a fault tolerance annotation, creating an instance of the corresponding strategy should be straightforward. + +On top of the core layer, {smallrye-fault-tolerance} implements {microprofile-fault-tolerance}, including a configuration system conforming to the specification. +This document briefly describes how the configuration system works. + +== `FaultToleranceOperation` + +The fault tolerance interceptor, which is the main entrypoint, looks up a `FaultToleranceOperation` instance for each class/method combination it has to process. +The `FaultToleranceOperation` holds a bean class, a descriptor of the guarded method, and configuration for each fault tolerance strategy applied to the method. +It also orchestrates configuration validation. +The interceptor uses a `FaultToleranceOperation` to instantiate all the core fault tolerance strategies for given guarded method. + +The `FaultToleranceOperation` instances are created early on, during application deployment, for all beans that use {microprofile-fault-tolerance}. +It may happen that a `FaultToleranceOperation` is created lazily, for special constructs such as runtime-built proxies, but that is largely irrelevant from the configuration perspective. + +== `FaultToleranceMethod` + +A `FaultToleranceOperation` is created from a `FaultToleranceMethod`. +A `FaultToleranceMethod` holds, again, a bean class, a descriptor of the guarded method, and the annotation instance for each annotation that is present on the guarded method or the class declaring the method. +It also knows what annotations have been declared directly on the method, and what annotations have been declared on the class. +This is because the {microprofile-fault-tolerance} configuration rules distinguish these two cases. + +A `FaultToleranceMethod` must be created in a CDI extension, because it represents the knowledge CDI has about the guarded method. +Note that CDI extensions may add, remove or modify annotations, so `FaultToleranceMethod` doesn't just hold the annotation instances as present in the class bytecode. + +.Quarkus +**** +In environments that bootstrap the CDI container during application build, such as Quarkus, the set of all ``FaultToleranceMethod``s in the application should be collected during build and transferred to runtime. +Then, at application runtime, ``FaultToleranceOperation``s should be created and validated. + +{smallrye-fault-tolerance} doesn't do anything like that, but is designed with Quarkus (and CDI Lite) in mind. +**** + +The main difference between `FaultToleranceMethod` and `FaultToleranceOperation` is that `FaultToleranceMethod` holds annotation instances, while `FaultToleranceOperation` holds configuration. + +== `@AutoConfig` + +As described above, `FaultToleranceMethod` holds annotation instances, while `FaultToleranceOperation` holds configuration. +Yet, `FaultToleranceOperation` exposes the annotation types (such as `@CircuitBreaker`) to anyone who needs the configuration. +The configuration consumers simply call the annotation methods (such as `requestVolumeThreshold()`), and configuration is handled behind the scenes, following the {microprofile-fault-tolerance} configuration rules. + +The infrastructure behind this is present in the `autoconfig` module. +This module allows creating simple configuration interfaces like this: + +[source,java] +---- +@AutoConfig +interface CircuitBreakerConfig extends CircuitBreaker, Config { + default void validate() { + ... + } +} +---- + +The config interface must: + +. be annotated with `@AutoConfig`; +. extend the annotation interface (`CircuitBreaker` in this case); +. extend the `Config` (or `ConfigDeclarativeOnly`) interface (from the `autoconfig/core` module); +. provide a `default` implementation of the `validate` method (defined in `Config`). + +For each such interface, an implementation class will be automatically generated by an annotation processor (implemented in the `autoconfig/processor` module). +In this case, it would be called `CircuitBreakerConfigImpl`. + +This implementation class has a `static` method called `create()` that accepts a `FaultToleranceMethod`. +It follows that for each guarded method, a new instance must be created. +If given `FaultToleranceMethod` doesn't contain an instance of particular annotation, `create()` simply returns `null`. + +The implementation class also overrides all methods from the annotation type. +Each annotation method is implemented to first consult MicroProfile Config, and only second to consult the annotation instance (which, as we described above, is present on the `FaultToleranceMethod`). + +The implementation class is supposed to be used just like the annotation type itself. +In other words, to obtain configured values, call the annotation methods on the config interface instance. +For example, to obtain the `requestVolumeThreshold` value, with respect to the {microprofile-fault-tolerance} configuration rules, we obtain an instance of `CircuitBreakerConfigImpl` and call `requestVolumeThreshold()`. + +The `FaultToleranceOperation` class holds instances of these config interfaces, and exposes them to configuration consumers. +It actually only exposes the annotation types, not the full config interface, but that is enough. + +== Putting it all together + +Let's recap how it all works. + +First, for each guarded method in the application, a `FaultToleranceMethod` instance is created, holding all the annotations that affect the method. + +Second, for each `FaultToleranceMethod`, a `FaultToleranceOperation` is created. +That, in turn, creates an instance of all relevant config interfaces. + +Finally, the fault tolerance interceptor looks up a `FaultToleranceOperation` and creates a chain of fault tolerance strategies. +For each fault tolerance strategy in the chain, the interceptor looks up configuration values from the corresponding config interface. + +The chain of fault tolerance strategies is created on the first invocation to a given guarded method and cached for all subsequent invocations. +That is how we satisfy the requirement that all stateful fault tolerance strategies are singletons, but that's a different story. + +== Supplement: Programmatic API + +The description above is slightly simplified for readability. +It is precise enough to describe how configuration works in the declarative, annotation-based API. + +The programmatic API itself supports configuration when used together with `@ApplyGuard`. +However, it doesn't support configuring _all_ strategies; some are built in a different manner and no configuration is possible. + +Therefore, the `FaultToleranceOperation` class described above is actually split into 2 classes: + +* `BasicFaultToleranceOperation`, which is shared for both configuration systems and holds all `Config` objects, +* its subclass `FaultToleranceOperation`, which only applies in case of the declarative API and holds all `ConfigDeclarativeOnly` objects. + +In addition to the description above, implementations of the `Config` interface (but not `ConfigDeclarativeOnly`) also include a `create()` method that takes a `String` identifier and a `Supplier` of the backing annotation instance. +This is supposed to be used to create an instance for configuring the programmatic API, because there's no `FaultToleranceMethod` in such case. +Also, a zero-configuration implementation of the `Config` interface is generated, called `...NoConfigImpl`. +This exists to be able to use the config interface even in the non-`@ApplyGuard` case, where no configuration is possible. diff --git a/content/docs/smallrye-fault-tolerance/main/internals/core.adoc b/content/docs/smallrye-fault-tolerance/main/internals/core.adoc new file mode 100644 index 000000000..271f108a9 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/main/internals/core.adoc @@ -0,0 +1,123 @@ += Fault Tolerance Core +:version-root: ../ +include::{version-root}_attributes.adoc[] + +include::{partialsdir}/internals.adoc[] + +The core idea in {smallrye-fault-tolerance} is a `FaultToleranceStrategy`. +It is an interface that looks like this: + +[source,java] +---- +interface FaultToleranceStrategy { + Future apply(FaultToleranceContext ctx); +} +---- + +NOTE: The `Future` type here is _not_ a `java.util.concurrent.Future`. +It comes from {smallrye-fault-tolerance} and it could be described as a very bare-bones variant of `CompletableFuture`, except it's split into `Completer` and `Future`. +It is not supposed to be used outside of this project. + +The `FaultToleranceContext` is similar to a `Callable>`; it represents the method invocation guarded by this fault tolerance strategy. +The fault tolerance strategy does its work around `ctx.call()`. +It can catch exceptions, invoke `ctx.call()` multiple times, invoke something else, etc. +As an example, let's consider this strategy, applicable to methods that return a `String`: + +[source,java] +---- +public class MyStringFallback implements FaultToleranceStrategy { + @Override + public Future apply(FaultToleranceContext ctx) { + Completer completer = Completer.create(); + try { + ctx.call().then((value, error) -> { + if (error == null) { + completer.complete(value); + } else { + completer.complete("my string value"); + } + }); + } catch (Exception ignored) { + completer.complete("my string value"); + } + return completer.future(); + } +} +---- + +This is a very simple fallback mechanism, which returns a pre-defined value in case of an exception. + +In the {smallrye-fault-tolerance} codebase, you can find implementations of all the strategies required by {microprofile-fault-tolerance}: retry, fallback, timeout, circuit breaker or bulkhead. +Asynchronous invocation, delegated to a thread pool, is of course also supported. + +When multiple fault tolerance strategies are supposed to be used to guard one method, they form a chain. +Continuing with our simple example, adding the ability to chain with another strategy would look like this: + +[source,java] +---- +public class MyStringFallback implements FaultToleranceStrategy { + private final FaultToleranceStrategy delegate; + + public MyStringFallback(FaultToleranceStrategy delegate) { + this.delegate = delegate; + } + + @Override + public Future apply(FaultToleranceContext ctx) { + Completer completer = Completer.create(); + try { + delegate.apply(ctx).then((value, error) -> { + if (error == null) { + completer.complete(value); + } else { + completer.complete("my string value"); + } + }); + } catch (Exception ignored) { + completer.complete("my string value"); + } + return completer.future(); + } +} +---- + +We see that one strategy delegates to another, passing the `FaultToleranceContext` along. +In fact, all the implementations in {smallrye-fault-tolerance} are written like this: they expect to be used in a chain, so they take another `FaultToleranceStrategy` to which they delegate. +But if all strategies have this form, when is `ctx.call()` actually invoked? +Good question! +The ultimate `ctx.call()` invocation is done by a special fault tolerance strategy which is called, well, `Invocation`. + +As an example which uses real {microprofile-fault-tolerance} annotations, let's consider this method: + +[source,java] +---- +@Retry(...) +@Timeout(...) +@Fallback(...) +public void doSomething() { + ... +} +---- + +The chain of fault tolerance strategies will look roughly like this: + +[source] +---- +Fallback( + Retry( + Timeout( + Invocation( + // ctx.call() will happen here + // that will, in turn, invoke doSomething() + ) + ) + ) +) +---- + +The order in which the strategies are chained (or, in fact, nested) is specified by {microprofile-fault-tolerance}. + +Other strategies might also be present in more complex cases. +For example, if metrics are enabled, a special strategy is used that collects metrics. +If the method is `@Asynchronous`, a special strategy is used that offloads method execution to another thread. +Etc. diff --git a/content/docs/smallrye-fault-tolerance/main/internals/instructions.adoc b/content/docs/smallrye-fault-tolerance/main/internals/instructions.adoc new file mode 100644 index 000000000..2b275e995 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/main/internals/instructions.adoc @@ -0,0 +1,21 @@ += Instructions +:version-root: ../ +include::{version-root}_attributes.adoc[] + +To compile this project and install the artifacts into local Maven repository, run: + +[source,bash] +---- +mvn clean install +---- + +This will also run all the tests and the TCK. + +You can use + +[source,bash] +---- +mvn clean install -DskipTests +---- + +to skip the tests and make the build a lot faster. diff --git a/content/docs/smallrye-fault-tolerance/main/internals/logging.adoc b/content/docs/smallrye-fault-tolerance/main/internals/logging.adoc new file mode 100644 index 000000000..bacfff5ec --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/main/internals/logging.adoc @@ -0,0 +1,16 @@ += Logging +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Logging in {smallrye-fault-tolerance} is based on https://jboss-logging.github.io/jboss-logging-tools/[JBoss Logging Tools] (see also https://github.com/smallrye/smallrye/wiki/I18N-Messages-and-Logging[SmallRye instructions]). +It's an annotation processor that takes annotated interfaces and generates logger implementations with out of the box support for message codes and internationalization. +We use a project code of `SRFTL`. +Message codes are divided into ranges that are defined in the `message-ranges.txt` file in the root directory. + +In addition to logging, JBoss Logging Tools can also be used for creating exceptions, again, with out of the box support for message codes and internationalization. +Contrary to both documentation sites mentioned above, it _is_ possible to mix logging and exceptions in a single interface, as demonstrated by `CdiLogger`. +However, {smallrye-fault-tolerance} doesn't use this feature yet, with the exception of a few methods in `CdiLogger`. +This should be rectified in the future. + +Note that `DEBUG` and `TRACE` logging doesn't need message codes or internationalization, so we usually don't declare logger methods for them, unless the log message requires parameters. +We still declare the logger interface, let it extend `BasicLogger` and use the singleton instance to log plain ``String``s. diff --git a/content/docs/smallrye-fault-tolerance/main/internals/project-structure.adoc b/content/docs/smallrye-fault-tolerance/main/internals/project-structure.adoc new file mode 100644 index 000000000..aa14ffa82 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/main/internals/project-structure.adoc @@ -0,0 +1,37 @@ += Project Structure +:version-root: ../ +include::{version-root}_attributes.adoc[] + +* `api`: {smallrye-fault-tolerance} API, additional to the {microprofile-fault-tolerance} API. + Treated as a public API. +* `implementation/core`: Implementation of core fault tolerance strategies. + Independent of the {microprofile-fault-tolerance} API or the {smallrye-fault-tolerance} API. + Treated as private API, no compatibility guaranteed. +* `implementation/apiimpl`: Implementation of the programmatic API. + Based on the `core` module. + Treated as private API, no compatibility guaranteed. +* `implementation/fault-tolerance`: Implementation of the {microprofile-fault-tolerance} API and the {smallrye-fault-tolerance} API. + Based on CDI and the `core` module. + The configuration system is based on the `autoconfig` module. +* `implementation/autoconfig/core`: Core classes of the {smallrye-fault-tolerance} configuration system. + Required at runtime. +* `implementation/autoconfig/processor`: An annotation processor that generates implementations of config interfaces. + Only required during {smallrye-fault-tolerance} build. +* `implementation/context-propagation`: Optional integration with MicroProfile Context Propagation. + See xref:integration/context-propagation.adoc[Context Propagation integration]. +* `implementation/kotlin`: Optional integration with Kotlin. + See xref:integration/kotlin.adoc[Kotlin Integration Concerns]. +* `implementation/mutiny`: Optional integration with Mutiny. + See xref:integration/async-types.adoc[Additional Asynchronous Types Integration Concerns]. +* `implementation/rxjava3`: Optional integration with RxJava 3. + See xref:integration/async-types.adoc[Additional Asynchronous Types Integration Concerns]. +* `implementation/standalone`: Standalone implementation of the {smallrye-fault-tolerance} programmatic API. + See xref:integration/programmatic-api.adoc[Programmatic API Integration Concerns]. +* `implementation/vertx`: Optional integration of the Vert.x event loop and Vert.x `Future`. + See xref:integration/event-loop.adoc[Event Loop integration] and xref:integration/async-types.adoc[Additional Asynchronous Types Integration Concerns]. +* `testsuite/basic`: Basic test suite. + Some parts of it overlap with the Eclipse MicroProfile Fault Tolerance TCK. +* `testsuite/integration`: Integration test suite. + Includes several Context Propagation and Vert.x tests. +* `testsuite/tck`: Runs the implementation against the Eclipse MicroProfile Fault Tolerance TCK. +* `doc`: This documentation. diff --git a/content/docs/smallrye-fault-tolerance/main/reference/asynchronous.adoc b/content/docs/smallrye-fault-tolerance/main/reference/asynchronous.adoc new file mode 100644 index 000000000..0937cf522 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/main/reference/asynchronous.adoc @@ -0,0 +1,276 @@ += Asynchronous Execution +:version-root: ../ +include::{version-root}_attributes.adoc[] + +For introduction, see xref:howto/asynchronous.adoc[How to Guard Asynchronous Methods]. +The how-to guide also explains what _asynchronous fault tolerance_ is and when does it apply. + +== Description + +=== `@Asynchronous` + +This annotation may only be placed on methods that return `java.util.concurrent.Future` or java.util.concurrent.`CompletionStage`. +It may also be placed on a class, in which case all business methods must return `Future` or `CompletionStage`. + +The method call is offloaded to another thread. +The thread pool that is used for offloading method calls is provided by the runtime that integrates {smallrye-fault-tolerance}. + +=== `@AsynchronousNonBlocking` + +include::{partialsdir}/srye-feature.adoc[] + +In addition to the {microprofile-fault-tolerance} `@Asynchronous` annotation, {smallrye-fault-tolerance} offers an annotation for asynchronous non-blocking processing: `@AsynchronousNonBlocking`. + +This annotation may only be placed on methods that return `CompletionStage`, because the `Future` type cannot really be used for non-blocking processing. +It may also be placed on a class, in which case all business methods must return `CompletionStage`. + +As opposed to `@Asynchronous`, the `@AsynchronousNonBlocking` annotation does _not_ offload the method execution to another thread. +The method is allowed to proceed on the original thread, assuming that it returns quickly and all processing it does is performed in a non-blocking way. + +=== `@Asynchronous` and `@AsynchronousNonBlocking` Compared + +|=== +|Annotation |Method return type |Execution + +|`@Asynchronous` +|`Future` or `CompletionStage` +|offloaded to a thread pool + +|`@AsynchronousNonBlocking` +|`CompletionStage` +|proceeds on the original thread +|=== + +=== `@Asynchronous` and `@AsynchronousNonBlocking` Combined + +When these annotations are combined, an annotation on a method has priority over an annotation on a class. +For example: + +[source,java] +---- +@ApplicationScoped +@Asynchronous +public class MyService { + @Retry + CompletionStage hello() { // <1> + ... + } + + @Retry + @AsynchronousNonBlocking + CompletionStage helloBlocking() { // <2> + ... + } +} +---- + +<1> Treated as `@Asynchronous`, based on the class annotation. +<2> Treated as `@AsynchronousNonBlocking`, the method annotation has priority over the class annotation. + +It is an error to put both `@Asynchronous` and `@AsynchronousNonBlocking` on the same program element. + +[[recommendation]] +=== Recommendation + +Use the <>. +In this mode, methods returning `CompletionStage` are automatically treated as if they were `@AsynchronousNonBlocking`. +Use `@Asynchronous` to mark blocking methods for thread offload. + +If you can't use the non-compatible mode, use `@AsynchronousNonBlocking` or `@Asynchronous` to mark all asynchronous methods. + +IMPORTANT: In previous releases, {smallrye-fault-tolerance} recommended to use the `@Blocking` and `@NonBlocking` annotations. +Using these annotations *for fault tolerance purposes* is no longer supported, starting with {smallrye-fault-tolerance} 7.0.0. + +We also recommend avoiding `@Asynchronous` methods that return `Future`, because the only way to obtain the future value is blocking. + +[[interactions]] +== Interactions with Other Strategies + +See xref:howto/multiple.adoc[How to Use Multiple Strategies] for an overview of how fault tolerance strategies are nested. + +=== `@Asynchronous` + +`@Asynchronous` does not interact with other fault tolerance strategies, with the obvious exception that it moves execution to another thread. + +=== `@AsynchronousNonBlocking` + +If the guarded method uses `@Retry` and some delay between retries is configured, only the initial execution is guaranteed to occur on the original thread. +Subsequent attempts may be offloaded to an extra thread, so that the original thread is not blocked on the delay. + +If the guarded method uses `@Bulkhead`, the execution is _not_ guaranteed to occur on the original thread. +If the execution has to wait in the bulkhead queue, it may later end up on a different thread. + +If the original thread is an event loop thread and event loop integration is enabled, then the event loop is always used to execute the guarded method. +In such case, all retry attempts and queued bulkhead executions are guaranteed to happen on the original thread. + +[[configuration]] +== Configuration + +There is no configuration for `@Asynchronous` or `@AsynchronousNonBlocking`. + +[[metrics]] +== Metrics + +Asynchronous execution does not emit any metrics. + +See xref:reference/metrics.adoc[the Metrics reference guide] for general metrics information. + +== Extra Features + +[[async-types]] +=== Additional Asynchronous Types + +include::{partialsdir}/srye-feature.adoc[] + +{microprofile-fault-tolerance} supports asynchronous fault tolerance for methods that return `CompletionStage`. +(The `java.util.concurrent.Future` type is not truly asynchronous, so we won't take it into account here.) +{smallrye-fault-tolerance} adds support for additional asynchronous types: + +* Mutiny: `Uni` +* RxJava 3: `Single`, `Maybe`, `Completable` +* Vert.x: `io.vertx.core.Future` + +These types are treated just like `CompletionStage`, so everything that works for `CompletionStage` works for these types as well. +Stream-like types (`Multi`, `Observable`, `Flowable`) are not supported, because their semantics can't be easily expressed in terms of `CompletionStage`. + +For example: + +[source,java] +---- +@ApplicationScoped +public class MyService { + @Retry + @AsynchronousNonBlocking // <1> + Uni hello() { // <2> + ... + } +} +---- + +<1> Using the `@AsynchronousNonBlocking` annotation, because the method doesn't block and offloading execution to another thread is not necessary. +<2> Returning the `Uni` type from Mutiny. +This shows that whatever works for `CompletionStage` also works for the other async types. + +The implementation internally converts the async types to a `CompletionStage` and back. +This means that to be able to use any particular asynchronous type, the corresponding converter must be present. +{smallrye-fault-tolerance} provides support libraries for popular asynchronous types, and these support libraries include the corresponding converters. + +It is possible that the runtime you use already provides the correct integration. +Otherwise, add a dependency to your application: + +* https://smallrye.io/smallrye-mutiny/[Mutiny]: `io.smallrye:smallrye-fault-tolerance-mutiny` +* https://github.com/ReactiveX/RxJava/tree/3.x[RxJava 3]: `io.smallrye:smallrye-fault-tolerance-rxjava3` +* https://vertx.io/docs/vertx-core/java/#_future_results[Vert.x `Future`]: `io.smallrye:smallrye-fault-tolerance-vertx` + +.Quarkus +**** +In Quarkus, the Mutiny and Vert.x support libraries are present by default. +You can use fault tolerance on methods that return `Uni` (and Vert.x `Future`) out of the box. +**** + +[[kotlin-suspend-functions]] +=== Kotlin `suspend` Functions + +include::{partialsdir}/srye-feature.adoc[] + +{smallrye-fault-tolerance} includes support for Kotlin suspending functions. +They are treated as <>, even though the internal implementation is more complex than support for Mutiny or RxJava 3. + +For example: + +[source,kotlin] +---- +@ApplicationScoped +open class MyService { + @Retry(maxRetries = 2) + @Fallback(fallbackMethod = "helloFallback") + open suspend fun hello(): String { // <1> + delay(100) + throw IllegalArgumentException() + } + + private suspend fun helloFallback(): String { // <2> + delay(100) + return "hello" + } +} +---- + +<1> As a suspending function, this method can only be called from another suspending function. +It will be guarded by the retry and fallback strategies, as defined using the annotations. +<2> Similarly to fallback methods in Java, fallback methods in Kotlin must have the same signature as the guarded method. +Since the guarded method is suspending, the fallback method must be suspending. + +As mentioned above, suspending functions are treated as async types. +This means that for asynchronous fault tolerance to work correctly on suspending functions, they must be determined to be asynchronous. +That happens automatically in the <>, based on the method signature, but if you use strictly compatible mode, the `@Asynchronous` or `@AsynchronousNonBlocking` annotation must be present. +It is expected that most users will use the Kotlin support in the non-compatible mode, so the example above does not include any such annotation. + +To be able to use this, a support library must be present. +It is possible that the runtime you use already provides the correct integration. +Otherwise, add a dependency to your application: `io.smallrye:smallrye-fault-tolerance-kotlin`. + +.Quarkus +**** +In Quarkus, the Kotlin support library is present by default, if you use the Quarkus Kotlin support. +You can declare fault tolerance annotations on suspending methods out of the box. +**** + +==== Programmatic API + +Suspending functions are currently only supported in the declarative, annotation-based API, as shown in the example above. +The xref:reference/programmatic-api.adoc[Programmatic API] of {smallrye-fault-tolerance} does not support suspending functions, but other than that, it can of course be used from Kotlin through its Java interop. + +[[method-asynchrony]] +=== Determining Asynchrony from Method Signature + +include::{partialsdir}/srye-feature.adoc[] + +include::{partialsdir}/non-compat.adoc[] + +In the non-compatible mode, method asynchrony is determined solely from its signature. +That is, methods that + +* have some fault tolerance annotation (such as `@Retry`), +* return `CompletionStage` (or some other <>), + +always have asynchronous fault tolerance applied. + +For example: + +[source,java] +---- +@ApplicationScoped +public class MyService { + @Retry + CompletionStage hello() { // <1> + ... + } + + @Retry + Uni helloMutiny() { // <2> + ... + } + + @Retry + @Asynchronous + CompletionStage helloBlocking() { // <3> + ... + } +} +---- + +<1> Executed on the original thread, because the method returns `CompletionStage`. +It is as if the method was annotated `@AsynchronousNonBlocking`. +<2> Executed on the original thread, because the method returns an <>. +It is as if the method was annotated `@AsynchronousNonBlocking`. +<3> The explicit `@Asynchronous` annotation is honored. +The method is executed on a thread pool. + +Note that the existing annotations still work without a change, both in compatible and non-compatible mode. +That is, if a method (or class) is annotated `@Asynchronous`, execution will be offloaded to a thread pool. +If a method (or class) is annotated `@AsynchronousNonBlocking`, execution will happen on the original thread. + +Also note that this doesn't affect methods returning `java.util.concurrent.Future`. +You still have to annotate them `@Asynchronous` to make sure they are executed on a thread pool and are guarded properly. +As mentioned in the <>, we discourage using these methods, because the only way to obtain the future value is blocking. diff --git a/content/docs/smallrye-fault-tolerance/main/reference/bulkhead.adoc b/content/docs/smallrye-fault-tolerance/main/reference/bulkhead.adoc new file mode 100644 index 000000000..a671d958b --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/main/reference/bulkhead.adoc @@ -0,0 +1,136 @@ += Bulkhead +:version-root: ../ +include::{version-root}_attributes.adoc[] + +For introduction, see xref:howto/bulkhead.adoc[How to Limit Concurrency]. + +== Description + +If a guarded method is called by too many callers concurrently, the excess attempts will fail with `BulkheadException`. + +If the guarded method is `@Asynchronous` or `@AsynchronousNonBlocking`, excess attempts are first put into a queue. +The queued attempts are executed later. +When the queue becomes full, excess attempts are rejected with `BulkheadException`. + +=== Lifecycle + +Bulkhead needs to maintain some state between invocations: the number of current executions and the queue of asynchronous executions. +This state is a singleton, irrespective of the lifecycle of the bean that uses the `@Bulkhead` annotation. + +More specifically, the bulkhead state is uniquely identified by the combination of the bean class (`java.lang.Class`) and the method object (`java.lang.reflect.Method`) representing the guarded method. + +For example, if there’s a guarded method `doWork` on a bean which is `@RequestScoped`, each request will have its own instance of the bean, but all invocations of `doWork` will share the same bulkhead state. + +[[interactions]] +== Interactions with Other Strategies + +See xref:howto/multiple.adoc[How to Use Multiple Strategies] for an overview of how fault tolerance strategies are nested. + +If `@Fallback` is used with `@Bulkhead`, the fallback method or handler may be invoked if a `BulkheadException` is thrown, depending on the fallback configuration. + +If `@Retry` is used with `@Bulkhead`, each retry attempt is processed by the bulkhead as an independent invocation. +If `BulkheadException` is thrown, the execution may be retried, depending on how retry is configured. + +If `@CircuitBreaker` is used with `@Bulkhead`, the circuit breaker is checked before enforcing the concurrency limit. +If concurrency limiting results in `BulkheadException`, this may be counted as a failure, depending on how the circuit breaker is configured. + +If `@RateLimit` is used with `@Bulkhead`, the rate limit is enforced before enforcing the concurrency limit. + +If `@Timeout` is used with `@Bulkhead`, the timeout watcher is started before enforcing the concurrency limit. + +[[configuration]] +== Configuration + +There are 2 configuration options, corresponding to the 2 members of the `@Bulkhead` annotation. + +=== `value` + +Type: `int` + +Default: `10` + +The concurrency limit. + +=== `waitingTaskQueue` + +Type: `int` + +Default: `10` + +The size of queue for excess attempts. +Only effective when the method is `@Asynchronous` or `@AsynchronousNonBlocking`. +See xref:reference/asynchronous.adoc[the Asynchronous Execution reference guide]. + +[[metrics]] +== Metrics + +Bulkhead exposes the following metrics: + +[cols="1,5"] +|=== +| Name | `ft.bulkhead.calls.total` +| Type +a| * OpenTelemetry: `LongCounter` +* Micrometer: `Counter` +| Unit | None +| Description | The number of times the bulkhead logic was run. This is usually once per method call, but may be zero times if the circuit breaker or rate limit prevented execution or more than once if the method call was retried. +| Tags +a| * `method` - the fully qualified method name +* `bulkheadResult` = `[accepted\|rejected]` - whether the bulkhead allowed the method call to run +|=== + +[cols="1,5"] +|=== +| Name | `ft.bulkhead.executionsRunning` +| Type +a| * OpenTelemetry: `LongUpDownCounter` +* Micrometer: `Gauge` +| Unit | None +| Description | Number of currently running executions. +| Tags +a| * `method` - the fully qualified method name +|=== + +[cols="1,5"] +|=== +| Name | `ft.bulkhead.executionsWaiting` +| Type +a| * OpenTelemetry: `LongUpDownCounter` +* Micrometer: `Gauge` +| Unit | None +| Description | Number of executions currently waiting in the queue. +| Tags +a| * `method` - the fully qualified method name +| Notes | Only added if the method is also `@Asynchronous` or `@AsynchronousNonBlocking` +|=== + +[cols="1,5"] +|=== +| Name | `ft.bulkhead.runningDuration` +| Type +a| * OpenTelemetry: `DoubleHistogram` with explicit bucket boundaries `[0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10]` +* Micrometer: `Timer` +| Unit +a| * OpenTelemetry: seconds +* Micrometer: nanoseconds +| Description | Histogram of the time that method executions spent running. +| Tags +a| * `method` - the fully qualified method name +|=== + +[cols="1,5"] +|=== +| Name | `ft.bulkhead.waitingDuration` +| Type +a| * OpenTelemetry: `DoubleHistogram` with explicit bucket boundaries `[0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10]` +* Micrometer: `Timer` +| Unit +a| * OpenTelemetry: seconds +* Micrometer: nanoseconds +| Description | Histogram of the time that method executions spent waiting in the queue. +| Tags +a| * `method` - the fully qualified method name +| Notes | Only added if the method is also `@Asynchronous` or `@AsynchronousNonBlocking` +|=== + +See xref:reference/metrics.adoc[the Metrics reference guide] for general metrics information. diff --git a/content/docs/smallrye-fault-tolerance/main/reference/circuit-breaker.adoc b/content/docs/smallrye-fault-tolerance/main/reference/circuit-breaker.adoc new file mode 100644 index 000000000..9b67acd68 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/main/reference/circuit-breaker.adoc @@ -0,0 +1,282 @@ += Circuit Breaker +:version-root: ../ +include::{version-root}_attributes.adoc[] + +For introduction, see xref:howto/circuit-breaker.adoc[How to Fail Fast on Recurring Failures]. + +== Description + +The circuit breaker is a simple state machine: + +image::images/diagrams/mermaid-f40d3cbcabca.svg[circuit-breaker] + +The circuit breaker starts _closed_. +In this state, the circuit breaker maintains a rolling window of recent invocations. +For each invocation, the rolling window tracks whether it finished successfully or failed. + +The rolling window must be full to take any state transition decision. +For example, if the rolling window has size 10, a closed circuit breaker always allows at least 10 invocations. + +If the rolling window contains a number of failures higher than the configured ratio, a closed circuit breaker moves to _open_. +When the circuit breaker is open, invocations are not allowed to proceed. +Instead, the circuit breaker fails fast and throws `CircuitBreakerOpenException`. + +For example, if the rolling window has size 10 and the failure rate is 0.5, it means that 5 invocations out of the most recent 10 invocations must fail for the circuit breaker to move to _open_. + +After some time, an open circuit breaker moves to _half-open_ to determine whether failing fast is still appropriate. +A half-open circuit breaker allows some configured number of probe attempts to proceed. +If all of them succeed, the circuit breaker moves to _closed_ and invocations are allowed again. +If some probe invocations fail, the circuit breaker moves back to _open_ and invocations are prevented. + +Successes and failures are determined from the method result. +More specifically: + +. If the guarded method returns normally, it is treated as success. +. Otherwise, if the guarded method throws an exception whose type is assignable to any of the `skipOn` types (see <>), it is treated as success. +. Otherwise, if the guarded method throws an exception whose type is assignable to any of the `failOn` types (see <>), it is treated as failure. +. Otherwise, it is treated as success. + +=== Lifecycle + +Circuit breaker needs to maintain some state between invocations: the number of recent successful invocations, the number of recent failed invocations, and so on. +This state is a singleton, irrespective of the lifecycle of the bean that uses the `@CircuitBreaker` annotation. + +More specifically, the circuit breaker state is uniquely identified by the combination of the bean class (`java.lang.Class`) and the method object (`java.lang.reflect.Method`) representing the guarded method. + +For example, if there’s a guarded method `doWork` on a bean which is `@RequestScoped`, each request will have its own instance of the bean, but all invocations of `doWork` will share the same circuit breaker state. + +[[interactions]] +== Interactions with Other Strategies + +See xref:howto/multiple.adoc[How to Use Multiple Strategies] for an overview of how fault tolerance strategies are nested. + +If `@Fallback` is used with `@CircuitBreaker`, the fallback method or handler may be invoked if a `CircuitBreakerOpenException` is thrown, depending on the fallback configuration. + +If `@Retry` is used with `@CircuitBreaker`, each retry attempt is processed by the circuit breaker as an independent invocation. +If `CircuitBreakerOpenException` is thrown, the execution may be retried, depending on how retry is configured. + +[[configuration]] +== Configuration + +There are 7 configuration options, corresponding to the 7 members of the `@CircuitBreaker` annotation. + +=== `requestVolumeThreshold` + +Type: `int` + +Default: `20` + +The size of the rolling window. +That is, the number of recent consecutive invocations tracked by a _closed_ circuit breaker. + +=== `failureRatio` + +Type: `double` + +Default: `0.5` + +The ratio of failures in the rolling window that causes a _closed_ circuit breaker to move to _open_. + +=== `delay` + `delayUnit` + +Type: `long` + `ChronoUnit` + +Default: `5000 millis`, or 5 seconds + +The delay after which an _open_ circuit breaker moves to _half-open_. + +=== `successThreshold` + +Type: `int` + +Default: `1` + +The number of probe invocations allowed when the circuit breaker is _half-open_. +If they all succeed, the circuit breaker moves to _closed_, otherwise it moves back to _open_. + +[[fail-on]] +=== `failOn` + +Type: `Class[]` + +Default: `{Throwable.class}` + +Set of exception types that the circuit breaker considers failures. + +[[skip-on]] +=== `skipOn` + +Type: `Class[]` + +Default: `{}` + +Set of exception types that the circuit breaker considers success. +This configuration takes precedence over `failOn`. + +[[metrics]] +== Metrics + +Circuit breaker exposes the following metrics: + +[cols="1,5"] +|=== +| Name | `ft.circuitbreaker.calls.total` +| Type +a| * OpenTelemetry: `LongCounter` +* Micrometer: `Counter` +| Unit | None +| Description | The number of times the circuit breaker logic was run. This is usually once per method call, but may be more than once if the method call is retried. +| Tags +a| * `method` - the fully qualified method name +* `circuitBreakerResult` = `[success\|failure\|circuitBreakerOpen]` - the result of the method call, as considered by the circuit breaker +** `success` - the method ran and was successful +** `failure` - the method ran and failed +** `circuitBreakerOpen` - the method did not run because the circuit breaker was in open or half-open state +|=== + +[cols="1,5"] +|=== +| Name | `ft.circuitbreaker.state.total` +| Type +a| * OpenTelemetry: `LongCounter` +* Micrometer: `TimeGauge` +| Unit +a| * OpenTelemetry: nanoseconds +* Micrometer: nanoseconds +| Description | Amount of time the circuit breaker has spent in each state +| Tags +a| * `method` - the fully qualified method name +* `state` = `[open\|closed\|halfOpen]` - the circuit breaker state +| Notes | Although this metric is a `Gauge`, its value increases monotonically. +|=== + +[cols="1,5"] +|=== +| Name | `ft.circuitbreaker.opened.total` +| Type +a| * OpenTelemetry: `LongCounter` +* Micrometer: `Counter` +| Unit | None +| Description | Number of times the circuit breaker has moved from closed state to open state +| Tags +a| * `method` - the fully qualified method name +|=== + +[cols="1,5"] +|=== +| Name | `ft.circuitbreaker.state.current` +2+a| +include::{partialsdir}/srye-feature.adoc[] +| Type +a| * OpenTelemetry: `LongUpDownCounter` +* Micrometer: `Gauge` +| Unit | None +| Description | Whether the circuit breaker is currently in given state (`1`) or not (`0`) +| Tags +a| * `method` - the fully qualified method name +* `state` = `[open\|closed\|halfOpen]` - the circuit breaker state +|=== + +See xref:reference/metrics.adoc[the Metrics reference guide] for general metrics information. + +== Extra Features + +=== Circuit Breaker Maintenance + +include::{partialsdir}/srye-feature.adoc[] + +It is sometimes useful to see the circuit breaker status from within the application, or reset it to the initial state. +This is possible in two steps: + +. Give the circuit breaker a name by annotating the guarded method with `@CircuitBreakerName`: ++ +[source,java] +---- +@ApplicationScoped +public class MyService { + @CircuitBreaker + @CircuitBreakerName("hello-cb") // <1> + public String hello() { + ... + } +} +---- ++ +<1> The circuit breaker guarding the `MyService.hello` method is given a name `hello-cb`. +. Inject `CircuitBreakerMaintenance` and call its methods: ++ +[source,java] +---- +@ApplicationScoped +public class Example { + @Inject + CircuitBreakerMaintenance maintenance; + + public void test() { + System.out.println("Circuit breaker state: " + + maintenance.currentState("hello-cb")); // <1> + maintenance.resetAll(); // <2> + } +} +---- ++ +<1> Obtains current circuit breaker state. +<2> Resets all circuit breakers to the initial state. + +The `CircuitBreakerMaintenance` interface provides 4 methods: + +. `currentState(name)`: returns current state of given circuit breaker. +The return type `CircuitBreakerState` is an `enum` with 3 values: `CLOSED`, `OPEN`, `HALF_OPEN`. +. `onStateChange(name, callback)`: registers a callback that will be called when given circuit breaker changes state. +. `reset(name)`: resets given circuit breaker to the initial state. +. `resetAll()`: resets all circuit breakers in the application to the initial state. + +See the javadoc of those methods for more information. + +[[inspecting-exception-cause-chains]] +=== Inspecting Exception Cause Chains + +include::{partialsdir}/srye-feature.adoc[] + +include::{partialsdir}/non-compat.adoc[] + +The `@CircuitBreaker` annotation can specify that certain exceptions should be treated as failures (`failOn`) and others as successes (`skipOn`). +The specification limits this to inspecting the actual exception that was thrown. +However, in many cases, exceptions are wrapped and the exception the user wants to decide on is only present in the cause chain. + +For that reason, in the non-compatible mode, if the actual thrown exception isn't known failure or known success, {smallrye-fault-tolerance} inspects the cause chain. +To be specific, in case a `@CircuitBreaker` method throws an exception, the decision process is: + +1. If the `skipOn` exceptions are not default and the exception is assignable to one of the `skipOn` exceptions, the circuit breaker treats it as a success. +2. Otherwise, if the `failOn` exceptions are not default and the exception is assignable to one of the `failOn` exceptions, the circuit breaker treats it as a failure. +3. Otherwise, if the exception is assignable to one of the `skipOn` exceptions or its cause chain contains an exception assignable to one of the `skipOn` exceptions, the circuit breaker treats it as a success. +4. Otherwise, if the exception is assignable to one of the `failOn` exceptions or its cause chain contains an exception assignable to one of the `failOn` exceptions, the circuit breaker treats it as a failure. +5. Otherwise, the exception is treated as a success. + +For example: + +[source,java] +---- +@CircuitBreaker(requestVolumeThreshold = 10, + skipOn = ExpectedOutcomeException.class, // <1> + failOn = IOException.class) // <2> +public Result doSomething() { + ... +} +---- + +<1> If `doSomething` throws an `ExpectedOutcomeException`, or a `WrapperException` whose cause is `ExpectedOutcomeException`, the circuit breaker treats it as a success. +<2> If `doSomething` throws an `IOException`, or a `WrapperException` whose cause is `IOException`, the circuit breaker treats it as a failure. + +[source,java] +---- +@CircuitBreaker(requestVolumeThreshold = 10, + skipOn = ExpectedOutcomeException.class) // <1> <2> +public Result doSomething() { + ... +} +---- + +<1> If `doSomething` throws an `ExpectedOutcomeException`, or a `WrapperException` whose cause is `ExpectedOutcomeException`, the circuit breaker treats it as a success. +<2> There's no `failOn`, so the 2nd step in the algorithm above is skipped. +This is what turns the `WrapperException` whose cause is `ExpectedOutcomeException` into a success. diff --git a/content/docs/smallrye-fault-tolerance/main/reference/config.adoc b/content/docs/smallrye-fault-tolerance/main/reference/config.adoc new file mode 100644 index 000000000..7a9288a12 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/main/reference/config.adoc @@ -0,0 +1,207 @@ += Configuration +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The fault tolerance strategies are primarily configured using annotations. +This is convenient, but changing such configuration requires recompilation. +For that reason, configuration using MicroProfile Config is also possible. + +NOTE: The configuration system of {microprofile-fault-tolerance}, as described in this guide, also applies to annotations defined by {smallrye-fault-tolerance}. + +== Annotation on Method + +The annotation may be present on a method: + +[source,java] +---- +package com.example; + +@ApplicationScoped +public class MyService { + @Retry(maxRetries = 10, retryOn = IOException.class) // <1> + public String hello() { + ... + } +} +---- + +<1> Retries will only be attempted if the thrown exception was `IOException`. +Other exceptions will be rethrown directly. +Also, the maximum number of retry attempts is changed to 10 (the default is 3). + +To change the `maxRetries` property using MicroProfile Config, set: + +[source,properties] +---- +smallrye.faulttolerance."com.example.MyService/hello".retry.max-retries=5 + +# alternatively, a specification-defined property can be used +com.example.MyService/hello/Retry/maxRetries=5 +---- + +== Annotation on Class + +The annotation may be present on a class, in which case it applies to all business method in the class: + +[source,java] +---- +package com.example; + +@ApplicationScoped +@Retry(maxRetries = 10, retryOn = IOException.class) // <1> +public class MyService { + public String hello() { + ... + } +} +---- +<1> The `hello()` method will use the `@Retry` strategy, as configured in this annotation. + +A class-level annotation may be overridden by explicitly annotating a method. + +The MicroProfile Config configuration property doesn't include the method name in this case: + +[source,properties] +---- +smallrye.faulttolerance."com.example.MyService".retry.max-retries=5 + +# alternatively, a specification-defined property can be used +com.example.MyService/Retry/maxRetries=5 +---- + +[IMPORTANT] +==== +Attempting to configure some property for a method or a class which does not have the annotation has no effect. + +* If the annotation is present on a method, the configuration property must target the method. +* If the annotation is present on a class, the configuration property must target the class. +==== + +== Global Configuration + +It is also possible to reconfigure some property for all annotations in the application. +For example: + +[source,properties] +---- +smallrye.faulttolerance.global.retry.max-retries=5 + +# alternatively, a specification-defined property can be used +Retry/maxRetries=5 +---- + +All `@Retry` annotations in the application will be considered to have `maxRetries` set to `5`. + +== Configuration Priority + +The most specific configuration has priority over more general configuration. + +For example: + +[source,java] +---- +package com.example; + +@ApplicationScoped +@Retry(maxRetries = 5) +public class MyService { + public String simpleHello() { + ... + } + + @Retry(maxRetries = 10) + public String complexHello() { + ... + } +} +---- + +The following configuration: + +[source,properties] +---- +smallrye.faulttolerance.global.retry.max-retries=30 +smallrye.faulttolerance."com.example.MyService".retry.max-retries=15 +smallrye.faulttolerance."com.example.MyService/complexHello".retry.max-retries=20 + +# alternatively, specification-defined properties can be used +Retry/maxRetries=30 +com.example.MyService/Retry/maxRetries=15 +com.example.MyService/complexHello/Retry/maxRetries=20 +---- + +Has the following effects: + +* `MyService.complexHello()` has `maxRetries` set to `20`. +* `MyService.simpleHello()` has `maxRetries` set to `15`. +* Other `@Retry` methods in the application have `maxRetries` set to `30`. + +== Disabling Fault Tolerance + +It is possible to disable certain fault tolerance annotation on some method, if the annotation is present on the method: + +[source,properties] +---- +smallrye.faulttolerance."com.example.MyService/hello".retry.enabled=false + +# alternatively, a specification-defined property can be used +com.example.MyService/hello/Retry/enabled=false +---- + +Or on some class, if the annotation is present on the class: + +[source,properties] +---- +smallrye.faulttolerance."com.example.MyService".retry.enabled=5 + +# alternatively, a specification-defined property can be used +com.example.MyService/Retry/enabled=false +---- + +Or globally: + +[source,properties] +---- +smallrye.faulttolerance.global.retry.enabled=false + +# alternatively, a specification-defined property can be used +Retry/enabled=false +---- + +It is also possible to disable all fault tolerance completely: + +[source,properties] +---- +smallrye.faulttolerance.enabled=false + +# alternatively, a specification-defined property can be used +MP_Fault_Tolerance_NonFallback_Enabled=false +---- + +This will leave only fallbacks enabled, all other annotations will be disabled. + +== {smallrye-fault-tolerance} Configuration Properties + +As demonstrated in the examples above, {smallrye-fault-tolerance} provides its own configuration properties, in addition to the specification-defined properties. +The specification-defined properties can of course be used, but the {smallrye-fault-tolerance} configuration properties have higher priority. + +The mapping is relatively straightforward: + +- `///` moves to `smallrye.faulttolerance."/"..` +- `//` moves to `smallrye.faulttolerance.""..` +- `/` moves to `smallrye.faulttolerance.global..` + +All the `` and `` parts are changed from camel case (`BeforeRetry`, `methodName`) to kebab case (`before-retry`, `method-name`). +Two annotation members are special cased to improve consistency: + +- `Retry/durationUnit` moves to `retry.max-duration-unit`, because the value property is called `maxDuration` (`max-duration`) +- `Retry/jitterDelayUnit` moves to `retry.jitter-unit`, because the value property is called `jitter` + +Further: + +- `MP_Fault_Tolerance_NonFallback_Enabled` moves to `smallrye.faulttolerance.enabled` +- `MP_Fault_Tolerance_Metrics_Enabled` moves to `smallrye.faulttolerance.metrics.enabled` + +== Links + +Configuration is described in detail in the {microprofile-fault-tolerance-url}#configuration[{microprofile-fault-tolerance} specification]. diff --git a/content/docs/smallrye-fault-tolerance/main/reference/fallback.adoc b/content/docs/smallrye-fault-tolerance/main/reference/fallback.adoc new file mode 100644 index 000000000..af6316c15 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/main/reference/fallback.adoc @@ -0,0 +1,302 @@ += Fallback +:version-root: ../ +include::{version-root}_attributes.adoc[] + +For introduction, see xref:howto/fallback.adoc[How to Supply Fallback Values]. + +== Description + +If a guarded method throws an exception, fallback will prevent that exception from being propagated to the caller. +Instead, a fallback method or fallback handler is called and their result is returned. + +More specifically: + +. If the guarded method returns normally, its return value is returned. +. Otherwise, if the guarded method throws an exception whose type is assignable to any of the `skipOn` types (see <>), the exception is rethrown. +. Otherwise, if the guarded method throws an exception whose type is assignable to any of the `applyOn` types (see <>), the fallback value is obtained and returned. +. Otherwise, the exception is rethrown. + +[[interactions]] +== Interactions with Other Strategies + +See xref:howto/multiple.adoc[How to Use Multiple Strategies] for an overview of how fault tolerance strategies are nested. + +If `@Fallback` is used, the fallback method or handler may be invoked if other fault tolerance strategies throw an exception, depending on the fallback configuration. + +[[configuration]] +== Configuration + +There are 4 configuration options, corresponding to the 4 members of the `@Fallback` annotation. + +Exactly one of `value` or `fallbackMethod` must be set explicitly, they don't have a useful default value. + +=== `value` + +Type: `Class>` + +Default: empty + +Class of the fallback handler implementation that should be used to obtain the fallback value. +When this is set, the `fallbackMethod` must be left empty. + +The fallback handler class must implement the `FallbackHandler` interface. +The type argument to `FallbackHandler` must be assignable to the return type of the guarded method. +For example, if the guarded method returns `Number`, the fallback handler may implement `FallbackHandler` or `FallbackHandler`. + +=== `fallbackMethod` + +Type: `String` + +Default: empty + +Name of the fallback method that should be used to obtain the fallback value. +When this is set, the `value` must be left empty. + +The fallback method must be declared directly on the class where the guarded method is declared, or must be inherited from any of the class's supertypes. +The fallback method must have the same parameter types as the guarded method. +The fallback method must have a return type that is assignable to the return type of the guarded method. +For example, if the guarded method returns `Number`, the fallback method may return `Number` or `Integer`. + +[[apply-on]] +=== `applyOn` + +Type: `Class[]` + +Default: `{Throwable.class}` + +Set of exception types to which fallback applies. + +[[skip-on]] +=== `skipOn` + +Type: `Class[]` + +Default: `{}` + +Set of exception types to which fallback does _not_ apply. +This configuration takes precedence over `applyOn`. + +[[metrics]] +== Metrics + +Fallback does not emit any metrics on its own. + +See xref:reference/metrics.adoc[the Metrics reference guide] for general metrics information. +Fallback is included in the overall method invocation metric described there. + +== Extra Features + +[[fallback-methods-with-exception-parameter]] +=== Fallback Methods with Exception Parameter + +include::{partialsdir}/srye-feature.adoc[] + +include::{partialsdir}/non-compat.adoc[] + +{smallrye-fault-tolerance} provides access to the causing exception in a fallback method. + +A fallback method, as defind by the {microprofile-fault-tolerance} specification, must have the same parameters as the guarded method. +{smallrye-fault-tolerance} permits defining one additional parameter, at the end of the parameter list, which must be of an exception type. +If such parameter is defined, the exception that caused the fallback will be supplied in it. + +For example: + +[source,java] +---- +@ApplicationScoped +public class MyService { + @Fallback(fallbackMethod = "fallback") + public String doSomething(String param) { + ... + } + + public String fallback(String param, IllegalArgumentException cause) { // <1> + ... + } +} +---- + +<1> The fallback method matches the guarded method signature, except for one additional parameter at the end. + +All rules of {microprofile-fault-tolerance} specification related to looking up fallback methods still apply. +That is, the return types must match, the parameter types must match (with this one exception), etc. + +If the thrown exception is not assignable to the exception parameter type, it is rethrown as if no fallback was declared. +In the previous example, if `IllegalStateException` was thrown, the fallback method would not be called, as `IllegalStateException` is not a subtype of `IllegalArgumentException`. + +If the guarded method has a vararg parameter and you want to declare a fallback method with an exception parameter, simply replace the vararg syntax with an array type: + +[source,java] +---- +@ApplicationScoped +public class MyService { + @Fallback(fallbackMethod = "fallback") + public String doSomething(String... params) { + ... + } + + public String fallback(String[] params, IllegalArgumentException cause) { + ... + } +} +---- + +==== Multiple Fallback Methods with Exception Parameter + +It is possible to declare multiple overloads of the fallback method, each having different type of the exception parameter: + +[source,java] +---- +@ApplicationScoped +public class MyService { + @Fallback(fallbackMethod = "fallback") + public String doSomething(String param) { + ... + } + + public String fallback(String param, IllegalArgumentException cause) { + ... + } + + public String fallback(String param, RuntimeException cause) { + ... + } +} +---- + +In that case, which fallback method is called depends on the type of thrown exception. +The method that declares a most-specific supertype of the actual exception is selected. + +In the previous example, if `IllegalArgumentException` was thrown by `doSomething`, the first fallback method would be called. +If `IllegalStateException` was thrown, the second fallback method would be called. + +If the thrown exception is not assignable to the exception parameter type of any fallback method, it is rethrown as if no fallback was declared. + +==== Fallback Methods with and without Exception Parameter + +It is possible to declare the fallback method with and without an exception parameter at the same time: + +[source,java] +---- +@ApplicationScoped +public class MyService { + @Fallback(fallbackMethod = "fallback") + public String doSomething(String param) { + ... + } + + public String fallback(String param, IllegalArgumentException cause) { + ... + } + + public String fallback(String param, RuntimeException cause) { + ... + } + + public String fallback(String param) { + ... + } +} +---- + +The fallback methods with an exception parameter have precedence. +The fallback method without an exception parameter is only called if the thrown exception is not assignable to any declared exception parameter. + +==== Interactions with `applyOn` / `skipOn` + +The presence or absence of a fallback method with specific exception parameter may seem related to the usage of `applyOn` / `skipOn` on the `@Fallback` annotation, but in fact, it is not. +These features are completely independent. + +Simply put, the `applyOn` / `skipOn` configuration is always evaluated first. +A fallback method is only selected and invoked when this configuration indicates that a fallback should apply. + +If `@Fallback` is configured to skip `IllegalStateException` and `IllegalStateException` is thrown, no fallback method is invoked. +That applies even if a fallback method with a matching exception parameter exists. + +For example: + +[source,java] +---- +@ApplicationScoped +public class MyService { + @Fallback(fallbackMethod = "fallback", skipOn = IllegalStateException.class) + public String doSomething(String param) { + ... + } + + public String fallback(String param, IllegalArgumentException cause) { + ... + } + + public String fallback(String param, RuntimeException cause) { + ... + } + + public String fallback(String param) { + ... + } +} +---- + +In this case: + +- if `doSomething` throws `IllegalArgumentException`, the first fallback method is called; +- if `doSomething` throws `IllegalStateException`, no fallback method is called, because this exception type is skipped; +- if `doSomething` throws any other `RuntimeException`, the second fallback method is called; +- if `doSomething` throws any other exception, the last fallback method is called. + +[[inspecting-exception-cause-chains]] +=== Inspecting Exception Cause Chains + +include::{partialsdir}/srye-feature.adoc[] + +include::{partialsdir}/non-compat.adoc[] + +The `@Fallback` annotation can specify that certain exceptions should be treated as failures (`applyOn`) and others as successes (`skipOn`). +The specification limits this to inspecting the actual exception that was thrown. +However, in many cases, exceptions are wrapped and the exception the user wants to decide on is only present in the cause chain. + +For that reason, in the non-compatible mode, if the actual thrown exception isn't known failure or known success, {smallrye-fault-tolerance} inspects the cause chain. +To be specific, in case a `@Fallback` method throws an exception, the decision process is: + +1. If the `skipOn` exceptions are not default and the exception is assignable to one of the `skipOn` exceptions, fallback is skipped and the exception is rethrown. +2. Otherwise, if the `applyOn` exceptions are not default and the exception is assignable to one of the `applyOn` exceptions, fallback is applied. +3. Otherwise, if the exception is assignable to one of the `skipOn` exceptions or its cause chain contains an exception assignable to one of the `skipOn` exceptions, fallback is skipped and the exception is rethrown. +4. Otherwise, if the exception is assignable to one of the `applyOn` exceptions or its cause chain contains an exception assignable to one of the `applyOn` exceptions, fallback is applied. +5. Otherwise, the exception is rethrown. + +For example: + +[source,java] +---- +@Fallback(fallbackMethod = "fallback", + skipOn = ExpectedOutcomeException.class, // <1> + applyOn = IOException.class) // <2> +public Result doSomething() { + ... +} + +public Result fallback() { + ... +} +---- + +<1> If `doSomething` throws an `ExpectedOutcomeException`, or a `WrapperException` whose cause is `ExpectedOutcomeException`, fallback is skipped and the exception is thrown. +<2> If `doSomething` throws an `IOException`, or a `WrapperException` whose cause is `IOException`, fallback is applied. + +[source,java] +---- +@Fallback(fallbackMethod = "fallback", + skipOn = ExpectedOutcomeException.class) // <1> <2> +public Result doSomething() { + ... +} + +public Result fallback() { + ... +} +---- + +<1> If `doSomething` throws an `ExpectedOutcomeException`, or a `WrapperException` whose cause is `ExpectedOutcomeException`, fallback is skipped and the exception is thrown. +<2> There's no `applyOn`, so the 2nd step in the algorithm above is skipped. +This is what turns the `WrapperException` whose cause is `ExpectedOutcomeException` into a skipped fallback. diff --git a/content/docs/smallrye-fault-tolerance/main/reference/metrics.adoc b/content/docs/smallrye-fault-tolerance/main/reference/metrics.adoc new file mode 100644 index 000000000..09300ea27 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/main/reference/metrics.adoc @@ -0,0 +1,109 @@ += Metrics +:version-root: ../ +include::{version-root}_attributes.adoc[] + +{smallrye-fault-tolerance} exposes metrics, as {microprofile-fault-tolerance-url}#_integration_with_microprofile_metrics_and_microprofile_telemetry[specified] by {microprofile-fault-tolerance}. + +[[general]] +== General Metrics + +For all methods guarded with some fault tolerance strategy, the following metric is emitted: + +[cols="1,5"] +|=== +| Name | `ft.invocations.total` +| Type +a| * OpenTelemetry: `LongCounter` +* Micrometer: `Counter` +| Unit | None +| Description | The number of times the method was called. +| Tags +a| * `method` - the fully qualified method name +* `result` = `[valueReturned\|exceptionThrown]` - whether the invocation returned a value or threw an exception +* `fallback` = `[applied\|notApplied\|notDefined]` - `applied` if fallback was used, `notApplied` if a fallback is configured but was not used, `notDefined` if a fallback is not configured +|=== + +For xref:reference/asynchronous.adoc[asynchronous methods], "value returned" means that the `CompletionStage` completed successfully, while "exception thrown" means that the `CompletionStage` completed exceptionally. + +== Specific Metrics + +In addition to the general metrics described above, most fault tolerance strategies emit their own metrics. +Those are described in the reference guides: + +* xref:reference/bulkhead.adoc#metrics[Bulkhead] +* xref:reference/circuit-breaker.adoc#metrics[Circuit Breaker] +* xref:reference/rate-limit.adoc#metrics[Rate Limit] +* xref:reference/retry.adoc#metrics[Retry] +* xref:reference/timeout.adoc#metrics[Timeout] + +== Timer Metrics + +For task scheduling purposes (e.g. watching timeouts or delaying retries), {smallrye-fault-tolerance} maintains one thread called the _timer thread_. +Most of the time, it is kept sleeping (parked), it only wakes up (unparks) when necessary to submit tasks to executors. + +The behavior of the timer thread can be observed through the following metrics: + +[cols="1,5"] +|=== +| Name | `ft.timer.scheduled` +| Type +a| * OpenTelemetry: `LongUpDownCounter` +* Micrometer: `Gauge` +| Unit | None +| Description | The number of tasks that are currently scheduled (for future execution) on the timer. +| Tags +a| * `id` - the ID of the timer, to distinguish multiple timers in a multi-application environment +|=== + +== Micrometer Support + +In addition to the OpenTelemetry support (as specified by {microprofile-fault-tolerance}), {smallrye-fault-tolerance} also provides support for https://micrometer.io/[Micrometer]. +The set of metrics emitted to Micrometer is the same, using the same metric names and tags. + +Note that distribution summaries in Micrometer, including timers, do not emit quantiles by default. +Micrometer recommends that libraries should not configure them out of the box, so if you need them, you should use a `MeterFilter`. + +The following implementation makes sure Micrometer emits some common quantiles for all fault tolerance metrics: + +[source,java] +---- +static final MeterFilter ENABLE_HISTOGRAMS = new MeterFilter() { + @Override + public DistributionStatisticConfig configure(Meter.Id id, DistributionStatisticConfig config) { + if (id.getName().startsWith("ft.")) { + return DistributionStatisticConfig.builder() + .percentiles(0.5, 0.75, 0.9, 0.95, 0.99, 0.999) + .build() + .merge(config); + } + return config; + } +}; +---- + +== Disabling Metrics + +It is possible to completely disable fault tolerance metrics using MicroProfile Config: + +[source,properties] +---- +smallrye.faulttolerance.metrics.enabled=false + +# alternatively, a specification-defined property can be used +MP_Fault_Tolerance_Metrics_Enabled=false +---- + +It is also possible to disable individual metric providers: + +[source,properties] +---- +# to disable OpenTelemetry Metrics +smallrye.faulttolerance.opentelemetry.disabled=true + +# to disable Micrometer +smallrye.faulttolerance.micrometer.disabled=true +---- + +Note that setting `smallrye.faulttolerance.*.disabled` to `false` does not mean the provider is enabled unconditionally. +When that provider is not discovered or selected by the integrator, it cannot be enabled in any way. +These properties are only meant for disabling an otherwise enabled metrics provider; not the other way around. diff --git a/content/docs/smallrye-fault-tolerance/main/reference/non-compat.adoc b/content/docs/smallrye-fault-tolerance/main/reference/non-compat.adoc new file mode 100644 index 000000000..20fcf0b64 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/main/reference/non-compat.adoc @@ -0,0 +1,36 @@ += Non-compatible Mode +:version-root: ../ +include::{version-root}_attributes.adoc[] + +include::{partialsdir}/srye-feature.adoc[] + +{smallrye-fault-tolerance} offers a mode where certain features are improved beyond specification, as described below. +This mode is *not compatible* with the {microprofile-fault-tolerance} specification (and doesn't necessarily pass the entire TCK). + +This mode is disabled by default. +To enable, set the configuration property `smallrye.faulttolerance.mp-compatibility` to `false`. + +.Quarkus +**** +In Quarkus, the non-compatible mode is enabled by default. +To restore compatibility, add the following to your `application.properties`: + +[source,properties] +---- +smallrye.faulttolerance.mp-compatibility=true +---- + +Note that the non-compatible mode is available since {smallrye-fault-tolerance} 5.2.0 and Quarkus 2.1.0.Final. +Previous versions are always compatible. +**** + +== Improvements + +The improvements enabled in the non-compatible mode are: + +* xref:reference/asynchronous.adoc#method-asynchrony[Determining asynchrony from method signatures] +* Inspecting exception cause chains +** in xref:reference/fallback.adoc#inspecting-exception-cause-chains[fallback] +** in xref:reference/circuit-breaker.adoc#inspecting-exception-cause-chains[circuit breaker] +** in xref:reference/retry.adoc#inspecting-exception-cause-chains[retry] +* xref:reference/fallback.adoc#fallback-methods-with-exception-parameter[Fallback methods with exception parameter] diff --git a/content/docs/smallrye-fault-tolerance/main/reference/programmatic-api.adoc b/content/docs/smallrye-fault-tolerance/main/reference/programmatic-api.adoc new file mode 100644 index 000000000..2260df49e --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/main/reference/programmatic-api.adoc @@ -0,0 +1,414 @@ += Programmatic API +:version-root: ../ +include::{version-root}_attributes.adoc[] + +include::{partialsdir}/srye-feature.adoc[] + +In addition to the declarative, annotation-based API of {microprofile-fault-tolerance}, {smallrye-fault-tolerance} also offers a programmatic API for advanced scenarios. +This API is present in the `io.smallrye:smallrye-fault-tolerance-api` artifact, just like all other additional APIs {smallrye-fault-tolerance} provides. + +== Installation + +If you use {smallrye-fault-tolerance} as part of a runtime that implements {microprofile-fault-tolerance}, you don't have to do anything. +The programmatic API is ready to use. +In this documentation, we'll call this the _CDI implementation_ of the programmatic API, because it's integrated with the {microprofile-fault-tolerance} implementation, which is naturally based on CDI. + +.Quarkus +**** +In Quarkus, the {smallrye-fault-tolerance} API is brought in automatically, as a transitive dependency of the Quarkus extension for {smallrye-fault-tolerance}. +That is, you don't need to do anything to be able to use the programmatic API. +**** + +.WildFly +**** +In WildFly, the {smallrye-fault-tolerance} API is not readily available to deployments. +If you want to use it, you need to add a module dependency to the deployment using `jboss-deployment-structure.xml`. + +Note that at the time of this writing, the {smallrye-fault-tolerance} module in WildFly is considered private. +If you do add a module dependency on it, to be able to use the {smallrye-fault-tolerance} API, you may be stepping out of the WildFly support scope. +**** + +In addition to the CDI implementation, {smallrye-fault-tolerance} also offers a _standalone implementation_ that is meant to be used outside of any runtime. +This implementation does not need CDI or anything else. +If you want to use {smallrye-fault-tolerance} in a standalone fashion, just add a dependency on `io.smallrye:smallrye-fault-tolerance-standalone`. +The API is brought in transitively. + +== Usage + +The entrypoints to the programmatic API are the `Guard` and `TypedGuard` interfaces. + +These interfaces represent a configured set of fault tolerance strategies. +Their configuration, order of application and behavior in general corresponds to the declarative API, so if you know that, you'll feel right at home. +If not, the javadoc has all the information you need (though it often points to the annotation-based API for more information). + +The interfaces are very similar, there are only 2 differences: + +. `Guard` requires specifying the return type of the guarded operation for each `call()` or `get()` (or `adapt*`) method call. + `TypedGuard`, on the other hand, requires specifying the return type once, when creating an instance using `create()`. + Therefore, `Guard` may be used for guarding many different types, while `TypedGuard` may only be used to guard single type. +. `TypedGuard` allows defining a fallback. + `Guard` does not. + +In the following text, we'll only talk about `Guard`, but it also applies to `TypedGuard` without a change (except of the differences mentioned above). + +To create an instance of `Guard`, you can use the `create` static method. +It returns a builder which has to be used to add and configure all the fault tolerance strategies that should apply. +There is no external configuration, so all configuration properties have to be set explicitly, using the builder methods. +If you don't set a configuration property, it will default to the same value the annotation-based API uses. + +.Disabling Fault Tolerance +**** +There's one exception to the "no external configuration" rule. + +The CDI implementation looks for the `smallrye.faulttolerance.enabled` / `MP_Fault_Tolerance_NonFallback_Enabled` configuration properties in MicroProfile Config. +The standalone implementation looks for system properties with the same name, or obtains the value from custom configuration (as xref:integration/programmatic-api.adoc[described] in the integration section). + +If at least one of these properties exists and is set to `false`, only the fallback and thread offload fault tolerance strategies will be applied. +Everything else will be ignored. + +Note that this is somewhat different to the declarative, annotation-based API, where only fallback is retained and the `@Asynchronous` strategy is skipped as well. +Since this significantly changes execution semantics, the programmatic API will apply thread offload even if fault tolerance is disabled. + +Similarly to the declarative API, implementations of the programmatic API also read this property only once, when the `Guard` API is first used. +It is _not_ read again later. +**** + +Let's take a look at a simple example: + +[source,java] +---- +public class MyService { + private static final Guard GUARD = Guard.create() + .withRetry().maxRetries(3).done() + .build(); + + public String hello() throws Exception { + return GUARD.call(() -> externalService.hello(), String.class); // <1> + } +} +---- + +<1> Here, we call `externalService.hello()` and guard the call with the previously configured set of fault tolerance strategies. + The `call` method used here takes the `Callable` type, which represent the guarded action. + The `get` method works just like `call`, but accepts a `Supplier`. + +The previous example shows how to apply fault tolerance to synchronous actions. +{smallrye-fault-tolerance} naturally also supports guarding asynchronous actions, using the `java.util.concurrent.CompletionStage` type. +Unlike the declarative API, the programmatic API doesn't support asynchronous actions that return the `java.util.concurrent.Future` type. + +[source,java] +---- +public class MyService { + private static final Guard GUARD = Guard.create() + .withBulkhead().done() + .withThreadOffload(true) // <1> + .build(); + + public CompletionStage hello() throws Exception { + return GUARD.call(() -> externalService.hello(), + new TypeLiteral>() {}); // <2> + } +} +---- + +<1> The thread offload here only applies to asynchronous actions. + If you use the same `GUARD` to guard a synchronous action, no thread offload will apply. +<2> Note that here, we use the `TypeLiteral` class (from CDI) to specify the type of the guarded action. + +Asynchronous actions may be blocking or non-blocking. +In the example above, we assume the `externalService.hello()` call is blocking, so we set thread offload to `true`. +{smallrye-fault-tolerance} will automatically move the actual execution of the action to another thread. + +If we didn't configure `withThreadOffload`, however, the execution of an asynchronous action would continue on the original thread. +This is often desired for non-blocking actions, which are very common in modern reactive architectures. + +Also note that in this example, we configured multiple fault tolerance strategies: bulkhead and thread offload. +When that happens, the fault tolerance strategies are ordered according to the {microprofile-fault-tolerance} specification, just like in the declarative API. +Order of all the `with*` method invocations doesn’t matter. + +=== Single-Action Usage + +The `Guard` API is general and permits guarding multiple different actions using the same set of fault tolerance strategies. +Often, we need to guard just a single action, although possibly several times. + +For such use case, the `Guard` instance may be adapted to a `Callable` or `Supplier` using the `adapt*` methods: + +[source,java] +---- +public class MyService { + private static final Callable guard = Guard.create() + .withTimeout().duration(5, ChronoUnit.SECONDS).done() + .build() // <1> + .adaptCallable(() -> externalService.hello(), String.class); // <2> + + public String hello() throws Exception { + return callable.call(); // <3> + } + +} +---- + +<1> Create a `Guard` object that can guard arbitrary actions. +<2> Adapt the general `Guard` instance to a `Callable` that guards the `externalService.hello()` invocation. + Similar method exists that accepts and returns a `Supplier`: `adaptSupplier`. +<3> You can do whatever you wish with the adapted `Callable`. + Here, we just call it once, which isn't very interesting, but it could possibly be called multiple times, passed to other methods etc. + +=== Synchronous vs. Asynchronous + +The `Guard` and `TypedGuard` interfaces both decide whether the guarded action is synchronous or asynchronous based on the action type. +This is given to `Guard` when calling or adapting the action, and to `TypedGuard` when creating it. + +If the type is asynchronous (such as `CompletionStage`), the asynchronous behavior will also be guarded; the action is only considered complete when the `CompletionStage` actually completes. +If the type is not asynchronous, only the synchronous behavior will be guarded; the action is considered complete when the method returns. + +To enable support of additional asynchronous types, it is enough to include the support libraries, as described in xref:reference/asynchronous.adoc#async-types[Additional Asynchronous Types]. + +==== Mutiny Support + +For example, when the Mutiny support library `io.smallrye:smallrye-fault-tolerance-mutiny` is present, both `Guard` and `TypedGuard` will recognize `Uni` as an asynchronous type and guard it properly. +Guarding a `Multi` is not supported. + +For example: + +[source,java] +---- +public class MyService { + private final Supplier> GUARD = TypedGuard.create( + new TypeLiteral>() {}) + .withTimeout().duration(5, ChronoUnit.SECONDS).done() + .withFallback().handler(() -> Uni.createFrom().item("fallback")).done() + .build() + .adaptSupplier(() -> externalService.hello()); // <1> + + public Uni hello() { + return guard.get(); + } +} +---- + +<1> The call to `externalService.hello()` is supposed to return `Uni`. + +Note that the `Uni` type is lazy, so the action itself won't execute until the guarded `Uni` is subscribed to. + +.Quarkus +**** +In Quarkus, the Mutiny support library is present by default. +You can guard `Uni`-returning actions out of the box. +**** + +=== Stateful Fault Tolerance Strategies + +The bulkhead, circuit breaker and rate limit strategies are stateful. +That is, they hold some state required for their correct functioning, such as the number of current executions for bulkhead, the rolling window of successes/failures for circuit breaker, or the time window for rate limit. +If you use these strategies, you have to consider their lifecycle. + +The {smallrye-fault-tolerance} programmatic API makes such reasoning pretty straightforward. +Each `Guard` object has its own instance of each fault tolerance strategy, including the stateful strategies. +If you use a single `Guard` object for guarding multiple different actions, all those actions will be guarded by the same bulkhead, circuit breaker and/or rate limit. +If, on the other hand, you use different `Guard` objects for guarding different actions, each action will be guarded by its own bulkhead, circuit breaker and/or rate limit. + +If you call the `adapt*` methods on the same `Guard` multiple times, the resulting `Callable` or `Supplier` objects will guard the underlying action using the original `Guard` instance, so stateful strategies will be shared. + +=== Circuit Breaker Maintenance + +The `CircuitBreakerMaintenance` API, accessed through `CircuitBreakerMaintenance.get()` or by injection in the CDI implementation, can be used to manipulate all named circuit breakers. +A circuit breaker is given a name by calling `withCircuitBreaker().name("\...")` on the fault tolerance builder, or using the `@CircuitBreakerName` annotation in the declarative API. + +Additionally, `CircuitBreakerMaintenance.resetAll()` will also reset all unnamed circuit breakers declared using the `@CicruitBreaker` annotation. +For this to work, all unnamed circuit breakers have to be remembered. +This is safe in case of the declarative, annotation-based API, because the number of such declared circuit breakers is fixed. +At the same time, this would _not_ be safe to do for all unnamed circuit breakers created using the programmatic API, as their number is potentially unbounded. +(In other words, remembering all unnamed circuit breakers created using the programmatic API would easily lead to a memory leak.) + +Therefore, all circuit breakers created using the programmatic API must be given a name when `CircuitBreakerMaintenance` is supposed to affect them. +Note that duplicate names are not permitted and lead to an error, so lifecycle of the circuit breaker must be carefully considered. + +=== Event Listeners + +The programmatic API has one feature that the declarative API doesn't have: ability to observe certain events. +For example, when configuring a circuit breaker, it is possible to register a callback for circuit breaker state changes or for a situation when an open circuit breaker prevents an invocation. +When configuring a timeout, it is possible to register a callback for when the invocation times out, etc. etc. +For example: + +[source,java] +---- +private static final Guard GUARD = Guard.create() + .withTimeout().duration(5, ChronoUnit.SECONDS).onTimeout(() -> ...).done() // <1> + .build(); +---- + +<1> The `onTimeout` method takes a `Runnable` that will later be executed whenever an invocation guarded by `GUARD` times out. + +All event listeners registered like this must run quickly and must not throw exceptions. + +== Configuration + +As mentioned above, except of `smallrye.faulttolerance.enabled` / `MP_Fault_Tolerance_NonFallback_Enabled`, there is no support for external configuration of fault tolerance strategies. +This may change in the future, though possibly only in the CDI implementation. + +== Metrics + +The programmatic API is integrated with metrics. +All metrics, as described xref:reference/metrics.adoc[in the Metrics reference guide] and the linked guides, are supported. +The only difference is the value of the `method` tag. +With the programmatic API, the `method` tag will be set to the _description_ of the guarded operation, provided on the `Guard` builder. + +[source,java] +---- +private static final Guard GUARD = Guard.create() + .withDescription("hello") // <1> + .withRetry().maxRetries(3).done() + .build(); +---- + +<1> A description of `hello` is set, it will be used as a value of the `method` tag in all metrics. + +It is possible to create multiple `Guard` objects with the same description. +In this case, it won't be possible to distinguish the different `Guard` objects in metrics; their values will be aggregated. + +If no description is provided, metrics are not emitted. + +== Differences to the Specification + +`Guard` and `TypedGuard` have the following differences to standard MicroProfile Fault Tolerance: + +* asynchronous actions of type `java.util.concurrent.Future` are not supported; +* the fallback, circuit breaker and retry strategies always inspect the cause chain of exceptions, following the behavior of SmallRye Fault Tolerance in the non-compatible mode. + +== Kotlin `suspend` Functions + +The `Guard` and `TypedGuard` APIs do not support Kotlin `suspend` functions at the moment. + +== Integration Concerns + +Integration concerns, which are particularly interesting for users of the standalone implementation, are xref:integration/programmatic-api.adoc[described] in the integration section. + +[[migration_from_faulttolerance]] +== Migration from `FaultTolerance` + +The 1st version of the programmatic API had the `FaultTolerance` interface. +That interface was removed in {smallrye-fault-tolerance} 7.0. +The replacement are the `Guard` and `TypedGuard` types. + +When migrating, one first has to decide which type to use as a replacement. +The `TypedGuard` is much closer to the `FaultTolerance` type, so that is the easiest way to migrate. +There are some differences still: + +- the `create()` static method now takes a parameter that expresses the guarded action type, either as a `Class` or as a `TypeLiteral`; +- there are no `cast()` and `castAsync()` methods, because `TypedGuard` only guards actions of a single type; +- there is no support for guarding or adapting ``Runnable``s, only ``Callable``s and ``Supplier``s are supported. + As a replacement for `Runnable`, a `Supplier` (or `Supplier>` etc.) can be used. + +For example, these usages of `FaultTolerance`: + +[source,java] +---- +static final FaultTolerance FT1 = FaultTolerance.create() + .withDescription("ft1") + .withRetry().maxRetries(3).done() + .withFallback().handler(() -> "fallback").done() + .build(); + +static final FaultTolerance> FT2 = FaultTolerance.createAsync() + .withDescription("ft2") + .withBulkhead().limit(5).queueSize(100).done() + .withTimeout().duration(3, ChronoUnit.SECONDS).done() + .build(); +---- + +can be rewritten to: + +[source,java] +---- +static final TypedGuard GUARD1 = TypedGuard.create(String.class) + .withDescription("ft1") + .withRetry().maxRetries(3).done() + .withFallback().handler(() -> "fallback").done() + .build(); + +static final TypedGuard> GUARD2 = TypedGuard.create( + new TypeLiteral>() {}) + .withDescription("ft2") + .withBulkhead().limit(5).queueSize(100).done() + .withTimeout().duration(3, ChronoUnit.SECONDS).done() + .build(); +---- + +After creating a `TypedGuard`, there's no change in how it's used. +The methods `call()`, `get()`, `adaptCallable()` and `adaptSupplier()` have the same signatures as before. + +`TypedGuard` is the only possible replacement if you need fallback. +If you don't need fallback, and especially if you need to guard actions of multiple types, `Guard` is a better choice. + +The differences are: + +- `Guard` has no type parameter (is not generic) and does not allow defining fallback; +- there are no `cast()` and `castAsync()` methods, because they are no longer needed; +- there is no support for guarding or adapting ``Runnable``s, only ``Callable``s and ``Supplier``s are supported. + As a replacement for `Runnable`, a `Supplier` (or `Supplier>` etc.) can be used. + +For example, these usages of `FaultTolerance`: + +[source,java] +---- +static final FaultTolerance FT1 = FaultTolerance.create() + .withDescription("ft1") + .withRetry().maxRetries(3).done() + .withCircuitBreaker().done() + .build(); + +static final FaultTolerance> FT2 = FaultTolerance.createAsync() + .withDescription("ft2") + .withBulkhead().limit(5).queueSize(100).done() + .withTimeout().duration(3, ChronoUnit.SECONDS).done() + .build(); +---- + +can be rewritten to: + +[source,java] +---- +static final Guard GUARD1 = Guard.create() + .withDescription("ft1") + .withRetry().maxRetries(3).done() + .withCircuitBreaker().done() + .build(); + +static final Guard GUARD2 = Guard.create() + .withDescription("ft2") + .withBulkhead().limit(5).queueSize(100).done() + .withTimeout().duration(3, ChronoUnit.SECONDS).done() + .build(); +---- + +Now, as mentioned above, `Guard` is used slightly differently than `FaultTolerance`. +The methods `call()`, `get()`, `adaptCallable()` and `adaptSupplier()` take an extra parameter that expresses the type of the guarded action, again either as a `Class` or as a `TypeLiteral`. + +For example, these usages of `FaultTolerance`: + +[source,java] +---- +String result1 = FT1.call(() -> externalService.hello()); +CompletionStage result2 = FT2.get(() -> externalService.helloAsync()); + +Callable callable = FT1.adaptCallable( + () -> externalService.hello()); +Supplier> supplier = FT2.adaptSupplier( + () -> externalService.helloAsync()); +---- + +can be rewritten to: + +[source,java] +---- +String result1 = GUARD1.call(() -> externalService.hello(), String.class); +CompletionStage result2 = GUARD2.get(() -> externalService.helloAsync(), + new TypeLiteral>() {}); + +Callable callable = GUARD1.adaptCallable( + () -> externalService.hello(), String.class); +Supplier> supplier = GUARD2.adaptSupplier( + () -> externalService.helloAsync(), + new TypeLiteral>() {}); +---- diff --git a/content/docs/smallrye-fault-tolerance/main/reference/rate-limit.adoc b/content/docs/smallrye-fault-tolerance/main/reference/rate-limit.adoc new file mode 100644 index 000000000..abb823f0f --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/main/reference/rate-limit.adoc @@ -0,0 +1,148 @@ += Rate Limit +:version-root: ../ +include::{version-root}_attributes.adoc[] + +For introduction, see xref:howto/rate-limit.adoc[How to Limit Execution Rate]. + +include::{partialsdir}/srye-feature.adoc[] + +== Description + +Rate limit enforces a maximum number of permitted invocations in a time window of some length. +For example, with a rate limit, one can make sure that a method may only be called 50 times per minute. +Invocations that would exceed the limit are rejected with an exception of type `RateLimitException`. + +Additionally, it is possible to define minimum spacing between invocations. +For example, with minimum spacing of 1 second, if a second invocation happens 500 millis after the first, it is rejected even if the limit would not be exceeded yet. + +Rate limit is superficially similar to a bulkhead (concurrency limit), but is in fact quite different. +Bulkhead limits the number of executions happening concurrently at any point in time. +Rate limit limits the number of executions in a time window of some length, without considering concurrency. + +A method or a class can be annotated with `@RateLimit`, which means the method or the methods in the class will apply the rate limit strategy. +The previous example with 50 maximum invocations per minute and minimum spacing of 1 second would look like this: + +[source,java] +---- +@RateLimit(value = 50, + window = 1, windowUnit = ChronoUnit.MINUTES, + minSpacing = 1, minSpacingUnit = ChronoUnit.SECONDS) +public void doSomething() { + ... +} +---- + +The `RateLimitException` class has a method `getRetryAfterMillis()` which returns the number of milliseconds after which the user may retry the rejected invocation. +Retrying sooner is guaranteed to be rejected again. +Note that this information is accurate only at the time the invocation is rejected. +It may be invalidated by any subsequent or concurrent invocations, so there is no guarantee that a retry attempt after the given number of milliseconds will in fact be permitted. + +=== Lifecycle + +Rate limit needs to maintain some state between invocations: the number of recent invocations, the time stamp of last invocation, and so on. +This state is a singleton, irrespective of the lifecycle of the bean that uses the `@RateLimit` annotation. + +More specifically, the rate limit state is uniquely identified by the combination of the bean class (`java.lang.Class`) and the method object (`java.lang.reflect.Method`) representing the guarded method. + +For example, if there’s a guarded method `doWork` on a bean which is `@RequestScoped`, each request will have its own instance of the bean, but all invocations of `doWork` will share the same rate limit state. + +[[interactions]] +== Interactions with Other Strategies + +See xref:howto/multiple.adoc[How to Use Multiple Strategies] for an overview of how fault tolerance strategies are nested. + +If `@Fallback` is used with `@RateLimit`, the fallback method or handler may be invoked if a `RateLimitException` is thrown, depending on the fallback configuration. + +If `@Retry` is used with `@RateLimit`, each retry attempt is processed by the rate limit as an independent invocation. +If `RateLimitException` is thrown, the execution may be retried, depending on how retry is configured. + +If `@CircuitBreaker` is used with `@RateLimit`, the circuit breaker is checked before enforcing the rate limit. +If rate limiting results in `RateLimitException`, this may be counted as a failure, depending on how the circuit breaker is configured. + +[[configuration]] +== Configuration + +There are 6 configuration options, corresponding to the 6 members of the `@RateLimit` annotation. + +=== `value` + +Type: `int` + +Default: `100` + +The limit of maximum invocations to be permitted in the time window. + +=== `window` + `windowUnit` + +Type: `long` + `ChronoUnit` + +Default: `1 second` + +The length of the time window. + +=== `minSpacing` + `minSpacingUnit` + +Type: `long` + `ChronoUnit` + +Default: `0 seconds` + +The minimum spacing between two consecutive invocations. + +=== `type` + +Type: `RateLimitType` + +Default: `RateLimitType.FIXED` + +The type of time window used for rate limiting. +There are 3 types of time windows used for rate limiting: _fixed_, _rolling_ and _smooth_. + +Fixed time windows are a result of dividing time into non-overlapping intervals of given length. +The invocation limit is enforced for each interval independently. +This means that short bursts of invocations occuring near the time window boundaries may temporarily exceed the configured rate limit. +This kind of rate limiting is also called _fixed window_ rate limiting. + +Rolling time windows enforce the limit continuously, instead of dividing time into independent intervals. +The invocation limit is enforced for all possible time intervals of given length, regardless of overlap. +This is more precise, but requires more memory and may be slower. +This kind of rate limiting is also called _sliding log_ rate limiting. + +Smooth time windows enforce a uniform distribution of invocations under a rate calculated from given time window length and given limit. +If recent rate of invocations is under the limit, a subsequent burst of invocations is allowed during a shorter time span, but the calculated rate is never exceeded. +This kind of rate limiting is also called _token bucket_ or _leaky bucket (as a meter)_ rate limiting, with the additional property that all work units are considered to have the same size. + +With fixed and rolling time windows, rejected invocations always count towards the limit, so if a caller continuously invokes the guarded method faster than the configuration allows, all invocations are rejected until the caller slows down. +With smooth time windows, rejected invocations do not count towards the recent rate of invocations. + +Example usage: + +[source,java] +---- +@RateLimit(value = 50, + window = 1, windowUnit = ChronoUnit.MINUTES, + minSpacing = 1, minSpacingUnit = ChronoUnit.SECONDS, + type = RateLimitType.ROLLING) +public void doSomething() { + ... +} +---- + +[[metrics]] +== Metrics + +Rate limit exposes the following metrics: + +[cols="1,5"] +|=== +| Name | `ft.ratelimit.calls.total` +| Type +a| * OpenTelemetry: `LongCounter` +* Micrometer: `Counter` +| Unit | None +| Description | The number of times the rate limit logic was run. This is usually once per method call, but may be zero times if the circuit breaker prevented execution or more than once if the method call was retried. +| Tags +a| * `method` - the fully qualified method name +* `rateLimitResult` = `[permitted\|rejected]` - whether the rate limit permitted the method call +|=== + +See xref:reference/metrics.adoc[the Metrics reference guide] for general metrics information. diff --git a/content/docs/smallrye-fault-tolerance/main/reference/retry.adoc b/content/docs/smallrye-fault-tolerance/main/reference/retry.adoc new file mode 100644 index 000000000..0c3bf8dc4 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/main/reference/retry.adoc @@ -0,0 +1,350 @@ += Retry +:version-root: ../ +include::{version-root}_attributes.adoc[] + +For introduction, see xref:howto/retry.adoc[How to Retry on Failures]. + +== Description + +If a guarded method throws an exception, retry will swallow that exception and call the method again. + +More specifically: + +. If the guarded method returns normally, its return value is returned. +. Otherwise, if the guarded method throws an exception whose type is assignable to any of the `abortOn` types (see <>), retrying is aborted and the exception is rethrown. +. Otherwise, if the guarded method throws an exception whose type is assignable to any of the `retryOn` types (see <>), the method call is retried. +. Otherwise, the exception is rethrown. + +[[interactions]] +== Interactions with Other Strategies + +See xref:howto/multiple.adoc[How to Use Multiple Strategies] for an overview of how fault tolerance strategies are nested. + +If `@Fallback` is used with `@Retry`, the fallback method or handler may be invoked if all retry attempts failed, depending on the fallback configuration. + +[[configuration]] +== Configuration + +There are 9 configuration options, corresponding to the 9 members of the `@Retry` annotation. + +=== `maxRetries` + +Type: `int` + +Default: `3` + +The maximum number of retries. +`-1` means the maximum number of retries is unlimited. + +=== `delay` + `delayUnit` + +Type: `long` + `ChronoUnit` + +Default: `0 millis` + +The delay between retry attempts. + +=== `maxDuration` + `durationUnit` + +Type: `long` + `ChronoUnit` + +Default: `180_000 millis`, or 3 minutes + +The maximum duration for which retries will be attempted. + +:time-value-name: maxDuration +:time-unit-bad-name: durationUnit +:time-unit-good-name: maxDurationUnit +include::{partialsdir}/time-unit-inconsistency.adoc[] + +=== `jitter` + `jitterDelayUnit` + +Type: `long` + `ChronoUnit` + +Default: `200 millis` + +The maximum jitter to adjust the delay between retries. +If set, the delay before each retry attempt will be computed as follows: + +. Generate random number from the closed interval `[-jitter, jitter]`. +. Add the generated random number to the configured `delay`. +. If the result is by any chance negative, use `0`. + +:time-value-name: jitter +:time-unit-bad-name: jitterDelayUnit +:time-unit-good-name: jitterUnit +include::{partialsdir}/time-unit-inconsistency.adoc[] + +[[retry-on]] +=== `retryOn` + +Type: `Class[]` + +Default: `{Exception.class}` + +Set of exception types that trigger a retry. + +[[abort-on]] +=== `abortOn` + +Type: `Class[]` + +Default: `{}` + +Set of exception types that abort retrying. +This configuration takes precedence over `retryOn`. + +[[metrics]] +== Metrics + +Retry exposes the following metrics: + +[cols="1,5"] +|=== +| Name | `ft.retry.calls.total` +| Type +a| * OpenTelemetry: `LongCounter` +* Micrometer: `Counter` +| Unit | None +| Description | The number of times the retry logic was run. This will always be once per method call. +| Tags +a| * `method` - the fully qualified method name +* `retried` = `[true\|false]` - whether any retries occurred +* `retryResult` = `[valueReturned\|exceptionNotRetryable\|maxRetriesReached\|maxDurationReached]` - the reason that last attempt to call the method was not retried +|=== + +[cols="1,5"] +|=== +| Name | `ft.retry.retries.total` +| Type +a| * OpenTelemetry: `LongCounter` +* Micrometer: `Counter` +| Unit | None +| Description | The number of times the method was retried +| Tags +a| * `method` - the fully qualified method name +|=== + +See xref:reference/metrics.adoc[the Metrics reference guide] for general metrics information. + +== Extra Features + +=== Backoff Strategies for `@Retry` + +include::{partialsdir}/srye-feature.adoc[] + +When retrying failed operations, it is often useful to make a delay between retry attempts. +This delay is also called "backoff". +The `@Retry` annotation in {microprofile-fault-tolerance} supports a single backoff strategy: constant. +That is, the delay between all retry attempts is identical (except for a random jitter). + +{smallrye-fault-tolerance} offers 3 annotations to specify a different backoff strategy: + +* `@ExponentialBackoff` +* `@FibonacciBackoff` +* `@CustomBackoff` + +One of these annotations may be present on any program element (method or class) that also has the `@Retry` annotation. +For example: + +[source,java] +---- +package com.example; + +@ApplicationScoped +public class MyService { + @Retry + @ExponentialBackoff + public void hello() { + ... + } +} +---- + +It is an error to add a backoff annotation to a program element that doesn't have `@Retry` (e.g. add `@Retry` on a class and `@ExponentialBackoff` on a method). +It is also an error to add more than one of these annotations to the same program element. + +When any one of these annotations is present, it modifies the behavior specified by the `@Retry` annotation. +The new behavior is as follows: + +For `@ExponentialBackoff`, the delays between retry attempts grow exponentially, using a defined `factor`. +By default, the `factor` is 2, so each delay is 2 * the previous delay. +For example, if the initial delay (specified by `@Retry`) is 1 second, then the second delay is 2 seconds, third delay is 4 seconds, fourth delay is 8 seconds etc. +It is possible to define a `maxDelay`, so that this growth has a limit. + +For `@FibonacciBackoff`, the delays between retry attempts grow per the Fibonacci sequence. +For example, if the initial delay (specified by `@Retry`) is 1 second, then the second delay is 2 seconds, third delay is 3 seconds, fourth delay is 5 seconds etc. +It is possible to define a `maxDelay`, so that this growth has a limit. + +Both `@ExponentialBackoff` and `@FibonacciBackoff` also apply jitter, exactly like plain `@Retry`. + +Also, since `@Retry` has a default `maxDuration` of 3 minutes and default `maxRetries` of 3, both `@ExponentialBackoff` and `@FibonacciBackoff` define a `maxDelay` of 1 minute. +If we redefine `maxRetries` to a much higher value, and the guarded method keeps failing, the delay would eventually become higher than 1 minute. +In that case, it will be limited to 1 minute. +Of course, `maxDelay` can be configured. +If set to `0`, there's no limit, and the delays will grow without bounds. + +For `@CustomBackoff`, computing the delays between retry attempts is delegated to a specified implementation of `CustomBackoffStrategy`. +This is an advanced option. + +For more information about these backoff strategies, see the javadoc of the annotations. + +=== Predicate-Based `@Retry` + +include::{partialsdir}/srye-feature.adoc[] + +By default, an operation is retried only if it fails and the exception is assignable to one of the classes defined in `@Retry.retryOn` (and not assignable to any of the classes defined in `@Retry.abortOn`). +With the `@RetryWhen` annotation, it is possible to instead define a custom predicate for the exception, as well as a predicate for the result. + +The `@RetryWhen` annotation may be present on any program element (method or class) that also has the `@Retry` annotation. +For example: + +[source,java] +---- +package com.example; + +@ApplicationScoped +public class MyService { + @Retry + @RetryWhen( + result = IsNull.class, // <1> + exception = IsRuntimeException.class // <2> + ) + public String hello() { + ... + } + + public static final class IsNull implements Predicate { + @Override + public boolean test(Object o) { + return o == null; + } + } + + public static final class IsRuntimeException implements Predicate { + @Override + public boolean test(Throwable throwable) { + return throwable instanceof RuntimeException; + } + } +} +---- + +<1> When the method returns `null`, it will be retried. +<2> When the method throws a `RuntimeException`, it will be retried. + +All other results are considered expected and are not retried. + +It is an error to specify `@RetryWhen.exception` if `@Retry.retryOn` / `@Retry.abortOn` is specified. +Specifying `@RetryWhen.result` together with `@Retry.retryOn` / `@Retry.abortOn` is possible, although perhaps not the best idea. + +It is an error to add a `@RetryWhen` annotation to a program element that doesn't have `@Retry` (e.g. add `@Retry` on a class and `@RetryWhen` on a method). + +For more information about `@RetryWhen`, see the javadoc of the annotation. + +=== Before Retry Actions + +include::{partialsdir}/srye-feature.adoc[] + +The `@BeforeRetry` annotation supports specifying an action that should be performed before any retry. +The action will _not_ be performed before the initial attempt. + +The `@BeforeRetry` annotation may be present on any program element (method or class) that also has the `@Retry` annotation. +For example: + +[source,java] +---- +package com.example; + +@ApplicationScoped +public class MyService { + @Retry + @BeforeRetry(methodName = "beforeRetry") // <1> + public String hello() { + ... + } + + public void beforeRetry() { // <2> + ... + } +} +---- + +<1> Specifies that before retrying, the `beforeRetry` method should be called. +<2> The method must return `void` and have no parameters. + +Alternatively, it is possible to specify a class that is an implementation of the `BeforeRetryHandler` interface: + +[source,java] +---- +package com.example; + +@ApplicationScoped +public class MyService { + @Retry + @BeforeRetry(MyBeforeRetry.class) // <1> + public String hello() { + ... + } + + public static class MyBeforeRetry implements BeforeRetryHandler { + public void handle(ExecutionContext context) { + ... + } + } +} +---- + +<1> Specifies that the `MyBeforeRetry` handler should be called before retrying. + +It is only possible to specify one option; specifying both `value` and `methodName` leads to a deployment problem. + +Note that the before retry action must fast and non-blocking (i.e., it must not do any IO or long-running computations) and must not throw an exception. + +[[inspecting-exception-cause-chains]] +=== Inspecting Exception Cause Chains + +include::{partialsdir}/srye-feature.adoc[] + +include::{partialsdir}/non-compat.adoc[] + +The `@Retry` annotation can specify that certain exceptions should be treated as failures (`retryOn`) and others as successes (`abortOn`). +The specification limits this to inspecting the actual exception that was thrown. +However, in many cases, exceptions are wrapped and the exception the user wants to decide on is only present in the cause chain. + +For that reason, in the non-compatible mode, if the actual thrown exception isn't known failure or known success, {smallrye-fault-tolerance} inspects the cause chain. +To be specific, in case a `@Retry` method throws an exception, the decision process is: + +1. If the `abortOn` exceptions are not default and the exception is assignable to one of the `abortOn` exceptions, retry is aborted and the exception is rethrown. +2. Otherwise, if the `retryOn` exceptions are not default and the exception is assignable to one of the `retryOn` exceptions, retry is attempted. +3. Otherwise, if the exception is assignable to one of the `abortOn` exceptions or its cause chain contains an exception assignable to one of the `abortOn` exceptions, retry is aborted and the exception is rethrown. +4. Otherwise, if the exception is assignable to one of the `retryOn` exceptions or its cause chain contains an exception assignable to one of the `retryOn` exceptions, retry is attempted. +5. Otherwise, the exception is rethrown. + +For example: + +[source,java] +---- +@Retry(maxRetries = 5, + abortOn = ExpectedOutcomeException.class, // <1> + retryOn = IOException.class) // <2> +public Result doSomething() { + ... +} +---- + +<1> If `doSomething` throws an `ExpectedOutcomeException`, or a `WrapperException` whose cause is `ExpectedOutcomeException`, retry is aborted and the exception is thrown. +<2> If `doSomething` throws an `IOException`, or a `WrapperException` whose cause is `IOException`, retry is attempted. + +[source,java] +---- +@Retry(maxRetries = 5, + abortOn = ExpectedOutcomeException.class) // <1> <2> +public Result doSomething() { + ... +} +---- + +<1> If `doSomething` throws an `ExpectedOutcomeException`, or a `WrapperException` whose cause is `ExpectedOutcomeException`, retry is aborted and the exception is thrown. +<2> There's no `retryOn`, so the 2nd step in the algorithm above is skipped. +This is what turns the `WrapperException` whose cause is `ExpectedOutcomeException` into an aborted retry. diff --git a/content/docs/smallrye-fault-tolerance/main/reference/reusable.adoc b/content/docs/smallrye-fault-tolerance/main/reference/reusable.adoc new file mode 100644 index 000000000..9e7d97d04 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/main/reference/reusable.adoc @@ -0,0 +1,244 @@ += Reusable Fault Tolerance +:version-root: ../ +include::{version-root}_attributes.adoc[] + +include::{partialsdir}/srye-feature.adoc[] + +The declarative, annotation-based API of {microprofile-fault-tolerance} doesn't allow sharing configuration of fault tolerance strategies across multiple classes. +In a single class, the configuration may be shared across all methods by putting the annotations on the class instead of individual methods, but even then, _stateful_ fault tolerance strategies are not shared. +Each method has its own bulkhead, circuit breaker and/or rate limit, which is often not what you want. + +The xref:reference/programmatic-api.adoc[programmatic API] of {smallrye-fault-tolerance} allows using a single `Guard` or `TypedGuard` object to guard multiple disparate actions, which allows reuse and state sharing. +It is possible to use a programmatically constructed `Guard` or `TypedGuard` object declaratively, using the `@ApplyGuard` annotation. + +To be able to do that, we need a bean of type `Guard` (or `TypedGuard`) with the `@Identifier` qualifier: + +[source,java] +---- +@ApplicationScoped +public class PreconfiguredFaultTolerance { + @Produces + @Identifier("my-fault-tolerance") + public static final Guard GUARD = Guard.create() + .withRetry().maxRetries(2).done() + .withTimeout().done() + .build(); +} +---- + +See the xref:reference/programmatic-api.adoc[programmatic API] documentation for more information about creating the `Guard` or `TypedGuard` instance. + +It is customary to create the bean by declaring a `static` producer field, just like in the previous example. + +Once we have that, we can apply `my-fault-tolerance` to any method: + +[source,java] +---- +@ApplicationScoped +public class MyService { + @ApplyGuard("my-fault-tolerance") + public String doSomething() { + ... + } + + @ApplyGuard("my-fault-tolerance") + public CompletionStage doSomethingElse() { + ... + } +} +---- + +== Defining Fallback + +Note that it is not possible to define a fallback on `Guard`, because fallback is tied to the action type. +It is possible to define a fallback on `TypedGuard`, because it can only be used to guard methods with a single return type, equal to the type the `TypedGuard` was created with. + +However, the `@ApplyGuard` annotation pays attention to the `@Fallback` annotation. +If `@Fallback` is defined, it is used both by `Guard` and `TypedGuard` instances, and it overrides the possible fallback defined on the `TypedGuard`. + +== Defining Thread Offload + +Both `Guard` and `TypedGuard` allow enabling or disabling thread offload. +This is ignored by `@ApplyGuard` if the annotated method is asynchronous as determined from the method signature and possible annotations (`@Asynchronous` and `@AsynchronousNonBlocking`). +See xref:reference/asynchronous.adoc[] for more information about how that determination works. + +The thread offload configuration of `Guard` or `TypedGuard` is only honored when the method cannot be determined to be asynchronous, but it still declares an asynchronous return type. + +.Compatible Mode +**** +In the compatible mode, methods are determined to be asynchronous when they are annotated `@Asynchronous` or `@AsynchronousNonBlocking` and they declare an asynchronous return type. +For such methods, the `Guard` / `TypedGuard` thread offload configuration does not apply. + +For methods that declare an asynchronous return type but are not annotated with `@Asynchronous` or `@AsynchronousNonBlocking`, the configuration on `Guard` / `TypedGuard` is honored. +**** + +.Non-compatible Mode +**** +In the non-compatible mode, methods are determined to be asynchronous whenever they declare an asynchronous return type. +The `@Asynchronous` and `@AsynchronousNonBlocking` only affect whether the invocation of that method is offloaded to an extra thread. +If none of these annotations is present, no thread offload happens. + +In other words, in non-compatible mode, the `Guard` / `TypedGuard` thread offload configuration never applies. + +See xref:reference/non-compat.adoc[] for more information about the non-compatible mode. +**** + +== Configuration + +Even though the programmatic API of `Guard` and `TypedGuard` does not support configuration, the declarative API of `@ApplyGuard` does. + +The configuration closely resembles xref:reference/config.adoc[the configuration of the declarative API], where instead of the `` or `/`, you use the identifier that's used in the `@Identifier` qualifier and in the `@ApplyGuard` annotation. + +CAUTION: When configuring the `Guard` or `TypedGuard` using the following configuration properties, it is recommended to never use the `[Typed]Guard` programmatically. +The reason is that configuration is applied on the first invocation, and if that invocation is programmatic, the configuration key is unknown. + +For example, let's assume the following preconfigured `Guard` object: + +[source,java] +---- +@ApplicationScoped +public class PreconfiguredFaultTolerance { + @Produces + @Identifier("my-fault-tolerance") + public static final Guard GUARD = Guard.create() + .withRetry().maxRetries(2).done() + .withTimeout().done() + .build(); +} +---- + +This `Guard` supports retry and timeout; any attempt to configure other strategies will be ignored. + +To reconfigure the number of maximum retries, the following configuration properties may be used: + +[source,properties] +---- +smallrye.faulttolerance."my-fault-tolerance".retry.max-retries=5 + +# alternatively, a specification-defined property can be used +my-fault-tolerance/Retry/maxRetries=5 +---- + +Global configuration that applies to all usages of retry is also respected by `@ApplyGuard`: + +[source,properties] +---- +smallrye.faulttolerance.global.retry.max-retries=5 + +# alternatively, a specification-defined property can be used +Retry/maxRetries=5 +---- + +Note that the configuration keys follow the configuration of the declarative API, even if the methods in the programmatic API builders are named differently. +For example, to set the rate limit in the programmatic API, the `RateLimitBuilder` uses a method called `limit()`, while the corresponding annotation member of `@RateLimit` is named `value()`. +The configuration key in both cases is `rate-limit.value` (or `RateLimit/value`). + +=== Disabling Fault Tolerance + +Configuration properties for disabling individual strategies also work: + +[source,properties] +---- +smallrye.faulttolerance."my-fault-tolerance".retry.enabled=false + +# alternatively, a specification-defined property can be used +my-fault-tolerance/Retry/enabled=false +---- + +Or globally: + +[source,properties] +---- +smallrye.faulttolerance.global.retry.enabled=false + +# alternatively, a specification-defined property can be used +Retry/enabled=false +---- + +=== Supported Strategies + +The following fault tolerance strategies, when defined on a `Guard` (or `TypedGuard`) and used through `@ApplyGuard`, may be configured: + +* bulkhead (except of `BulkheadBuilder.enableSynchronousQueueing()`) +* circuit breaker +* rate limit +* retry, including exponential backoff and Fibonacci backoff +* timeout + +In the programmatic API, the following strategies are not built declaratively, and so their configuration is ignored: + +* fallback +* circuit breaker name (cannot be configured in the declarative API either) +* certain parts of retry: custom backoff, before retry action and retry predicates +* thread offload + +== Metrics + +Methods annotated `@ApplyGuard` gather metrics similarly to methods annotated with {microprofile-fault-tolerance} annotations. +That is, each method gets its own metrics, with the `method` tag being `.`. + +At the same time, state is still shared. +All methods annotated `@ApplyGuard` share the same bulkhead, circuit breaker and/or rate limit. + +If the `Guard` or `TypedGuard` object used for `@ApplyGuard` is also used xref:reference/programmatic-api.adoc[programmatically], that usage is coalesced in metrics (where the `method` tag is set to the description). + +== Differences to the Specification + +`@ApplyGuard` has the same differences to standard MicroProfile Fault Tolerance as `Guard` / `TypedGuard`: + +* asynchronous actions of type `java.util.concurrent.Future` are not supported; +* the fallback, circuit breaker and retry strategies always inspect the cause chain of exceptions, following the behavior of SmallRye Fault Tolerance in the non-compatible mode. + +== Kotlin `suspend` Functions + +Even though the programmatic API of `Guard` and `TypedGuard` does not support Kotlin `suspend` functions, the declarative API of `@ApplyGuard` does. +When the guard is a `Guard`, no restrictions apply. + +When the guard is a `TypedGuard`, however, its type must be a synchronous return type of the `suspend` function. +For example, when the `suspend` function is declared to return a `String` asynchronously: + +[source,kotlin] +---- +@ApplyGuard("my-fault-tolerance") +@Fallback(fallbackMethod = "fallback") +suspend fun hello(): String { + delay(100) + throw IllegalArgumentException() +} +---- + +The `TypedGuard` must be declared to guard actions of type `String`: + +[source,kotlin] +---- +@Produces +@Identifier("my-fault-tolerance") +val GUARD = TypedGuard.create(String::class.java) + .withRetry().maxRetries(2).done() + .withFallback().handler(Supplier { "fallback" }).done() + .build() +---- + +This means that a possible fallback declared on the `TypedGuard` must be synchronous; it cannot be a `suspend` lambda. + +The `@Fallback` method, if declared, must have a matching signature and so must be a `suspend` function: + +[source,kotlin] +---- +suspend fun fallback(): String { + delay(100) + return "fallback" +} +---- + +[[migration_from_applyfaulttolerance]] +== Migration from `@ApplyFaultTolerance` + +The 1st version of the programmatic API had the `@ApplyFaultTolerance` annotation. +That annotation was removed in {smallrye-fault-tolerance} 7.0. + +To migrate, replace `@ApplyFaultTolerance` with `@ApplyGuard` and change the `FaultTolerance<>` producers to produce `Guard` or `TypedGuard<>`. +See xref:reference/programmatic-api.adoc#migration_from_faulttolerance[the programmatic API migration guide] for more details about that. + +Note that it is not possible to define both `Guard` and `TypedGuard<>` with the same identifier; that leads to a deployment problem. +Therefore, for each producer of `FaultTolerance<>`, you have to decide whether the replacement should be `Guard` or `TypedGuard<>`. diff --git a/content/docs/smallrye-fault-tolerance/main/reference/timeout.adoc b/content/docs/smallrye-fault-tolerance/main/reference/timeout.adoc new file mode 100644 index 000000000..183009bd7 --- /dev/null +++ b/content/docs/smallrye-fault-tolerance/main/reference/timeout.adoc @@ -0,0 +1,78 @@ += Timeout +:version-root: ../ +include::{version-root}_attributes.adoc[] + +For introduction, see xref:howto/timeout.adoc[How to Watch for Timeouts]. + +== Description + +If a guarded method takes too long, the caller will get a `TimeoutException`. + +If the guarded method is not `@Asynchronous` or `@AsynchronousNonBlocking`, the thread executing the method is interrupted. +Note that interruption requires cooperation from the executing code (for example, it must not swallow `InterruptedException`). +In other words, it is not guaranteed that interrupting the method will actually make it stop. + +If the guarded method is `@Asynchronous` or `@AsynchronousNonBlocking`, timeout is always signalled correctly, but there is no cancellation. +Even after timeout, the asynchronous action will keep running; its eventual result is just ignored. + +[[interactions]] +== Interactions with Other Strategies + +See xref:howto/multiple.adoc[How to Use Multiple Strategies] for an overview of how fault tolerance strategies are nested. + +If `@Fallback` is used with `@Timeout`, the fallback method or handler may be invoked if a `TimeoutException` is thrown, depending on the fallback configuration. + +If `@Retry` is used with `@Timeout`, each retry attempt is processed by the timeout as an independent invocation. +If `TimeoutException` is thrown, the execution may be retried, depending on how retry is configured. + +If `@CircuitBreaker` is used with `@Timeout`, the circuit breaker is checked before enforcing the timeout. +If enforcing the timeout results in `TimeoutException`, this may be counted as a failure, depending on how the circuit breaker is configured. + +If `@RateLimit` is used with `@Timeout`, the rate limit is enforced before enforcing the timeout. + +[[configuration]] +== Configuration + +There are 2 configuration options, corresponding to the 2 members of the `@Timeout` annotation. + +=== `value` + `unit` + +Type: `long` + `ChronoUnit` + +Default: `1000 millis`, or 1 second + +The timeout to enforce. + +[[metrics]] +== Metrics + +Timeout exposes the following metrics: + +[cols="1,5"] +|=== +| Name | `ft.timeout.calls.total` +| Type +a| * OpenTelemetry: `LongCounter` +* Micrometer: `Counter` +| Unit | None +| Description | The number of times the timeout logic was run. This is usually once per method call, but may be zero times if the circuit breaker or rate limit prevents execution or more than once if the method is retried. +| Tags +a| * `method` - the fully qualified method name +* `timedOut` = `[true\|false]` - whether the method call timed out +|=== + +[cols="1,5"] +|=== +| Name | `ft.timeout.executionDuration` +| Type +a| * OpenTelemetry: `DoubleHistogram` with explicit bucket boundaries `[0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10]` +* Micrometer: `Timer` +| Unit +a| * OpenTelemetry: seconds +* Micrometer: nanoseconds +| Description | Histogram of execution times for the method +| Tags +a| * `method` - the fully qualified method name +|=== + +See xref:reference/metrics.adoc[the Metrics reference guide] for general metrics information. diff --git a/content/docs/smallrye-graphql/1/_attributes.adoc b/content/docs/smallrye-graphql/1/_attributes.adoc new file mode 100644 index 000000000..cee3f4aac --- /dev/null +++ b/content/docs/smallrye-graphql/1/_attributes.adoc @@ -0,0 +1,20 @@ +:project-version: 1.9.5 +// Attributes shared by every page. smallrye.io overrides the ones it needs when it publishes. +// Pages in subdirectories set :version-root: (../ per level) before including this file. +ifndef::version-root[:version-root:] +:idprefix: +:idseparator: - +:sectanchors: +:examples-dir: {version-root}_examples +:partialsdir: {version-root}_partials +:project-name: SmallRye GraphQL +:javadoc-version: latest +:generated-dir: {version-root}_generated + +// Added by smallrye.io (tools/docs/Docs.java sync) +:javadoc-version: 1.9.5 +:relfileprefix: /docs/smallrye-graphql/1/ +:imagesdir: /docs/smallrye-graphql/1 +:docs-project: smallrye-graphql +:docs-version: 1 +:docs-ref: 1.9.5 diff --git a/content/docs/smallrye-graphql/1/_examples/examples/dynamicclient/MyClientUsage.java b/content/docs/smallrye-graphql/1/_examples/examples/dynamicclient/MyClientUsage.java new file mode 100644 index 000000000..75341f569 --- /dev/null +++ b/content/docs/smallrye-graphql/1/_examples/examples/dynamicclient/MyClientUsage.java @@ -0,0 +1,37 @@ +package examples.dynamicclient; + +import examples.typesafeclient.SuperHero; +import io.smallrye.graphql.client.GraphQLClient; +import io.smallrye.graphql.client.Response; +import io.smallrye.graphql.client.core.Document; +import io.smallrye.graphql.client.dynamic.api.DynamicGraphQLClient; + +import javax.inject.Inject; +import javax.json.JsonArray; +import java.util.List; +import java.util.concurrent.ExecutionException; + +import static io.smallrye.graphql.client.core.Argument.arg; +import static io.smallrye.graphql.client.core.Argument.args; +import static io.smallrye.graphql.client.core.Document.document; +import static io.smallrye.graphql.client.core.Field.field; +import static io.smallrye.graphql.client.core.Operation.operation; + +public class MyClientUsage { + + @Inject + @GraphQLClient("superHeroes") + DynamicGraphQLClient client; + + public void execute() throws ExecutionException, InterruptedException { + Document document = document( // <1> + operation(field("allHeroesIn", + args(arg("location", "Outer Space")), + field("name"), + field("superPowers")))); + Response response = client.executeSync(document); // <2> + + JsonArray heroesArray = response.getData().getJsonArray("allHeroesIn"); // <3> + List heroes = response.getList(SuperHero.class, "allHeroesIn"); // <4> + } +} diff --git a/content/docs/smallrye-graphql/1/_examples/examples/dynamicclient/MyClientUsageString.java b/content/docs/smallrye-graphql/1/_examples/examples/dynamicclient/MyClientUsageString.java new file mode 100644 index 000000000..efa20efee --- /dev/null +++ b/content/docs/smallrye-graphql/1/_examples/examples/dynamicclient/MyClientUsageString.java @@ -0,0 +1,39 @@ +package examples.dynamicclient; + +import io.smallrye.graphql.client.GraphQLClient; +import io.smallrye.graphql.client.Response; +import io.smallrye.graphql.client.dynamic.api.DynamicGraphQLClient; + +import javax.inject.Inject; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutionException; + +public class MyClientUsageString { + + @Inject + @GraphQLClient("superheroes") + DynamicGraphQLClient client; + + public void execute() throws ExecutionException, InterruptedException { + String queryWithInlineVariables = // <1> + "query {" + + " allHeroesIn(location: \"Outer Space\") {" + + " name" + + " superPowers" + + " }" + + "}"; + Response response = client.executeSync(queryWithInlineVariables); + + String queryWithExtractedVariables = // <2> + "query($loc: String) {" + + " allHeroesIn(location: $loc) {" + + " name" + + " superPowers" + + " }" + + "}"; + Map variables = new HashMap<>(); // <3> + variables.put("loc", "Outer Space"); + Response response2 = client.executeSync(queryWithExtractedVariables, variables); + } +} diff --git a/content/docs/smallrye-graphql/1/_examples/examples/typesafeclient/MyClientUsage.java b/content/docs/smallrye-graphql/1/_examples/examples/typesafeclient/MyClientUsage.java new file mode 100644 index 000000000..2c574e1f2 --- /dev/null +++ b/content/docs/smallrye-graphql/1/_examples/examples/typesafeclient/MyClientUsage.java @@ -0,0 +1,16 @@ +package examples.typesafeclient; + +import javax.inject.Inject; +import java.util.List; + +public class MyClientUsage { + + @Inject + SuperHeroesApi superHeroesApi; + + public void execute() { + List allHeroes = superHeroesApi.allHeroesIn("Outer Space"); + // ... + } + +} diff --git a/content/docs/smallrye-graphql/1/_examples/examples/typesafeclient/SuperHero.java b/content/docs/smallrye-graphql/1/_examples/examples/typesafeclient/SuperHero.java new file mode 100644 index 000000000..7c19a1119 --- /dev/null +++ b/content/docs/smallrye-graphql/1/_examples/examples/typesafeclient/SuperHero.java @@ -0,0 +1,12 @@ +package examples.typesafeclient; + +import java.util.List; + +public class SuperHero { + + private String name; + private List superPowers; + + // plus getters and setters + +} diff --git a/content/docs/smallrye-graphql/1/_examples/examples/typesafeclient/SuperHeroesApi.java b/content/docs/smallrye-graphql/1/_examples/examples/typesafeclient/SuperHeroesApi.java new file mode 100644 index 000000000..0fae71966 --- /dev/null +++ b/content/docs/smallrye-graphql/1/_examples/examples/typesafeclient/SuperHeroesApi.java @@ -0,0 +1,12 @@ +package examples.typesafeclient; + +import io.smallrye.graphql.client.typesafe.api.GraphQLClientApi; + +import java.util.List; + +@GraphQLClientApi +public interface SuperHeroesApi { + + List allHeroesIn(String location); + +} diff --git a/content/docs/smallrye-graphql/1/_nav.yml b/content/docs/smallrye-graphql/1/_nav.yml new file mode 100644 index 000000000..9ab6f9cb6 --- /dev/null +++ b/content/docs/smallrye-graphql/1/_nav.yml @@ -0,0 +1,54 @@ +title: SmallRye GraphQL +nav: +- title: Overview + page: index +- title: Server side features + items: + - title: Customizing JSON deserializers + page: custom-json-deserializers + - title: Directives + page: directives + - title: Federation + page: federation + - title: Custom error extensions + page: custom-error-extensions + - title: Response extensions + page: extensions +- title: Typesafe client + items: + - title: Basic usage + page: typesafe-client-usage + - title: Reactive + page: typesafe-client-reactive-types + - title: Custom scalar types + page: typesafe-client-custom-scalars + - title: Adding headers + page: typesafe-client-headers + - title: Logging + page: typesafe-client-logging + - title: Running multiple queries at once + page: typesafe-client-multiple-queries + - title: Error handling + page: typesafe-client-error-handling +- title: Dynamic client + items: + - title: Basic usage + page: dynamic-client-usage + - title: Error handling + page: dynamic-client-error-handling +- title: Clients in general + items: + - title: Using SmallRye Stork + page: using-smallrye-stork +- title: Developer tools + items: + - title: Maven plugin + page: maven-plugin + - title: Gradle plugin + page: gradle-plugin +- title: Reference material + items: + - title: Server-side configuration + page: server-configuration + - title: Client-side configuration + page: client-configuration diff --git a/content/docs/smallrye-graphql/1/client-configuration.adoc b/content/docs/smallrye-graphql/1/client-configuration.adoc new file mode 100644 index 000000000..ceaad890a --- /dev/null +++ b/content/docs/smallrye-graphql/1/client-configuration.adoc @@ -0,0 +1,27 @@ += Client-side configuration properties +include::_attributes.adoc[] + +Note: if you are using Quarkus, it is recommended to use the `quarkus.++*++` property counterparts instead. +See https://quarkus.io/guides/all-config#quarkus-smallrye-graphql-client_quarkus-smallrye-graphql-client-smallrye-graphql-client[Quarkus Documentation] for more info. + +[width="100%",cols="33%,35%,32%",options="header",] +|=== +|Property |Default value |Meaning +|`CLIENT++_++NAME/mp-graphql/url` |none |Denotes URL to connect to +|`CLIENT++_++NAME/mp-graphql/header/KEY` |none |Adds a HTTP header named `KEY` to all HTTP requests performed by the client +|`CLIENT++_++NAME/mp-graphql/subprotocols` |`graphql-ws,graphql-transport-ws` |Comma-separated list of websocket subprotocols supported by this client. We currently support `graphql-ws` and `graphql-transport-ws`. If multiple subprotocols are provided, choosing the actual subprotocol will be subject to negotiation with the server. +|`CLIENT++_++NAME/mp-graphql/keystore` |none |Path to client's keystore (for example `file:/path/to/keystore` or `classpath:path/to/keystore`) +|`CLIENT++_++NAME/mp-graphql/keystoreType` |`JKS` |Keystore type +|`CLIENT++_++NAME/mp-graphql/keystorePassword` |none |Keystore password +|`CLIENT++_++NAME/mp-graphql/truststore` |none |Path to client's truststore (for example `file:/path/to/truststore` or `classpath:path/to/truststore`) +|`CLIENT++_++NAME/mp-graphql/truststoreType` |`JKS` |Truststore type +|`CLIENT++_++NAME/mp-graphql/truststorePassword` |none |Truststore password +|`CLIENT++_++NAME/mp-graphql/proxyHost` |none |Hostname of the proxy to use +|`CLIENT++_++NAME/mp-graphql/proxyPort` |none |Port of the proxy to use +|`CLIENT++_++NAME/mp-graphql/proxyUsername` |none |Username for the proxy to use +|`CLIENT++_++NAME/mp-graphql/proxyPassword` |none |Password for the proxy to use +|`CLIENT++_++NAME/mp-graphql/maxRedirects` |16 |Max number of redirects to follow. Set to 0 to disable redirects. +|`CLIENT++_++NAME/mp-graphql/websocketInitializationTimeout` |none |Maximum time in milliseconds that will be allowed to wait for the server to acknowledge a websocket connection. +|`CLIENT++_++NAME/mp-graphql/runSingleOperationsOverWebsocket` |`false` |If true, then queries and mutations will run over the websocket transport rather than pure HTTP. Off by default, because it has higher overhead. +|`CLIENT++_++NAME/mp-graphql/initPayload/KEY` |none |Adds a property named `KEY` to the `connection++_++init` message payload when negotiating a websocket connection. All values will be treated as string. For other types instatiate the API with the builder. +|=== diff --git a/content/docs/smallrye-graphql/1/custom-error-extensions.adoc b/content/docs/smallrye-graphql/1/custom-error-extensions.adoc new file mode 100644 index 000000000..82aa86e5b --- /dev/null +++ b/content/docs/smallrye-graphql/1/custom-error-extensions.adoc @@ -0,0 +1,44 @@ += GraphQL Error Extensions +include::_attributes.adoc[] + +Exceptions are reported in GraphQL in the `errors` array, next to the `data` field, so it's possible to return partial results, e.g.: + +[source,json] +---- +{ + "data": { + "superHero": { + "name": "Wolverine", + "location": null + } + }, + "errors": [{ + "message":"location unknown", + "path": ["superHero","location"], + "extensions":{"code":"location-unknown"} + }] +} +---- + +The `location` field couldn't be returned, and in the `errors`, there's the reason with a few predefined fields, +and a map of `extensions` that can contain custom details about the error. + +You can add your own extensions by implementing the `io.smallrye.graphql.api.ErrorExtensionProvider` interface and +adding your class name to a file `META-INF/services/io.smallrye.graphql.api.ErrorExtensionProvider` (this is a `ServiceLoader`). + +As an example, this class provides an extension named `exception-length` with the length of the simple class name of the exception: + +[source,java] +---- +public class ExceptionLengthErrorExtensionProvider implements ErrorExtensionProvider { + @Override + public String getKey() { + return "exception-length"; + } + + @Override + public JsonNumber mapValueFrom(Throwable exception) { + return Json.createValue(exception.getClass().getSimpleName().length()); + } +} +---- diff --git a/content/docs/smallrye-graphql/1/custom-json-deserializers.adoc b/content/docs/smallrye-graphql/1/custom-json-deserializers.adoc new file mode 100644 index 000000000..902ca3582 --- /dev/null +++ b/content/docs/smallrye-graphql/1/custom-json-deserializers.adoc @@ -0,0 +1,30 @@ += Customizing JSON-B deserializers +include::_attributes.adoc[] + +If your application needs finer-grained control over JSON deserialization than you can achieve via +formatting annotations like `@JsonbDateFormat`, you may plug in your own custom instances of the `Jsonb` class +for each input type that your GraphQL application exposes. + +To do this, you'll need to create an implementation of `io.smallrye.graphql.spi.EventingService` +and implement its `overrideJsonbConfig` method. +An example that plugs in a custom date format for a particular class that is used as input: + +.... +public class CustomJsonbService implements EventingService { + + @Override + public String getConfigKey() { + return null; // activate this service always regardless of the configuration + } + + @Override + public Map overrideJsonbConfig() { + JsonbConfig config = new JsonbConfig().withDateFormat("MM dd yyyy HH:mm Z", null); + return Collections.singletonMap("org.example.model.MyModelClass", JsonbBuilder.create(config)); + } +} +.... + +As the discovery of eventing services uses the ServiceLoader mechanism, don't forget to add a +`META-INF/services/io.smallrye.graphql.spi.EventingService` file that contains the fully qualified +name of your implementation. diff --git a/content/docs/smallrye-graphql/1/directives.adoc b/content/docs/smallrye-graphql/1/directives.adoc new file mode 100644 index 000000000..34b420ca8 --- /dev/null +++ b/content/docs/smallrye-graphql/1/directives.adoc @@ -0,0 +1,45 @@ += Directives +include::_attributes.adoc[] + +== Custom Directives + +You can add your own https://spec.graphql.org/draft/#sec-Language.Directives[GraphQL Directives] by writing +a corresponding Java Annotation and annotate it as `@Directive`, e.g.: + +[source,java] +---- +@Directive(on = { OBJECT, INTERFACE }) +@Description("Just a test") +@Retention(RUNTIME) +public @interface MyDirective { +} +---- + +Directives can be repeatable, see the `@Key` annotation for an example. + +== Directives generated from Bean Validation annotations + +If your project uses Bean Validation to validate fields on input types and operation arguments, and you enable +inclusion of directives in the schema (by setting `smallrye.graphql.schema.includeDirectives=true`), +then constraints decoded from annotations will be added to your schema as directives. This is currently only +supported for some built-in constraints (annotations from the +`javax.validation.constraints` package), and custom constraints aren't supported at all. + +Each bean validation annotation is mapped to a single `@constraint` directive. The directive is declared as repeatable, +so if you have multiple constraints on an input field, the field will contain multiple `@constraint` directives. +The following table describes the mapping between BV annotations and `@constraint` directives (all currently supported +BV annotations are listed here): + +[cols=",",options="header",] +|=== +|BV annotation |GraphQL directive +|`@Size(MIN, MAX)` |`@constraint(minLength=MIN, maxLength=MAX)` +|`@Email` |`@constraint(format='email')` +|`@Max` |`@constraint(max=VALUE)` +|`@Min` |`@constraint(min=VALUE)` +|`@Pattern(REGEXP)` |`@constraint(pattern=REGEXP)` +|=== + +Note: The `@NotNull` annotation does not map to a directive, instead it makes the GraphQL type non-nullable. + +Constraints will only appear on fields of input types and operation arguments. diff --git a/content/docs/smallrye-graphql/1/dynamic-client-error-handling.adoc b/content/docs/smallrye-graphql/1/dynamic-client-error-handling.adoc new file mode 100644 index 000000000..e7a9b5f79 --- /dev/null +++ b/content/docs/smallrye-graphql/1/dynamic-client-error-handling.adoc @@ -0,0 +1,34 @@ += Error handling with dynamic clients +include::_attributes.adoc[] + +== System-level errors + +If the service returns a system-level error (which means the response body doesn't contain +a valid GraphQL response document), the client invocation will (instead of returning a +`io.smallrye.graphql.client.Response`) throw a `io.smallrye.graphql.client.InvalidResponseException` +whose message contains information about the received status code, status message, and body +contents. + +== Application-level errors + +If the service returns one or more application-level errors (which means that +there is valid GraphQL response in the body that has a non-empty `errors` entry), the +client invocation will return a `io.smallrye.graphql.client.Response` as normal, and that object +will contain information about the errors. Errors are represented as `io.smallrye.graphql.client.GraphQLError` +objects and can be inspected after retrieving a list of all errors using `response.getErrors()`. + +____ +++[++NOTE++]++ +An error response is considered application-level regardless of the HTTP status code as long as it +contains a valid GraphQL response. We define a GraphQL response as a well-formed JSON document +that contains at least one of `errors`, `data` and `extensions` entries, and no other entries beyond +that. +____ + +It is also possible to convert a response's errors into a `io.smallrye.graphql.client.GraphQLClientException` +by calling `throwExceptionIfErrors()` - the response has to be cast to `ResponseImpl` for this as of now. +This method will, if there are any errors in the response, convert the errors into a `GraphQLClientException` +and throw it. + +If you need to access the HTTP response code and message pertaining to a response that contained +application-level errors, refer to xref:dynamic-client-usage.adoc#accessing-http-headers-and-response-codes[Accessing HTTP headers and response codes]. diff --git a/content/docs/smallrye-graphql/1/dynamic-client-usage.adoc b/content/docs/smallrye-graphql/1/dynamic-client-usage.adoc new file mode 100644 index 000000000..d166184dd --- /dev/null +++ b/content/docs/smallrye-graphql/1/dynamic-client-usage.adoc @@ -0,0 +1,113 @@ += Dynamic client introduction and basic usage +include::_attributes.adoc[] + +A Java GraphQL client. The main difference from the typesafe client is +that while the typesafe client behaves like a typesafe proxy very +similar to the MicroProfile REST Client, the dynamic client is more like +the JAX-RS client from the `javax.ws.rs.client` package. Instead of +working with model classes directly, the dynamic client focuses on +programmatically working with GraphQL documents representing GraphQL +requests and responses. It still offers the option to convert between +documents and model classes when necessary. + +In the current implementation, Vert.x HTTP client is used for handling +the underlying traffic. + +Creating a client instance + +Generally there are two ways to obtain a client instance. + +First, using CDI injection where the configuration values are defined in system +properties: + +[source,java] +---- +@Inject +@GraphQLClient("superheroes") +DynamicGraphQLClient client; + +// assuming that this system property exists: +// superheroes/mp-graphql/url=https://superheroes.org/graphql +---- + +The above example assumes that configuration for the client is present in system properties. For a full list of +supported configuration properties, see link:/client_configuration[Client configuration reference] + +The other way to build a client is programmatically using a builder: + +[source,java] +---- +DynamicGraphQLClient client = DynamicGraphQLClientBuilder.newBuilder() + .url("https://superheroes.org/graphql") + .build(); +---- + +The usage examples in the following sections will assume using the first approach - injection. + +Basic Usage + +Given the following GraphQL service on the server side: + +[source,java] +---- +@GraphQLApi +class SuperHeroesApi { + @Query + List allHeroesIn(String location) { + // .... + } +} + +class SuperHero { + private String name; + private List superPowers; +} +---- + +Such service can be queried this way: + +[source,java] +---- +include::{examples-dir}/examples/dynamicclient/MyClientUsage.java[] +---- + +* ++<++1++>++ Creating the document representing the request. We used static +imports to make the code easy to read, they all come from the +classes in the `io.smallrye.graphql.client.core` package. +* ++<++2++>++ Executing the request. You can either do that in a blocking way, or +request a `Uni` if you prefer the reactive style. +* ++<++3++>++ Obtaining the resulting list of heroes as a `JsonArray`. +* ++<++4++>++ Obtaining the resulting list of heroes as instances of the model +class. This is optional, you can continue working with the data as a `JsonArray` if you prefer. + +Using plain strings instead of the DSL + +If you don't like the DSL for some reason and want to use plain strings for your queries, these two examples +will serve you: + +[source,java] +---- +include::{examples-dir}/examples/dynamicclient/MyClientUsageString.java[] +---- + +* ++<++1++>++: In this variant, we inline values for query arguments directly into the query string. +* ++<++2++>++: In this variant, argument values are extracted into variables. The `location` argument of the `allHeroesIn` query is +declared to be using the variable `loc` (the variable can also be named `location` same as the argument, if +you prefer). +* ++<++3++>++: Here we create a map that defines the values for each variable. Values of this map are `Object`s, +so you can put in strings, numbers, booleans, or any object that corresponds to a GraphQL type and can be +serialized to JSON. Inserting a `JsonObject` directly is also supported. + +Accessing HTTP headers and response codes + +To access HTTP transport metadata that was passed by the server, you can inspect the `Response` object. + +`Response.getTransportMeta("HEADER-NAME")` returns a `List++<++String++>++` containing (potentially multiple) values of the requested header. + +To get the HTTP status code or status message, these are stored inside the `transportMeta` map too. `ResponseImpl` +contains convenience methods to retrieve them: `ResponseImpl.getStatusCode()` and `ResponseImpl.getStatusMessage()`. +It's also possible to retrieve them directly without casting to `ResponseImpl` by calling +`Integer.valueOf(response.getTransportMeta().get("++<++status-code++>++").get(0))` and + +`response.getTransportMeta().get("++<++status-message++>++").get(0)`. + +HTTP headers, status codes and messages are only available for operations executed over pure HTTP, not via websockets! diff --git a/content/docs/smallrye-graphql/1/extensions.adoc b/content/docs/smallrye-graphql/1/extensions.adoc new file mode 100644 index 000000000..0ebe7dfdf --- /dev/null +++ b/content/docs/smallrye-graphql/1/extensions.adoc @@ -0,0 +1,52 @@ += Adding extensions to GraphQL response +include::_attributes.adoc[] + +*SmallRye GraphQL* allows the user to add their own extensions inside the GraphQL response +which is received from the server. The `extensions` are stored next to the `data` field in the GraphQL response. + +[source,json] +---- +{ + "data": { + "shirt": { + "size": "L" + } + }, + "extensions": { + "country": "Germany", + "pi": 3.1415926535 + } +} +---- + +To add extensions, you need to `@Inject` an instance of `SmallRyeContext` in your `@GraphQLApi` annotated class. +After that, you can add your own extensions via the method `addExtensions` +with its input parameters: `key : String` as an identification of the added extension. +And `value: Object` as a value of the extension. Value can be any given object that can be converted into JsonValue. + +As an example, this class below contains the query `getShirt` and during the http request, the query adds these extensions: +`++{++"country": "Germany", "pi": 3.1415926535}`. +These extensions will be sent back via response in the `extensions` field. + +[source,java] +---- +import javax.inject.Inject; + +@GraphQLApi +public class ShirtResources { + @Inject + SmallRyeContext smallRyeContext; + + @Query + public Shirt getShirt() { + smallRyeContext.addExtensions("country", "Germany"); + smallRyeContext.addExtensions("pi", 3.1415926535); + //... + } +} +---- + +____ +++[++NOTE++]++ +You can also use the method `setAddedExtensions(Map++<++String, Object++>++ addedExtensions)` to set all the extensions with map instance. +____ diff --git a/content/docs/smallrye-graphql/1/federation.adoc b/content/docs/smallrye-graphql/1/federation.adoc new file mode 100644 index 000000000..081a24edc --- /dev/null +++ b/content/docs/smallrye-graphql/1/federation.adoc @@ -0,0 +1,66 @@ += Federation +include::_attributes.adoc[] + +Support for https://www.apollographql.com/docs/federation[GraphQL Federation] is enabled by default. If you add one of the federation annotations, the corresponding directives will be declared to your schema and the additional Federation queries will be added automatically. You can also disable Federation completely by setting the `smallrye.graphql.federation.enabled` config key to `false`. + +You can add the Federation directives by using the equivalent Java annotation, e.g. to extend a `Product` entity with a `price` field, you can write a class: + +[source,java] +---- +package org.example.price; + +import org.eclipse.microprofile.graphql.Id; + +import io.smallrye.graphql.api.federation.Extends; +import io.smallrye.graphql.api.federation.Key; + +@Extends @Key(fields = "id") +public class Product { + @Id + private String id; + + @Description("The price in cent") + private Integer price; + + // getters and setters omitted +} +---- + +And a normal query method that takes the `key` fields as a parameter and returns the requested type: + +[source,java] +---- +package org.example.price; + +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Id; +import org.eclipse.microprofile.graphql.Query; + +@GraphQLApi +public class Prices { + @Query + public Product product(@Id String id) { + return ... + } +} +---- + +The GraphQL Schema then contains: + +[source,graphql] +---- +type Product @extends @key(fields : "id") { + id: ID + price: Int +} + +union _Entity = Product + +type Query { + _entities(representations: [_Any!]!): [_Entity]! + _service: _Service! + product(id: ID): Product +} +---- + +If you can resolve, e.g., the product with different types of ids, you can add multiple `@Key` annotations. diff --git a/content/docs/smallrye-graphql/1/gradle-plugin.adoc b/content/docs/smallrye-graphql/1/gradle-plugin.adoc new file mode 100644 index 000000000..1cee539ab --- /dev/null +++ b/content/docs/smallrye-graphql/1/gradle-plugin.adoc @@ -0,0 +1,80 @@ += SmallRye GraphQL Gradle plugin +include::_attributes.adoc[] + +This Gradle plugin allows you to generate the GraphQL Schema on build and save it as a text file. + +Add this to your build.gradle: + +.... +plugins { + id 'io.smallrye.graphql' version '1.0.2-SNAPSHOT' +} +.... + +The schema will appear as `build/generated/schema.graphql` by default. + +The plugin’s `generateSchema` task can customized like: + +.... +generateSchema { + destination = null + includeTransitiveDependencies = true + includeScalars = true +} +.... + +This will include the project’s dependencies (including the transitive +dependencies) to scan for GraphQL model classes. As the destination is +set to `null` the generated schema will only be output to the console +and saved to a file. + +In case external dependencies need to be scanned for GraphQL model +classes a more advanced configuration could look like: + +.... +configrations { + graphQLSchema +} + +dependencies { + graphQLSchema .. +} + +generateSchema { + includeDependencies = true + configurations = ["graphQLSchema"] +} +.... + +This defines a separate `graphQLSchema` configuration. In the +dependencies block the external dependencies containing the GraphQL mode +class should then be registered for the `graphQLSchema` configuration. +The `generateSchema` task is then customized to only include +dependencies from the `graphQLSchema` configuration. Also note that we +set `includeDependencies` to `true` (and not +`includeTransitiveDependencies`). This will make sure only direct +dependencies will be be scanned and not possible transitive +dependencies. + +Configuration options + +* `destination` - To override the default +`build/generated/schema.graphql` destination. +* `includeDependencies` - Scan project’s dependencies for GraphQL +model classes too. Default false. +* `includeTransitiveDependencies` - Scan project’s dependencies +(including the transitive dependencies) for GraphQL model classes +too. Default false. +* `configurations` - If the above `includeDependencies` or +`includeTransitiveDependencies` is true, you can control what +configurations should be included. Default is `implementation`. +* `dependencyExtensions` - If the above `includeDependencies` or +`includeTransitiveDependencies` is true, you can control what +dependency extensions should be included. Default is `jar`. +* `includeScalars` - Include scalars in the schema. Default false. +* `includeDirectives` - Include directives in the schema. Default +false. +* `includeSchemaDefinition` - Include the schema definition. Default +false. +* `includeIntrospectionTypes` - Include the introspection types in the +schema. Default false. diff --git a/content/docs/smallrye-graphql/1/images/redhat_reversed.svg b/content/docs/smallrye-graphql/1/images/redhat_reversed.svg new file mode 100644 index 000000000..37fe83c48 --- /dev/null +++ b/content/docs/smallrye-graphql/1/images/redhat_reversed.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/content/docs/smallrye-graphql/1/index.adoc b/content/docs/smallrye-graphql/1/index.adoc new file mode 100644 index 000000000..f277286e0 --- /dev/null +++ b/content/docs/smallrye-graphql/1/index.adoc @@ -0,0 +1,6 @@ += Overview +include::_attributes.adoc[] + +SmallRye GraphQL is a library that allows you to easily use GraphQL (both server-side endpoints +and client-side code) in your Java-based projects. It implements +the MicroProfile GraphQL specification. diff --git a/content/docs/smallrye-graphql/1/maven-plugin.adoc b/content/docs/smallrye-graphql/1/maven-plugin.adoc new file mode 100644 index 000000000..b3f175eff --- /dev/null +++ b/content/docs/smallrye-graphql/1/maven-plugin.adoc @@ -0,0 +1,45 @@ += SmallRye GraphQL Maven plugin +include::_attributes.adoc[] + +This Maven plugin allows you to generate the GraphQL Schema on build and save it as a text file. + +Add this to your pom.xml: + +[source,xml] +---- + + smallrye-graphql-maven-plugin + io.smallrye + + + + generate-schema + + + + +---- + +The schema will appear as `target/generated/schema.graphql` by default. The goal is bound to the `process-classes` +build phase. + +Configuration options + +* `destination` - To override the default `target/generated/schema.graphql` destination +* `includeDependencies` - Scan project’s dependencies for GraphQL +model classes too. This is necessary if your GraphQL endpoint exposes +classes that are not in the application's main module. Off (`false`) by default, because +scanning a potentially large dependency tree slows down execution substantially. +Usage of reactive types does not require turning this option on. +* `includeDependenciesScopes` - If the above `includeDependencies` is +true, you can control what scopes should be included. Default is +`compile, system` +* `includeDependenciesTypes` - If the above `includeDependencies` is +true, you can control what types should be included. Default is +`jar` +* `includeScalars` - Include scalars in the schema. Default false. +* `includeDirectives` - Include directives in the schema. Default false. +* `includeSchemaDefinition` - Include the schema definition. Default false. +* `includeIntrospectionTypes` - Include the introspection types in the schema. Default false. +* `typeAutoNameStrategy` - Strategy for transforming class names into GraphQL type names. +Valid values are `MergeInnerClass`, `Full` and`Default`. diff --git a/content/docs/smallrye-graphql/1/server-configuration.adoc b/content/docs/smallrye-graphql/1/server-configuration.adoc new file mode 100644 index 000000000..054c01128 --- /dev/null +++ b/content/docs/smallrye-graphql/1/server-configuration.adoc @@ -0,0 +1,38 @@ += Configuration properties +include::_attributes.adoc[] + +These are properties understood directly by SmallRye GraphQL. If you're using SmallRye GraphQL through +Quarkus, these will generally work, but Quarkus offers its own counterparts for most of these, +and it is recommended to use the `quarkus.++*++` properties. Refer to the Quarkus documentation: +https://quarkus.io/guides/all-config#quarkus-smallrye-graphql_quarkus-smallrye-graphql-smallrye-graphql[Server] and +https://quarkus.io/guides/all-config#quarkus-smallrye-graphql-client_quarkus-smallrye-graphql-client-smallrye-graphql-client[Client] +side Quarkus properties. + +== From MicroProfile GraphQL + +[width="100%",cols="33%,35%,32%",options="header",] +|=== +|Property |Default value |Meaning +|`mp.graphql.defaultErrorMessage` |`Server Error` |Error message for hidden exceptions +|`mp.graphql.hideErrorMessage` |Runtime exceptions |Exceptions that will be hidden +|`mp.graphql.showErrorMessage` |Checked exceptions |Exceptions that will not be hidden. +|=== + +== From SmallRye GraphQL + +[width="100%",cols="33%,35%,32%",options="header",] +|=== +|Property |Default value |Meaning +|`smallrye.graphql.printDataFetcherException` |`false` |Include the stacktrace of the data fetching exception in the log output +|`smallrye.graphql.allowGet` |`false` |Allow HTTP GET Method +|`smallrye.graphql.metrics.enabled` |`false` |Enable metrics +|`smallrye.graphql.tracing.enabled` |`false` |Enable tracing +|`smallrye.graphql.validation.enabled` |`true` if Bean Validation is present |Enable Bean Validation. This property is DEPRECATED, setting to `false` won't actually turn off validation. It will be removed in a future release. +|`smallrye.graphql.events.enabled` |`true` if one of metrics, tracing or bean validation is true |Enable eventing +|`smallrye.graphql.logPayload` |`false` |Log the payload in the log file +|`smallrye.graphql.fieldVisibility` | |To control the field visibility on introspection +|`smallrye.graphql.schema.includeScalars` |`true` |Include Scalar definitions in the schema +|`smallrye.graphql.schema.includeSchemaDefinition` |`false` |Include Schema definition +|`smallrye.graphql.schema.includeDirectives` |`false` |Include directives in the schema +|`smallrye.graphql.schema.includeIntrospectionTypes` |`false` |Include Introspection types in the schema +|=== diff --git a/content/docs/smallrye-graphql/1/typesafe-client-custom-scalars.adoc b/content/docs/smallrye-graphql/1/typesafe-client-custom-scalars.adoc new file mode 100644 index 000000000..c6fbc61ce --- /dev/null +++ b/content/docs/smallrye-graphql/1/typesafe-client-custom-scalars.adoc @@ -0,0 +1,38 @@ += Custom scalar types +include::_attributes.adoc[] + +If the remote service contains custom scalar types represented as strings in GraphQL documents, +a typesafe client can learn to understand these types through introducing a class that represents them. +The class has to contain logic necessary for serializing and deserializing from/to raw strings. + +A model class is considered to represent a scalar GraphQL type if it contains a static builder method +named `of`, `valueOf`, or `parse` whose sole parameter is a `java.lang.String` and it returns an +instance of the class itself. The name of the GraphQL type is assumed to be equal to the short name +of the class. + +For example, if the server supports a `URL` type (there is a `scalar URL` clause in the schema), +create a class that represents a value for `URL`: + +[source,java] +---- +public class URL { + + private String value; + + public URL(String value) { + this.value = value; + } + + public static URL valueOf(String value) { // <1> + return new URL(value); + } + + @Override + public String toString() { // <2> + return value; + } +} +---- + +* ++<++1++>++ The static `valueOf(String)` method will be used for deserializing a `URL` from a string. +* ++<++2++>++ The `toString()` will be used for serializing a `URL` into a string to be sent to the GraphQL service diff --git a/content/docs/smallrye-graphql/1/typesafe-client-error-handling.adoc b/content/docs/smallrye-graphql/1/typesafe-client-error-handling.adoc new file mode 100644 index 000000000..dd8464f37 --- /dev/null +++ b/content/docs/smallrye-graphql/1/typesafe-client-error-handling.adoc @@ -0,0 +1,60 @@ += Error handling in typesafe clients +include::_attributes.adoc[] + +== System-level errors + +If the service returns a system-level error (which means the response body doesn't contain +a valid GraphQL response document), the client invocation will throw a +`io.smallrye.graphql.client.InvalidResponseException` +whose message contains information about the received status code, status message, and body +contents. + +== Application-level errors + +If the service returns one or more application-level errors (which means that +there is valid GraphQL response in the body that has a non-empty `errors` entry), +the client normally throws a `GraphQLClientException` containing the details in a list of +`GraphQLClientError`. + +____ +++[++NOTE++]++ +An error response is considered application-level regardless of the HTTP status code as long as it +contains a valid GraphQL response. We define a GraphQL response as a well-formed JSON document +that contains at least one of `errors`, `data` and `extensions` entries, and no other entries beyond +that. +____ + +If the error is specific to a `location`, you can use an `ErrorOr` +wrapper on the target field; the client the maps the error to that +wrapper instead of throwing an exception. I.e. your `SuperHero` class +could look like this: + +[source,java] +---- +class SuperHero { + String name; + ErrorOr location; +} +---- + +If the service returns a response like this: + +[source,json] +---- +{ + "data": { + "superHero": { + "name": "Wolverine", + "location": null + } + }, + "errors": [{ + "message":"location unknown", + "path": ["superHero","location"], + "extensions":{"code":"location-unknown"} + }] +} +---- + +Then the `SuperHero#location` wrapper field will not contain a `value` +but only the error above. See the `ErrorOr` class for details. diff --git a/content/docs/smallrye-graphql/1/typesafe-client-headers.adoc b/content/docs/smallrye-graphql/1/typesafe-client-headers.adoc new file mode 100644 index 000000000..d3dea2f6c --- /dev/null +++ b/content/docs/smallrye-graphql/1/typesafe-client-headers.adoc @@ -0,0 +1,84 @@ += Adding headers +include::_attributes.adoc[] + +To add a custom header to the http requests sent out by the GraphQL +Client, annotate your method or the API interface as `@Header`, e.g.: + +[source,java] +---- +@GraphQLClientApi +interface SuperHeroesApi { + @Header(name = "S.H.I.E.L.D.-Clearance", constant = "TOP-SECRET") + List allHeroesIn(String location); +} +---- + +The value can be specified with a `constant` or the name of a `method` for dynamic values, e.g.: + +[source,java] +---- +@GraphQLClientApi +interface SuperHeroesApi { + @Header(name = "S.H.I.E.L.D.-Clearance", method = "establishShieldClearance") + List allHeroesIn(String location); + + static Clearance establishShieldClearance() { + return userIsInRole(MANAGER) ? TOP_SECRET : PUBLIC; + } +} +---- + +* This example uses an enum, but it can be any Object; the GraphQL client +calls `toString()` to convert it. + +The method must be `static` and accessible by the interface, i.e. in the +interface itself or in one of the classes it’s nested in; if it’s in a +different class, prefix it with the fully qualified class name and a dot +`"."`, e.g. +`@Header(name = "S.H.I.E.L.D.-Clearance", method = "org.superheroes.SecurityTools.establishShieldClearance")`. + +A third option is to pass the value of a header as an API method +parameter: + +[source,java] +---- +@GraphQLClientApi +interface SuperHeroesApi { + List allHeroesIn(String location, @Header(name = "S.H.I.E.L.D.-Clearance") Clearance clearance); +} +---- + +The `@Header` parameter will not be part of the GraphQL query. + +The `name` of the header is always static, but can optionally be derived from the name of the parameter or method, i.e. if it has a `@Name` annotation, that annotation value is used. If it's not annotated, the method name (eventually stripped off a leading `get`) or the parameter name (if it's enabled in the compiler settings) is converted from camel case to kebab case, i.e. a parameter `@Header String requestIdentifier` will result in a header named `Request-Identifier`. + +`@Header` annotations can also be defined via `@Stereotype`. + +When instantiating the API with the builder, you can set (or overwrite) +one or more headers there: + +[source,java] +---- +SuperHeroesApi api = TypesafeGraphQLClientBuilder.newBuilder() + .header("S.H.I.E.L.D.-Clearance", "TOP-SECRET") + .build(SuperHeroesApi.class); +---- + +Authorization headers + +To add an `Authorization` header, instead of using the generic `@Header` +annotation, you can also use the special `@AuthorizationHeader` +annotation. It produces a `BASIC` `Authorization` header by default or a +`BEARER` token. You can configure the credentials in MP Config with a +prefix plus `/mp-graphql/` and either `username` and `password` for +`BASIC` or `bearer` for `BEARER`. The config key defaults to the fully +qualified name of the `GraphQLClientApi` interface or its `configKey`. + +You can use a custom prefix by setting the `confPrefix`. The infix +`/mp-graphql/` is still applied, unless you end the `confPrefix` with +`++*++`, e.g. +`@AuthorizationHeader(confPrefix = "org.superheroes.security.basic.++*++` +will use `org.superheroes.security.basic.username`, while `++*++` will use +plain `username`. + +`@AuthorizationHeader` annotations can be defined via `@Stereotype`. diff --git a/content/docs/smallrye-graphql/1/typesafe-client-logging.adoc b/content/docs/smallrye-graphql/1/typesafe-client-logging.adoc new file mode 100644 index 000000000..dad8dffc9 --- /dev/null +++ b/content/docs/smallrye-graphql/1/typesafe-client-logging.adoc @@ -0,0 +1,7 @@ += Logging in typesafe clients +include::_attributes.adoc[] + +The Client implementation logs all GraphQL requests and responses at +level `INFO` with the interface API as the logger name. It also logs the +keys of all headers added at level `DEBUG`; not the values, as they may +be security sensitive. diff --git a/content/docs/smallrye-graphql/1/typesafe-client-multiple-queries.adoc b/content/docs/smallrye-graphql/1/typesafe-client-multiple-queries.adoc new file mode 100644 index 000000000..80b399f00 --- /dev/null +++ b/content/docs/smallrye-graphql/1/typesafe-client-multiple-queries.adoc @@ -0,0 +1,48 @@ += Multiple +include::_attributes.adoc[] + +Say you need the result from several root queries, e.g. all +`superHeroes` and all `superVillains`. Java only supports a single +return value, so you’ll need a wrapper class: + +[source,java] +---- +@GraphQLClientApi +interface SuperHeroesApi { + HeroesAndVillains heroesAndVillains(); +} + +@Multiple +class HeroesAndVillains { + List superHeroes; + List superVillains; +} +---- + +The `@Multiple` annotation 'inlines' the wrapper class, i.e. the actual +query is: + +[source,graphql] +---- +query heroesAndVillains { superHeroes {...} superVillains {...}} +---- + +The actual response below will be mapped to an instance of the +`HeroesAndVillains` wrapper class: + +[source,json] +---- +{ + "data": { + "superHeroes": [ ... ], + "superVillains": [ ... ] + } +} +---- + +If the nested queries require parameters, use `@` annotations to put +them on the field (remember: GraphQL fields can have parameters). + +If you need the same request several times (e.g. with different query +parameters), use `@Name` annotations, so the actual field names are used +as xref:#_name_mapping_aliases[alias]. diff --git a/content/docs/smallrye-graphql/1/typesafe-client-reactive-types.adoc b/content/docs/smallrye-graphql/1/typesafe-client-reactive-types.adoc new file mode 100644 index 000000000..a1c7b98c1 --- /dev/null +++ b/content/docs/smallrye-graphql/1/typesafe-client-reactive-types.adoc @@ -0,0 +1,48 @@ += Reactive types usage in typesafe clients +include::_attributes.adoc[] + +== Reactive types with queries and mutations + +Regardless of the type that a query or mutation returns, you can always wrap the return type in a `Uni` in your +`@GraphQLClientApi` interface. The `Uni` will be completed after the response is received and processed. + +== Subscriptions + +The return type of a subscription operation must always be wrapped in a `Multi`. +The communication with the server runs over a WebSocket. + +=== Important note about errors + +When using the typesafe client for subscriptions, if it is expected that the server might return errors, +then it is highly recommended to wrap the return types into an `ErrorOr` +(for example, turn the API method `@Subscription Multi++<++Person++>++ people` into `@Subscription Multi++<++ErrorOr++<++Person++>>++ people`). +The reason is once there is an error returned from the service and the result can't be turned into a domain object due to that, +the `Multi` will receive a failure as a result. Because of the design of Mutiny, a `Multi` can't continue producing any items after +a failure. That means the subscription will be cancelled after the first error, even though the server might continue +sending more items. In such case, your application would have to detect the error and start a new subscription. +See xref:typesafe-client-error-handling.adoc[Error handling] for more details on how to use `ErrorOr`. + +=== Subscription example + +Given a server-side definition like this: + +[source,java] +---- +@Subscription +public Multi countToFive() { + return Multi.createFrom().range(0, 5); +} +---- + +A client to consume this subscription can look like this: + +[source,java] +---- +client.countToFive + .subscribe() + .with( // onItem: + i -> System.out.println("Received number" + i), + // onFailure: + (t) -> t.printStackTrace() + ); +---- diff --git a/content/docs/smallrye-graphql/1/typesafe-client-usage.adoc b/content/docs/smallrye-graphql/1/typesafe-client-usage.adoc new file mode 100644 index 000000000..175949bc8 --- /dev/null +++ b/content/docs/smallrye-graphql/1/typesafe-client-usage.adoc @@ -0,0 +1,153 @@ += Basic usage +include::_attributes.adoc[] + +A Java code-first type-safe GraphQL Client API suggestion for +https://github.com/eclipse/microprofile-graphql/issues/185[Microprofile GraphQL Issue +#185]. + +Basic Usage + +Creating the client-side counterpart of the GraphQL API: + +[source,java] +---- +include::{examples-dir}/examples/typesafeclient/SuperHeroesApi.java[] +---- + +A model class: + +[source,java] +---- +include::{examples-dir}/examples/typesafeclient/SuperHero.java[] +---- + +Injecting the client using CDI and using it: + +[source,java] +---- +include::{examples-dir}/examples/typesafeclient/MyClientUsage.java[] +---- + +* The default request type is `query`. To make it a mutation, annotate +it `@Mutation`. The parameter name is only available if you compile +the source with the `-parameters` option. Otherwise, you’ll have to +annotate all parameters with `@Name`. + +The example above uses CDI, e.g. when you are in a MicroProfile or +Jakarta EE environment. If you are in an environment without CDI +support, you need to instantiate the API interface by using the builder: + +[source,java] +---- +SuperHeroesApi api = TypesafeGraphQLClientBuilder.newBuilder().build(SuperHeroesApi.class); +---- + +The basic idea of the Java code-first approach is that you start by +writing the DTOs and query/mutation methods as you need them in your +client. This ensures that you don’t request fields that you don’t need; +the thinking is inspired by https://martinfowler.com/articles/consumerDrivenContracts.html[Consumer Driven +Contracts]. + +If the server uses names different from yours, you can simply use +annotations to do a mapping: + +Name Mapping / Aliases + +If the server defines a different field or parameter name, annotate it +with `@Name`. If the server defines a different query name, annotate the +method as, e.g., `@Query("findHeroesCurrentlyLocatedIn")`. + +By renaming methods, you can also define several variations of the same +request but using different return types or parameters. E.g.: + +[source,java] +---- +public interface SuperHeroesApi { + SuperHero findHeroByName(String name); + + @Query("findHeroByName") + SuperHeroWithTeams findHeroWithTeamsByName(String name); +} +---- + +* The `SuperHero` class has no team affiliations (for this example). +* The `SuperHeroWithTeams` class has a `List++<++Team++>++ teamAffiliations` +field. The actual query name is still `findHeroByName`. The `Team` +class doesn’t contain the members to break recursion. + +If you rename a field or method, the real field or method name will be +used as an alias, so you can select the same data twice (see `and` +below). + +Configuration + +If the endpoint is always the same, e.g. a public API of a cloud +service, you can add the URL to your API annotation, e.g.: + +[source,java] +---- +@GraphQLClientApi(endpoint = "https://superheroes.org/graphql") +interface SuperHeroesApi { +} +---- + +When instantiating the API with the builder, you can set (or overwrite) +the endpoint there: + +[source,java] +---- +SuperHeroesApi api = TypesafeGraphQLClientBuilder.newBuilder() + .endpoint("https://superheroes.org/graphql") + .build(SuperHeroesApi.class); +---- + +Commonly you’ll need different endpoints, e.g. when you need one +endpoint for your production system, but a different endpoint for your +test system. Simply use https://download.eclipse.org/microprofile/microprofile-config-1.4/microprofile-config-spec.html[MicroProfile +Config] +to set the endpoint; similar to the https://download.eclipse.org/microprofile/microprofile-rest-client-1.4.1/microprofile-rest-client-1.4.1.html[MicroProfile Rest +Client], +the key for the endpoint is the fully qualified name of the api +interface, plus `/mp-graphql/url`, e.g.: + +[source,properties] +---- +org.superheroes.SuperHeroesApi/mp-graphql/url=https://superheroes.org/graphql +---- + +If you want to use a different key, set the base config key on the +annotation `@GraphQLClientApi(configKey = "superheroes")`; then use this +key for the endpoint `superheroes/mp-graphql/url`. + +When using the builder, you can override the config key as well: +`TypesafeGraphQLClientBuilder.newBuilder().configKey("superheroes")`. + +NestedParameter + +Some APIs require parameters beyond the root level, e.g. for filtering +or paginating nested lists. Say you have a schema like this: + +[source,graphql] +---- +type Query { + team(name: String!): Team! +} + +type Team { + members(first: Int!): [SuperHero!]! +} +---- + +To pass the parameter to the nested field/method, annotate it as +`@NestedParameter`, e.g.: + +[source,java] +---- +@GraphQLClientApi +interface TeamsApi { + Team team(String name, @NestedParameter("members") int first); +} +---- + +The value of the `@NestedParameter` annotation is the dot-delimited path +to the nested field/method that the value should be added to. diff --git a/content/docs/smallrye-graphql/1/using-smallrye-stork.adoc b/content/docs/smallrye-graphql/1/using-smallrye-stork.adoc new file mode 100644 index 000000000..46064219c --- /dev/null +++ b/content/docs/smallrye-graphql/1/using-smallrye-stork.adoc @@ -0,0 +1,53 @@ += Using SmallRye Stork +include::_attributes.adoc[] + +SmallRye GraphQL client (both dynamic and typesafe) supports SmallRye Stork for +service discovery and load balancing. If your client is configured to connect to a URI +with the `stork://` or `storks://` scheme, then Stork will be used to determine the +actual endpoints to connect to. Load balancing, if configured, will be applied in a way that +each HTTP request can be routed to a different service instance. For WebSocket traffic, +once a WebSocket connection is established, the client will keep using that connection +for as long as possible. If that connection is lost for any reason and needs to be +reestablished, then the load balancer can route it to a different instance. + +== Using with Quarkus + +The dependency on Stork is declared as optional, so if you want to use it, you might need +to add it explicitly. If you're using Quarkus, use the Stork extension along with +the desired service discovery implementation, for example: + +.... + + io.quarkus + quarkus-smallrye-stork + + + io.smallrye.stork + smallrye-stork-service-discovery-static-list + +.... + +Example configuration in Quarkus `application.properties`: + +.... +quarkus.smallrye-graphql-client.myclient.url=stork://foo-service/graphql +quarkus.stork.foo-service.service-discovery.type=static +quarkus.stork.foo-service.service-discovery.address-list=server1.com:8080,server2.com:8080 +.... + +== When running as a standalone application + +If you're using the client in a standalone mode outside any container that handles +Stork initialization for you, you might need two additional steps to get it working: + +Add a configuration provider for Stork, such as + +.... + + io.smallrye.stork + stork-microprofile-config + +.... + +And before initializing any GraphQL client instances, make sure that Stork is initialized +by calling `io.smallrye.stork.Stork.initialize()` once. diff --git a/content/docs/smallrye-graphql/2/_attributes.adoc b/content/docs/smallrye-graphql/2/_attributes.adoc new file mode 100644 index 000000000..40c49b10d --- /dev/null +++ b/content/docs/smallrye-graphql/2/_attributes.adoc @@ -0,0 +1,20 @@ +:project-version: 2.18.5 +// Attributes shared by every page. smallrye.io overrides the ones it needs when it publishes. +// Pages in subdirectories set :version-root: (../ per level) before including this file. +ifndef::version-root[:version-root:] +:idprefix: +:idseparator: - +:sectanchors: +:examples-dir: {version-root}_examples +:partialsdir: {version-root}_partials +:project-name: SmallRye GraphQL +:javadoc-version: latest +:generated-dir: {version-root}_generated + +// Added by smallrye.io (tools/docs/Docs.java sync) +:javadoc-version: 2.18.5 +:relfileprefix: /docs/smallrye-graphql/2/ +:imagesdir: /docs/smallrye-graphql/2 +:docs-project: smallrye-graphql +:docs-version: 2 +:docs-ref: 2.18.5 diff --git a/content/docs/smallrye-graphql/2/_examples/examples/dynamicclient/MyClientUsage.java b/content/docs/smallrye-graphql/2/_examples/examples/dynamicclient/MyClientUsage.java new file mode 100644 index 000000000..f0d731ca8 --- /dev/null +++ b/content/docs/smallrye-graphql/2/_examples/examples/dynamicclient/MyClientUsage.java @@ -0,0 +1,37 @@ +package examples.dynamicclient; + +import examples.typesafeclient.SuperHero; +import io.smallrye.graphql.client.GraphQLClient; +import io.smallrye.graphql.client.Response; +import io.smallrye.graphql.client.core.Document; +import io.smallrye.graphql.client.dynamic.api.DynamicGraphQLClient; + +import jakarta.inject.Inject; +import jakarta.json.JsonArray; +import java.util.List; +import java.util.concurrent.ExecutionException; + +import static io.smallrye.graphql.client.core.Argument.arg; +import static io.smallrye.graphql.client.core.Argument.args; +import static io.smallrye.graphql.client.core.Document.document; +import static io.smallrye.graphql.client.core.Field.field; +import static io.smallrye.graphql.client.core.Operation.operation; + +public class MyClientUsage { + + @Inject + @GraphQLClient("superHeroes") + DynamicGraphQLClient client; + + public void execute() throws ExecutionException, InterruptedException { + Document document = document( // <1> + operation(field("allHeroesIn", + args(arg("location", "Outer Space")), + field("name"), + field("superPowers")))); + Response response = client.executeSync(document); // <2> + + JsonArray heroesArray = response.getData().getJsonArray("allHeroesIn"); // <3> + List heroes = response.getList(SuperHero.class, "allHeroesIn"); // <4> + } +} diff --git a/content/docs/smallrye-graphql/2/_examples/examples/dynamicclient/MyClientUsageString.java b/content/docs/smallrye-graphql/2/_examples/examples/dynamicclient/MyClientUsageString.java new file mode 100644 index 000000000..c761d64b1 --- /dev/null +++ b/content/docs/smallrye-graphql/2/_examples/examples/dynamicclient/MyClientUsageString.java @@ -0,0 +1,39 @@ +package examples.dynamicclient; + +import io.smallrye.graphql.client.GraphQLClient; +import io.smallrye.graphql.client.Response; +import io.smallrye.graphql.client.dynamic.api.DynamicGraphQLClient; + +import jakarta.inject.Inject; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutionException; + +public class MyClientUsageString { + + @Inject + @GraphQLClient("superheroes") + DynamicGraphQLClient client; + + public void execute() throws ExecutionException, InterruptedException { + String queryWithInlineVariables = // <1> + "query {" + + " allHeroesIn(location: \"Outer Space\") {" + + " name" + + " superPowers" + + " }" + + "}"; + Response response = client.executeSync(queryWithInlineVariables); + + String queryWithExtractedVariables = // <2> + "query($loc: String) {" + + " allHeroesIn(location: $loc) {" + + " name" + + " superPowers" + + " }" + + "}"; + Map variables = new HashMap<>(); // <3> + variables.put("loc", "Outer Space"); + Response response2 = client.executeSync(queryWithExtractedVariables, variables); + } +} diff --git a/content/docs/smallrye-graphql/2/_examples/examples/typesafeclient/MyClientUsage.java b/content/docs/smallrye-graphql/2/_examples/examples/typesafeclient/MyClientUsage.java new file mode 100644 index 000000000..be521d196 --- /dev/null +++ b/content/docs/smallrye-graphql/2/_examples/examples/typesafeclient/MyClientUsage.java @@ -0,0 +1,16 @@ +package examples.typesafeclient; + +import jakarta.inject.Inject; +import java.util.List; + +public class MyClientUsage { + + @Inject + SuperHeroesApi superHeroesApi; + + public void execute() { + List allHeroes = superHeroesApi.allHeroesIn("Outer Space"); + // ... + } + +} diff --git a/content/docs/smallrye-graphql/2/_examples/examples/typesafeclient/SuperHero.java b/content/docs/smallrye-graphql/2/_examples/examples/typesafeclient/SuperHero.java new file mode 100644 index 000000000..7c19a1119 --- /dev/null +++ b/content/docs/smallrye-graphql/2/_examples/examples/typesafeclient/SuperHero.java @@ -0,0 +1,12 @@ +package examples.typesafeclient; + +import java.util.List; + +public class SuperHero { + + private String name; + private List superPowers; + + // plus getters and setters + +} diff --git a/content/docs/smallrye-graphql/2/_examples/examples/typesafeclient/SuperHeroesApi.java b/content/docs/smallrye-graphql/2/_examples/examples/typesafeclient/SuperHeroesApi.java new file mode 100644 index 000000000..0fae71966 --- /dev/null +++ b/content/docs/smallrye-graphql/2/_examples/examples/typesafeclient/SuperHeroesApi.java @@ -0,0 +1,12 @@ +package examples.typesafeclient; + +import io.smallrye.graphql.client.typesafe.api.GraphQLClientApi; + +import java.util.List; + +@GraphQLClientApi +public interface SuperHeroesApi { + + List allHeroesIn(String location); + +} diff --git a/content/docs/smallrye-graphql/2/_nav.yml b/content/docs/smallrye-graphql/2/_nav.yml new file mode 100644 index 000000000..69ee5af0b --- /dev/null +++ b/content/docs/smallrye-graphql/2/_nav.yml @@ -0,0 +1,74 @@ +title: SmallRye GraphQL +nav: +- title: Overview + page: index +- title: Server side features + items: + - title: Customizing JSON (de)serializers + page: custom-json-serializers-deserializers + - title: Directives + page: directives + - title: Federation + page: federation + - title: Custom error extensions + page: custom-error-extensions + - title: Response extensions + page: extensions + - title: Returning Void from Mutations + page: mutation-void + - title: Handling of the WebSocket's init-payload + page: handling-init-payload-from-the-websocket + - title: Custom scalars + page: custom-scalar + - title: Inspecting executable directives + page: inspecting-executable-directives-on-server-side + - title: Namespaces + page: namespaces-on-server-side + - title: Kotlin support + page: kotlin +- title: Typesafe client + items: + - title: Basic usage + page: typesafe-client-usage + - title: Unions and Interfaces + page: typesafe-client-unions-and-interfaces + - title: Reactive + page: typesafe-client-reactive-types + - title: Custom scalar types + page: typesafe-client-custom-scalars + - title: Adding headers + page: typesafe-client-headers + - title: Logging + page: typesafe-client-logging + - title: Running multiple queries at once + page: typesafe-client-multiple-queries + - title: Error handling + page: typesafe-client-error-handling + - title: Accessing metadata of responses + page: typesafe-client-accessing-metadata-of-responses + - title: Static Directives + page: typesafe-client-directives +- title: Dynamic client + items: + - title: Basic usage + page: dynamic-client-usage + - title: Error handling + page: dynamic-client-error-handling +- title: Clients in general + items: + - title: Bootstrapping the client + page: client-standalone + - title: Using SmallRye Stork + page: using-smallrye-stork +- title: Developer tools + items: + - title: Maven plugin + page: maven-plugin + - title: Gradle plugin + page: gradle-plugin +- title: Reference material + items: + - title: Server-side configuration + page: server-configuration + - title: Client-side configuration + page: client-configuration diff --git a/content/docs/smallrye-graphql/2/client-configuration.adoc b/content/docs/smallrye-graphql/2/client-configuration.adoc new file mode 100644 index 000000000..7da22fddd --- /dev/null +++ b/content/docs/smallrye-graphql/2/client-configuration.adoc @@ -0,0 +1,28 @@ += Client-side configuration properties +include::_attributes.adoc[] + +Note: if you are using Quarkus, it is recommended to use the `quarkus.++*++` property counterparts instead. +See https://quarkus.io/guides/all-config#quarkus-smallrye-graphql-client_quarkus-smallrye-graphql-client-smallrye-graphql-client[Quarkus Documentation] for more info. + +[width="100%",cols="18%,10%,72%",options="header",] +|=== +|Property |Default value |Meaning +|`CLIENT++_++NAME/mp-graphql/url` |none |Denotes URL to connect to +|`CLIENT++_++NAME/mp-graphql/header/KEY` |none |Adds a HTTP header named `KEY` to all HTTP requests performed by the client +|`CLIENT++_++NAME/mp-graphql/subprotocols` |`graphql-ws,graphql-transport-ws` |Comma-separated list of websocket subprotocols supported by this client. We currently support `graphql-ws` and `graphql-transport-ws`. If multiple subprotocols are provided, choosing the actual subprotocol will be subject to negotiation with the server. +|`CLIENT++_++NAME/mp-graphql/keystore` |none |Path to client's keystore (for example `file:/path/to/keystore` or `classpath:path/to/keystore`) +|`CLIENT++_++NAME/mp-graphql/keystoreType` |`JKS` |Keystore type +|`CLIENT++_++NAME/mp-graphql/keystorePassword` |none |Keystore password +|`CLIENT++_++NAME/mp-graphql/truststore` |none |Path to client's truststore (for example `file:/path/to/truststore` or `classpath:path/to/truststore`) +|`CLIENT++_++NAME/mp-graphql/truststoreType` |`JKS` |Truststore type +|`CLIENT++_++NAME/mp-graphql/truststorePassword` |none |Truststore password +|`CLIENT++_++NAME/mp-graphql/proxyHost` |none |Hostname of the proxy to use +|`CLIENT++_++NAME/mp-graphql/proxyPort` |none |Port of the proxy to use +|`CLIENT++_++NAME/mp-graphql/proxyUsername` |none |Username for the proxy to use +|`CLIENT++_++NAME/mp-graphql/proxyPassword` |none |Password for the proxy to use +|`CLIENT++_++NAME/mp-graphql/maxRedirects` |16 |Max number of redirects to follow. Set to 0 to disable redirects. +|`CLIENT++_++NAME/mp-graphql/websocketInitializationTimeout` |none |Maximum time in milliseconds that will be allowed to wait for the server to acknowledge a websocket connection. +|`CLIENT++_++NAME/mp-graphql/runSingleOperationsOverWebsocket` |`false` |If true, then queries and mutations will run over the websocket transport rather than pure HTTP. Off by default, because it has higher overhead. +|`CLIENT++_++NAME/mp-graphql/initPayload/KEY` |none |Adds a property named `KEY` to the `connection++_++init` message payload when negotiating a websocket connection. All values will be treated as string. For other types instatiate the API with the builder. +|`CLIENT++_++NAME/mp-graphql/allowUnexpectedResponseFields` |`false` |If true and there is an unexpected field in the response payload, it won't throw an error, and it will log a warning message. +|=== diff --git a/content/docs/smallrye-graphql/2/client-standalone.adoc b/content/docs/smallrye-graphql/2/client-standalone.adoc new file mode 100644 index 000000000..e991a36b2 --- /dev/null +++ b/content/docs/smallrye-graphql/2/client-standalone.adoc @@ -0,0 +1,76 @@ += Bootstraping GraphQL clients +include::_attributes.adoc[] + +In general, it is recommended to use SmallRye GraphQL client through the +https://quarkus.io/guides/smallrye-graphql-client[Quarkus extension], +or the https://github.com/wildfly-extras/wildfly-graphql-feature-pack/wiki/GraphQL-client-guide[WildFly feature pack], +each of them having their own documentation about how to set up dependencies and bootstrap a client. +It is however also possible to use SmallRye GraphQL Client in a standalone application outside any container. +The single dependency that you need is `io.smallrye:smallrye-graphql-client-implementation-vertx`, as the +Vert.x based implementation is currently the only supported one. + +If you're using the client as a script and notice that the JVM doesn't exit even after you close the client instance, +it's probably because the client is maintaining a `Vertx` instance that it created automatically. To solve this +issue, you might need to create a `Vertx` instance on your own, pass it to the client builder, and then close it after +closing the client. See the following JBang snippet for an example. + +== Logging with a standalone (Java SE) application using the SmallRye GraphQL Client + +If you want to properly configure logging in a standalone application, you +will also need to add a logging implementation to the classpath. The client +code uses the JBoss Logging facade, so you can, for example, use the JBoss +Logmanager. Add the `org.jboss.logmanager:jboss-logmanager` dependency to +your project(it needs to be on the runtime classpath). You will also need to +run the application with this system property declared: +`-Djava.util.logging.manager=org.jboss.logmanager.LogManager`. + +To customize logging levels etc, it picks up the `logging.properties` file +from the root of the runtime classpath, so, for example if using Maven and +the exec plugin to start the application, you would place the file into +`src/main/resources/logging.properties`. For an example of the contents of +this file, see +https://github.com/jamezp/jboss-logging-example/blob/master/src/main/resources/logging.properties. + +== Using with JBang + +This is a full script runnable directly with https://www.jbang.dev/[JBang] that +uses a dynamic client for connecting to https://countries.trevorblades.com[countries.trevorblades.com] +to obtain a list of countries from its database. + +[source,java] +---- +///usr/bin/env jbang "$0" "$@" ; exit $? +//DEPS io.smallrye:smallrye-graphql-client-implementation-vertx:RELEASE + +import io.smallrye.graphql.client.dynamic.api.DynamicGraphQLClient; +import io.smallrye.graphql.client.Response; +import io.smallrye.graphql.client.vertx.dynamic.VertxDynamicGraphQLClientBuilder; + +import io.vertx.core.Vertx; + +// Has a multiline string literal, requires Java 15+! +class Client { + public static void main(String... args) throws Exception { + Vertx vertx = Vertx.vertx(); + DynamicGraphQLClient client = new VertxDynamicGraphQLClientBuilder() + .url("https://countries.trevorblades.com") + .vertx(vertx) + .build(); + try { + Response response = client.executeSync(""" + query { + countries { + name + } + } + """); + System.out.println(response); + } finally { + client.close(); + vertx.close(); + } + } +} +---- + +Save this file as `Client.java` and execute with `jbang Client.java`. diff --git a/content/docs/smallrye-graphql/2/custom-error-extensions.adoc b/content/docs/smallrye-graphql/2/custom-error-extensions.adoc new file mode 100644 index 000000000..82aa86e5b --- /dev/null +++ b/content/docs/smallrye-graphql/2/custom-error-extensions.adoc @@ -0,0 +1,44 @@ += GraphQL Error Extensions +include::_attributes.adoc[] + +Exceptions are reported in GraphQL in the `errors` array, next to the `data` field, so it's possible to return partial results, e.g.: + +[source,json] +---- +{ + "data": { + "superHero": { + "name": "Wolverine", + "location": null + } + }, + "errors": [{ + "message":"location unknown", + "path": ["superHero","location"], + "extensions":{"code":"location-unknown"} + }] +} +---- + +The `location` field couldn't be returned, and in the `errors`, there's the reason with a few predefined fields, +and a map of `extensions` that can contain custom details about the error. + +You can add your own extensions by implementing the `io.smallrye.graphql.api.ErrorExtensionProvider` interface and +adding your class name to a file `META-INF/services/io.smallrye.graphql.api.ErrorExtensionProvider` (this is a `ServiceLoader`). + +As an example, this class provides an extension named `exception-length` with the length of the simple class name of the exception: + +[source,java] +---- +public class ExceptionLengthErrorExtensionProvider implements ErrorExtensionProvider { + @Override + public String getKey() { + return "exception-length"; + } + + @Override + public JsonNumber mapValueFrom(Throwable exception) { + return Json.createValue(exception.getClass().getSimpleName().length()); + } +} +---- diff --git a/content/docs/smallrye-graphql/2/custom-json-serializers-deserializers.adoc b/content/docs/smallrye-graphql/2/custom-json-serializers-deserializers.adoc new file mode 100644 index 000000000..70d87f6b6 --- /dev/null +++ b/content/docs/smallrye-graphql/2/custom-json-serializers-deserializers.adoc @@ -0,0 +1,36 @@ += Customizing JSON-B serializers/deserializers +include::_attributes.adoc[] + +If your application needs finer-grained control over JSON serialization/deserialization than you can achieve via +formatting annotations like `@JsonbDateFormat`, you may plug in your own custom instances of the `Jsonb` class +for each input type that your GraphQL application exposes. + +To do this, you'll need to create an implementation of `io.smallrye.graphql.spi.EventingService` +and implement its `overrideJsonbConfig` method. +An example that plugs in a custom date format for a particular class that is used as input: + +.... +package org.acme.custom.json.config; + +public class CustomJsonbService implements EventingService { + + @Override + public String getConfigKey() { + return null; // activate this service always regardless of the configuration + } + + @Override + public Map overrideJsonbConfig() { + JsonbConfig config = new JsonbConfig().withDateFormat("MM dd yyyy HH:mm Z", null).withDeserializers(OBJECTID_DESERIALIZER).withSerializers( + OBJECTID_SERIALIZER); + return Collections.singletonMap("MyModelClass.class.getName()", JsonbBuilder.create(config)); + } +} +.... + +We can customize the JsonbConfig as per our need in above example we have registered additional serializer and date formatting for the class MyModelClass. + +As the discovery of eventing services uses the ServiceLoader mechanism, don't forget to add a +`META-INF/services/io.smallrye.graphql.spi.EventingService` file that contains the fully qualified +name of your implementation. +Example `org.acme.custom.json.config.CustomJsonbService` diff --git a/content/docs/smallrye-graphql/2/custom-scalar.adoc b/content/docs/smallrye-graphql/2/custom-scalar.adoc new file mode 100644 index 000000000..27be60b11 --- /dev/null +++ b/content/docs/smallrye-graphql/2/custom-scalar.adoc @@ -0,0 +1,36 @@ += Creating GraphQL Custom Scalars with SmallRye GraphQL +include::_attributes.adoc[] + +While by default *MicroProfile GraphQL* specification doesn't provide direct support for creating +custom scalars for GraphQL literal types of String, Int and Float, *SmallRye GraphQL* has implemented +this feature based on user feedback. To implement a *custom scalar* with *SmallRye GraphQL*, +you can annotate your custom scalar class with `@CustomScalar` in addition to following the pattern below +of including a public constructor that takes one of `String`++|++`BigDecimal`++|++`BigInteger` depending on +the scalar type: + +[source,java] +---- + @CustomScalar("BigDecimalString") + public class BigDecimalString implements CustomStringScalar { + public BigDecimalString(String stringValue) { + ... + } + @Override + public String stringValueForSerialization() { + ... + } +} +---- + +In this example, `BigDecimalString` implements the `CustomStringScalar` which is used to identify the +proper (de) serialization for BigDecimalString. `BigDecimalString` also provides a single argument +constructor which takes a String. Finally, `BigDecimalString` implements +`stringValueForSerialization()` which provides the String representation to be used during +serialization. + +____ +++[++NOTE++]++ +If the user wants to create a literal for GraphQL Int or Float, they would implement either +CustomIntScalar with the intValueForSerialization method or CustomFloatScalar with the +floatValueForSerialization method respectively. +____ diff --git a/content/docs/smallrye-graphql/2/directives.adoc b/content/docs/smallrye-graphql/2/directives.adoc new file mode 100644 index 000000000..34b420ca8 --- /dev/null +++ b/content/docs/smallrye-graphql/2/directives.adoc @@ -0,0 +1,45 @@ += Directives +include::_attributes.adoc[] + +== Custom Directives + +You can add your own https://spec.graphql.org/draft/#sec-Language.Directives[GraphQL Directives] by writing +a corresponding Java Annotation and annotate it as `@Directive`, e.g.: + +[source,java] +---- +@Directive(on = { OBJECT, INTERFACE }) +@Description("Just a test") +@Retention(RUNTIME) +public @interface MyDirective { +} +---- + +Directives can be repeatable, see the `@Key` annotation for an example. + +== Directives generated from Bean Validation annotations + +If your project uses Bean Validation to validate fields on input types and operation arguments, and you enable +inclusion of directives in the schema (by setting `smallrye.graphql.schema.includeDirectives=true`), +then constraints decoded from annotations will be added to your schema as directives. This is currently only +supported for some built-in constraints (annotations from the +`javax.validation.constraints` package), and custom constraints aren't supported at all. + +Each bean validation annotation is mapped to a single `@constraint` directive. The directive is declared as repeatable, +so if you have multiple constraints on an input field, the field will contain multiple `@constraint` directives. +The following table describes the mapping between BV annotations and `@constraint` directives (all currently supported +BV annotations are listed here): + +[cols=",",options="header",] +|=== +|BV annotation |GraphQL directive +|`@Size(MIN, MAX)` |`@constraint(minLength=MIN, maxLength=MAX)` +|`@Email` |`@constraint(format='email')` +|`@Max` |`@constraint(max=VALUE)` +|`@Min` |`@constraint(min=VALUE)` +|`@Pattern(REGEXP)` |`@constraint(pattern=REGEXP)` +|=== + +Note: The `@NotNull` annotation does not map to a directive, instead it makes the GraphQL type non-nullable. + +Constraints will only appear on fields of input types and operation arguments. diff --git a/content/docs/smallrye-graphql/2/dynamic-client-error-handling.adoc b/content/docs/smallrye-graphql/2/dynamic-client-error-handling.adoc new file mode 100644 index 000000000..e7a9b5f79 --- /dev/null +++ b/content/docs/smallrye-graphql/2/dynamic-client-error-handling.adoc @@ -0,0 +1,34 @@ += Error handling with dynamic clients +include::_attributes.adoc[] + +== System-level errors + +If the service returns a system-level error (which means the response body doesn't contain +a valid GraphQL response document), the client invocation will (instead of returning a +`io.smallrye.graphql.client.Response`) throw a `io.smallrye.graphql.client.InvalidResponseException` +whose message contains information about the received status code, status message, and body +contents. + +== Application-level errors + +If the service returns one or more application-level errors (which means that +there is valid GraphQL response in the body that has a non-empty `errors` entry), the +client invocation will return a `io.smallrye.graphql.client.Response` as normal, and that object +will contain information about the errors. Errors are represented as `io.smallrye.graphql.client.GraphQLError` +objects and can be inspected after retrieving a list of all errors using `response.getErrors()`. + +____ +++[++NOTE++]++ +An error response is considered application-level regardless of the HTTP status code as long as it +contains a valid GraphQL response. We define a GraphQL response as a well-formed JSON document +that contains at least one of `errors`, `data` and `extensions` entries, and no other entries beyond +that. +____ + +It is also possible to convert a response's errors into a `io.smallrye.graphql.client.GraphQLClientException` +by calling `throwExceptionIfErrors()` - the response has to be cast to `ResponseImpl` for this as of now. +This method will, if there are any errors in the response, convert the errors into a `GraphQLClientException` +and throw it. + +If you need to access the HTTP response code and message pertaining to a response that contained +application-level errors, refer to xref:dynamic-client-usage.adoc#accessing-http-headers-and-response-codes[Accessing HTTP headers and response codes]. diff --git a/content/docs/smallrye-graphql/2/dynamic-client-usage.adoc b/content/docs/smallrye-graphql/2/dynamic-client-usage.adoc new file mode 100644 index 000000000..26832e7d1 --- /dev/null +++ b/content/docs/smallrye-graphql/2/dynamic-client-usage.adoc @@ -0,0 +1,136 @@ += Dynamic client introduction and basic usage +include::_attributes.adoc[] + +A Java GraphQL client. The main difference from the typesafe client is +that while the typesafe client behaves like a typesafe proxy very +similar to the MicroProfile REST Client, the dynamic client is more like +the JAX-RS client from the `jakarta.ws.rs.client` package. Instead of +working with model classes directly, the dynamic client focuses on +programmatically working with GraphQL documents representing GraphQL +requests and responses. It still offers the option to convert between +documents and model classes when necessary. + +In the current implementation, Vert.x HTTP client is used for handling +the underlying traffic. + +Creating a client instance + +Generally there are two ways to obtain a client instance. + +First, using CDI injection where the configuration values are defined in system +properties: + +[source,java] +---- +@Inject +@GraphQLClient("superheroes") +DynamicGraphQLClient client; + +// assuming that this system property exists: +// superheroes/mp-graphql/url=https://superheroes.org/graphql +---- + +The above example assumes that configuration for the client is present in system properties. For a full list of +supported configuration properties, see xref:client-configuration.adoc[Client configuration reference] + +The other way to build a client is programmatically using a builder: + +[source,java] +---- +DynamicGraphQLClient client = DynamicGraphQLClientBuilder.newBuilder() + .url("https://superheroes.org/graphql") + .build(); +---- + +The usage examples in the following sections will assume using the first approach - injection. + +Basic Usage + +Given the following GraphQL service on the server side: + +[source,java] +---- +@GraphQLApi +class SuperHeroesApi { + @Query + List allHeroesIn(String location) { + // .... + } +} + +class SuperHero { + private String name; + private List superPowers; +} +---- + +Such service can be queried this way: + +[source,java] +---- +include::{examples-dir}/examples/dynamicclient/MyClientUsage.java[] +---- + +* ++<++1++>++ Creating the document representing the request. We used static +imports to make the code easy to read, they all come from the +classes in the `io.smallrye.graphql.client.core` package. +* ++<++2++>++ Executing the request. You can either do that in a blocking way, or +request a `Uni` if you prefer the reactive style. +* ++<++3++>++ Obtaining the resulting list of heroes as a `JsonArray`. +* ++<++4++>++ Obtaining the resulting list of heroes as instances of the model +class. This is optional, you can continue working with the data as a `JsonArray` if you prefer. + +Using plain strings instead of the DSL + +If you don't like the DSL for some reason and want to use plain strings for your queries, these two examples +will serve you: + +[source,java] +---- +include::{examples-dir}/examples/dynamicclient/MyClientUsageString.java[] +---- + +* ++<++1++>++: In this variant, we inline values for query arguments directly into the query string. +* ++<++2++>++: In this variant, argument values are extracted into variables. The `location` argument of the `allHeroesIn` query is +declared to be using the variable `loc` (the variable can also be named `location` same as the argument, if +you prefer). +* ++<++3++>++: Here we create a map that defines the values for each variable. Values of this map are `Object`s, +so you can put in strings, numbers, booleans, or any object that corresponds to a GraphQL type and can be +serialized to JSON. Inserting a `JsonObject` directly is also supported. + +Accessing HTTP headers and response codes + +To access HTTP transport metadata that was passed by the server, you can inspect the `Response` object. + +`Response.getTransportMeta("HEADER-NAME")` returns a `List++<++String++>++` containing (potentially multiple) values of the requested header. + +To get the HTTP status code or status message, these are stored inside the `transportMeta` map too. `ResponseImpl` +contains convenience methods to retrieve them: `ResponseImpl.getStatusCode()` and `ResponseImpl.getStatusMessage()`. +It's also possible to retrieve them directly without casting to `ResponseImpl` by calling +`Integer.valueOf(response.getTransportMeta().get("++<++status-code++>++").get(0))` and + +`response.getTransportMeta().get("++<++status-message++>++").get(0)`. + +HTTP headers, status codes and messages are only available for operations executed over pure HTTP, not via websockets! + +Sending extensions to the server + +The GraphQL over HTTP specification allows clients to send an `extensions` map alongside the query. To do this +with the dynamic client, build a `Request` using the `RequestImpl.builder` and pass it to `executeSync` or +`executeAsync`: + +[source,java] +---- +Map extensions = Map.of("myKey", "myValue", "number", 42); + +Request request = RequestImpl.builder("{ myQuery }") + .variables(variables) // optional + .operationName("MyOperation") // optional + .extensions(extensions) + .build(); + +Response response = client.executeSync(request); +---- + +On the server side, the extensions sent by the client can be read from `SmallRyeContext.getExtensionsFromClient()`. +This is separate from `SmallRyeContext.getAddedExtensions()`, which is used for adding extensions to the +outgoing response. diff --git a/content/docs/smallrye-graphql/2/extensions.adoc b/content/docs/smallrye-graphql/2/extensions.adoc new file mode 100644 index 000000000..bad6a5991 --- /dev/null +++ b/content/docs/smallrye-graphql/2/extensions.adoc @@ -0,0 +1,86 @@ += Extensions in SmallRye GraphQL +include::_attributes.adoc[] + +`SmallRyeContext` provides two separate extension mechanisms: + +* *Outbound extensions* (`addedExtensions`) — extensions added by the server and sent back to the client in the response. +* *Inbound extensions* (`extensionsFromClient`) — extensions received from the client as part of the incoming request. + +== Adding extensions to the response + +*SmallRye GraphQL* allows the user to add their own extensions inside the GraphQL response +which is received from the server. The `extensions` are stored next to the `data` field in the GraphQL response. + +[source,json] +---- +{ + "data": { + "shirt": { + "size": "L" + } + }, + "extensions": { + "country": "Germany", + "pi": 3.1415926535 + } +} +---- + +To add extensions, you need to `@Inject` an instance of `SmallRyeContext` in your `@GraphQLApi` annotated class. +After that, you can add your own extensions via the method `addExtension` +with its input parameters: `key : String` as an identification of the added extension, +and `value: Object` as a value of the extension. Value can be any given object that can be converted into JsonValue. + +As an example, this class below contains the query `getShirt` and during the http request, the query adds these extensions: +`++{++"country": "Germany", "pi": 3.1415926535}`. +These extensions will be sent back via response in the `extensions` field. + +[source,java] +---- +import jakarta.inject.Inject; + +@GraphQLApi +public class ShirtResources { + @Inject + SmallRyeContext smallRyeContext; + + @Query + public Shirt getShirt() { + smallRyeContext.addExtension("country", "Germany"); + smallRyeContext.addExtension("pi", 3.1415926535); + //... + } +} +---- + +____ +++[++NOTE++]++ +You can also use `getAddedExtensions()` to access the underlying map directly. +____ + +== Reading extensions from the client request + +Clients can send an `extensions` map alongside the query (as defined by the GraphQL over HTTP specification). +On the server side, these can be read via `SmallRyeContext.getExtensionsFromClient()`, which returns a +`Map++<++String, Object++>++` (or `null` if the client did not send any extensions). + +[source,java] +---- +import jakarta.inject.Inject; + +@GraphQLApi +public class MyApi { + @Inject + SmallRyeContext smallRyeContext; + + @Query + public String process() { + Map clientExtensions = smallRyeContext.getExtensionsFromClient(); + if (clientExtensions != null) { + String traceId = (String) clientExtensions.get("traceId"); + // ... + } + //... + } +} +---- diff --git a/content/docs/smallrye-graphql/2/federation.adoc b/content/docs/smallrye-graphql/2/federation.adoc new file mode 100644 index 000000000..827f544eb --- /dev/null +++ b/content/docs/smallrye-graphql/2/federation.adoc @@ -0,0 +1,213 @@ += Federation +include::_attributes.adoc[] + +Support for https://www.apollographql.com/docs/federation[GraphQL Federation] is enabled by default. If you add one of the federation annotations, the corresponding directives will be declared in your schema, and the additional Federation queries will be added automatically. You can also disable Federation completely by setting the `smallrye.graphql.federation.enabled` config key to `false`. + +You can add the Federation directives by using the equivalent Java annotation, e.g. to extend a `Product` entity with a `price` field, you can write a class: + +[source,java] +---- +package org.example.price; + +import org.eclipse.microprofile.graphql.Id; + +import io.smallrye.graphql.api.federation.Extends; +import io.smallrye.graphql.api.federation.Key; + +@Extends @Key(fields = @FieldSet("id")) +public class Product { + @Id + private String id; + + @Description("The price in cent") + private Integer price; + + // getters and setters omitted +} +---- + +And a normal query method that takes the `key` fields as a parameter and returns the requested type: + +[source,java] +---- +package org.example.price; + +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Id; +import org.eclipse.microprofile.graphql.Query; + +@GraphQLApi +public class Prices { + @Query + public Product product(@Id String id) { + return [...] + } +} +---- + +The GraphQL Schema then contains: + +[source,graphql] +---- +type Product @extends @key(fields : "id") { + id: ID + price: Int +} + +union _Entity = Product + +type Query { + _entities(representations: [_Any!]!): [_Entity]! + _service: _Service! + product(id: ID): Product +} +---- + +If you can resolve, e.g., the product with different types of ids, you can add multiple `@Key` annotations. +## Federation Batch Resolver + +For better performance, there is the option to use batch resolvers with federation. This is not activated by default. It can be activated by setting `smallrye.graphql.federation.batchResolving.enabled` config key to `true`. + +It is unnecessary to provide batch resolvers for all entities, if there is no batch resolver, a non-batch resolver is used. + +[source,java] +---- +package org.example.price; + +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Id; +import org.eclipse.microprofile.graphql.Query; + +@GraphQLApi +public class Prices { + @Query + public List product(@Id List id) { + return [...] + } +} +---- + +It is crucial that the sequence of the argument list matches the order of the result list. Currently, the name of the Argument `id` must match with the property name in the type. + +== Federation Reference Resolver + +In federation, you may also want to extend the external type by some fields without publishing queries into the schema. You can do it by using the annotation `@Resolver`. + +[source,java] +---- +@Extends +@Key(fields = @FieldSet("upc")) +public final class Product { + @External + @NonNull + private String upc; + @External + private Integer weight; + @External + private Integer price; + private Boolean inStock; + @Requires(fields = @FieldSet("price weight")) + private Integer shippingPrice; +} + +@GraphQLApi +public class Api { + @Query // 0 query, that will be added into schema + public Product findByUPC(String upc) { + return new Product(upc , ...etc); + } + + @Resolver // 1 You dont receive external fields price weight here, just key + public Product resolveByUPC(String upc) { + return new Product(upc , ...etc); + } + + @Resolver // 2 The order of variables doesn't matter + public Product resolveByUPCForShipping(int price, String upc, @Name("weight") int someWeight) { + return new Product(upc , someWeight, price, (price * someWeight) /*calculate shippingPrice */, ...etc); + } + + @Resolver // 3 + public Product resolveByUPCForSource(int price, String upc) { + return new Product(upc, price, ...etc); + } + + @Requires(fields = @FieldSet("price")) + public int anotherWeight(@Source Product product) { + return product.price() * 2; + } +} +---- + +Generates the following schema: + +.... +type Product @extends @key(fields : "upc") { + anotherWeight: Int! @requires(fields : "price") + inStock: Boolean + price: Int @external + shippingPrice: Int @requires(fields : "price weight") + upc: String! @external + weight: Int @external +} + +type Query { + _entities(representations: [_Any!]!): [_Entity]! + _service: _Service! +} +.... + +These methods will only be available to the federation router, which may send requests like the following examples: + +.... +// request 1 +query { + _entities(representations: [{ + "__typename": "Product", + "upc": "1" // just id key + }]) { + __typename + ... on Product { + inStock + } + } +} + +// request 2 +query { + _entities(representations: [{ + "__typename": "Product", + "upc": "1", // id key + "price": 100, // shippingPrice requires this field + "weight": 100 // shippingPrice requires this field + }]) { + __typename + ... on Product { + inStock + shippingPrice + } + } +} + +// request 3 +query { + _entities(representations: [{ + "__typename": "Product", + "upc": "2", + "price": 1299 // anotherWeight requires this field + } + ]) { + __typename + ... on Product { + anotherWeight + } + } +} +.... + +Unfortunately, you must make separate methods with different `@External` parameters. + +It is not currently possible to combine them into one separate type. + +You also can use `@Query` (if you want to add queries into the schema) or `@Resolver` (requests 0 and 1). +And if it was requested, `++_++entities` - `@Resolvers` methods are checked first (they have higher priority). diff --git a/content/docs/smallrye-graphql/2/gradle-plugin.adoc b/content/docs/smallrye-graphql/2/gradle-plugin.adoc new file mode 100644 index 000000000..843bf33df --- /dev/null +++ b/content/docs/smallrye-graphql/2/gradle-plugin.adoc @@ -0,0 +1,84 @@ += SmallRye GraphQL Gradle plugin +include::_attributes.adoc[] + +This Gradle plugin allows you to generate the GraphQL Schema on build and save it as a text file. + +Add this to your build.gradle: + +.... +plugins { + id 'io.smallrye.graphql' version 'PLUGIN_VERSION' +} +.... + +The list of existing plugin versions (to substitute the +`PLUGIN++_++VERSION` placeholder) can be found +https://plugins.gradle.org/plugin/io.smallrye.graphql[here]. + +The schema will appear as `build/generated/schema.graphql` by default. + +The plugin’s `generateSchema` task can customized like: + +.... +generateSchema { + destination = null + includeTransitiveDependencies = true + includeScalars = true +} +.... + +This will include the project’s dependencies (including the transitive +dependencies) to scan for GraphQL model classes. As the destination is +set to `null` the generated schema will only be output to the console +and saved to a file. + +In case external dependencies need to be scanned for GraphQL model +classes a more advanced configuration could look like: + +.... +configrations { + graphQLSchema +} + +dependencies { + graphQLSchema .. +} + +generateSchema { + includeDependencies = true + configurations = ["graphQLSchema"] +} +.... + +This defines a separate `graphQLSchema` configuration. In the +dependencies block the external dependencies containing the GraphQL mode +class should then be registered for the `graphQLSchema` configuration. +The `generateSchema` task is then customized to only include +dependencies from the `graphQLSchema` configuration. Also note that we +set `includeDependencies` to `true` (and not +`includeTransitiveDependencies`). This will make sure only direct +dependencies will be be scanned and not possible transitive +dependencies. + +Configuration options + +* `destination` - To override the default +`build/generated/schema.graphql` destination. +* `includeDependencies` - Scan project’s dependencies for GraphQL +model classes too. Default false. +* `includeTransitiveDependencies` - Scan project’s dependencies +(including the transitive dependencies) for GraphQL model classes +too. Default false. +* `configurations` - If the above `includeDependencies` or +`includeTransitiveDependencies` is true, you can control what +configurations should be included. Default is `implementation`. +* `dependencyExtensions` - If the above `includeDependencies` or +`includeTransitiveDependencies` is true, you can control what +dependency extensions should be included. Default is `jar`. +* `includeScalars` - Include scalars in the schema. Default false. +* `includeDirectives` - Include directives in the schema. Default +false. +* `includeSchemaDefinition` - Include the schema definition. Default +false. +* `includeIntrospectionTypes` - Include the introspection types in the +schema. Default false. diff --git a/content/docs/smallrye-graphql/2/handling-init-payload-from-the-websocket.adoc b/content/docs/smallrye-graphql/2/handling-init-payload-from-the-websocket.adoc new file mode 100644 index 000000000..a691d6802 --- /dev/null +++ b/content/docs/smallrye-graphql/2/handling-init-payload-from-the-websocket.adoc @@ -0,0 +1,25 @@ += Handling of the WebSocket's init-payload +include::_attributes.adoc[] + +When executing GraphQL operations over WebSockets (both with the `graphql-ws` and `graphql-transport-ws` protocols), +the client can send an initial payload +to the server when establishing the WebSocket connection. This payload can contain +any information that the server might need to process GraphQL requests, like client agent identification (similar to the `User-Agent` header) or data specific to the client. On the server-side, you can access this payload +via the `GraphQLContext`, which is available through the `DataFetchingEnvironment` object. + +[source,java] +---- +@GraphQLApi +public class HeroResources { + + @Inject + SmallryeContext context; + + // ... + Map initPayload = smallRyeContext + .getDataFetchingEnvironment() + .getGraphQlContext() + .get("init-payload"); + // ... +} +---- diff --git a/content/docs/smallrye-graphql/2/images/redhat_reversed.svg b/content/docs/smallrye-graphql/2/images/redhat_reversed.svg new file mode 100644 index 000000000..37fe83c48 --- /dev/null +++ b/content/docs/smallrye-graphql/2/images/redhat_reversed.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/content/docs/smallrye-graphql/2/index.adoc b/content/docs/smallrye-graphql/2/index.adoc new file mode 100644 index 000000000..f277286e0 --- /dev/null +++ b/content/docs/smallrye-graphql/2/index.adoc @@ -0,0 +1,6 @@ += Overview +include::_attributes.adoc[] + +SmallRye GraphQL is a library that allows you to easily use GraphQL (both server-side endpoints +and client-side code) in your Java-based projects. It implements +the MicroProfile GraphQL specification. diff --git a/content/docs/smallrye-graphql/2/inspecting-executable-directives-on-server-side.adoc b/content/docs/smallrye-graphql/2/inspecting-executable-directives-on-server-side.adoc new file mode 100644 index 000000000..cd37d1852 --- /dev/null +++ b/content/docs/smallrye-graphql/2/inspecting-executable-directives-on-server-side.adoc @@ -0,0 +1,39 @@ += Inspecting executable directives on the server side +include::_attributes.adoc[] + +Let's say you have this _GraphQL query_ with an executable directive on a field _location_. + +.... +{ + hero(name: $nameValue) { + name + location @customDirective(arg: true) + } +} +.... + +To inspect your directive on the server side, you must inject a `SmallRyeContext` object and get the relevant information like this. + +[source,java] +---- + @Inject + SmallRyeContext context; + + @Query + public SuperHero getHero(String name) { + // ... + SelectionSet selectionSet = (SelectionSet) context.getDataFetchingEnvironment().getField().getChildren().get(0); + Optional locationNode = selectionSet.getChildren().stream() + .filter(node -> node instanceof Field && ((Field) node).getName().equals("location")).findAny(); // "location" is the field name where the directive is applied. + locationNode.ifPresent(node -> { + List directives = ((Field) node).getDirectives(); + System.out.println(directives); + }); + // the rest of the query... + } +---- + +____ +++[++NOTE++]++ +that `Field` refers to `graphql.language.Field`, not `io.smallrye.graphql.schema.model.Field` +____ diff --git a/content/docs/smallrye-graphql/2/kotlin.adoc b/content/docs/smallrye-graphql/2/kotlin.adoc new file mode 100644 index 000000000..b6ccf535d --- /dev/null +++ b/content/docs/smallrye-graphql/2/kotlin.adoc @@ -0,0 +1,4 @@ += SmallRye Graphql in Kotlin +include::_attributes.adoc[] + +When working with SmallRye GraphQL in Kotlin, it is very important to use the `-Xemit-jvm-type-annotations` flag for the Kotlin compiler. This flag is important to generate the necessary annotations for reflection to work properly. The flag is supported as of Kotlin 1.3.70. diff --git a/content/docs/smallrye-graphql/2/maven-plugin.adoc b/content/docs/smallrye-graphql/2/maven-plugin.adoc new file mode 100644 index 000000000..959ef8596 --- /dev/null +++ b/content/docs/smallrye-graphql/2/maven-plugin.adoc @@ -0,0 +1,48 @@ += SmallRye GraphQL Maven plugin +include::_attributes.adoc[] + +This Maven plugin allows you to generate the GraphQL Schema on build and save it as a text file. + +Add this to your pom.xml: + +[source,xml] +---- + + smallrye-graphql-maven-plugin + io.smallrye + + + + generate-schema + + + + +---- + +The schema will appear as `target/generated/schema.graphql` by default. The goal is bound to the `process-classes` +build phase. + +Configuration options + +* `destination` - To override the default `target/generated/schema.graphql` destination +* `includeDependencies` - Scan project’s dependencies for GraphQL +model classes too. This is necessary if your GraphQL endpoint exposes +classes that are not in the application's main module. Off (`false`) by default, because +scanning a potentially large dependency tree slows down execution substantially. +Usage of reactive types does not require turning this option on. +* `includeDependenciesScopes` - If the above `includeDependencies` is +true, you can control what scopes should be included. Default is +`compile, system` +* `includeDependenciesTypes` - If the above `includeDependencies` is +true, you can control what types should be included. Default is +`jar` +* `includeScalars` - Include scalars in the schema. Default false. +* `includeDirectives` - Include directives in the schema. Default false. +* `includeSchemaDefinition` - Include the schema definition. Default false. +* `includeIntrospectionTypes` - Include the introspection types in the schema. Default false. +* `federationEnabled` - Enable GraphQL Federation. This is automatically +enabled if any Federation-related annotations are found in your application, otherwise the default is false. +You should generally use this in conjunction with `includeDirectives`. +* `typeAutoNameStrategy` - Strategy for transforming class names into GraphQL type names. +Valid values are `MergeInnerClass`, `Full` and`Default`. diff --git a/content/docs/smallrye-graphql/2/mutation-void.adoc b/content/docs/smallrye-graphql/2/mutation-void.adoc new file mode 100644 index 000000000..4fc047e25 --- /dev/null +++ b/content/docs/smallrye-graphql/2/mutation-void.adoc @@ -0,0 +1,24 @@ += Returning Void Type in Mutations with SmallRye GraphQL +include::_attributes.adoc[] + +While by default *MicroProfile GraphQL* specification doesn't provide direct support for returning `void` type from mutation operations, *SmallRye GraphQL* has implemented this feature based on user feedback. + +To implement a *mutation* operation that returns `void` or `Void` type, you can use the following pattern in your annotated `@GraphQLApi` class: + +[source,java] +---- + @Mutation + public void createHero(SuperHero hero) { + superHeroService.create(hero); + } +---- + +In this example, `createHero()` receives a SuperHero object as input, +performs some operation with it (in this case, creating it using a +`superHeroService`). + +____ +++[++NOTE++]++ +On the GraphQL level, this is achieved by returning a special scalar type named Void from the mutation. +This scalar type will always have a value of null. +____ diff --git a/content/docs/smallrye-graphql/2/namespaces-on-server-side.adoc b/content/docs/smallrye-graphql/2/namespaces-on-server-side.adoc new file mode 100644 index 000000000..896ad6537 --- /dev/null +++ b/content/docs/smallrye-graphql/2/namespaces-on-server-side.adoc @@ -0,0 +1,167 @@ += Namespacing on the server side +include::_attributes.adoc[] + +____ +++[++NOTE++]++ +Using approaches to form namespaces in the schema can be useful for large APIs. There are several ways to do this. +However, read the documentation carefully, especially the limitations and possible problems. +____ + +____ +++[++NOTE++]++ You can only use one of the annotations - `@Name` or `@Namespace` over the GraphQLApi classes. +____ + +== Using @Namespace annotation + +The annotation accepts an array of strings containing the nesting of the namespace. +This method allows you to create any nesting of namespaces. +You can use any nesting and also combine different levels. + +[source,java] +---- +@GraphQLApi +@Namespace({"admin", "users"}) +@Description("Admin users operations") +public class AdminUsersApi { + @Query + public List findAll() { + // + } +} + +@GraphQLApi +@Namespace({"admin"}) +@Description("Admin operations") +public class AdminApi { + @Query + public List findAll() { + // + } +} +---- + +Will generate schema + +.... +"Query root" +type Query { + admin: AdminQuery +} + +"Admin operations" +type AdminQuery { + users: AdminUsersQuery + findAll: [Admin] +} + +"Admin users operations" +type AdminUsersQuery { + findAll: [User] +} + +type Admin { + id: BigInteger + ... +} + +type User { + id: BigInteger + ... +} +.... + +And you can send GraphQL request like: + +.... +query { + admin { + users { + findAll { + id + } + } + } +} +.... + +== Using @Name annotation (deprecated) + +____ +++[++NOTE++]++ This feature may be removed in the future. +____ + +It does the same thing as `@Namespace`, the only difference is that there can only be one nesting level. + +[source,java] +---- +@GraphQLApi +@Name("users") +@Description("Users operations") +public class UserApi { + @Query + public List findAll() { + // + } +} +---- + +As a result, you can get methods with the same names. + +.... +query { + users { + findAll { + .... + } + } +} +.... + +== Problems + +While dividing APIs into namespaces may seem convenient, several issues are important to be aware of. + +=== Mutations + +On the client side, be careful when working with mutations. +This violates the GraphQL specification since mutations in this form can be executed in parallel. +Read more here about namespaces https://www.apollographql.com/docs/technotes/TN0012-namespacing-by-separation-of-concern/[Namespacing by Separation of Concerns]. +This article describes how to work with namespaces, what problems you may encounter, and how to solve them. + +What does GraphQL say about this - https://benjie.dev/graphql/nested-mutations["GraphQL" Nested Mutations] + +In summary, you can use nested mutations, but with some overhead on the client side. Be careful with mutations. + +==== Subscriptions + +GraphQL does not guarantee subscription resolution within namespaces. +While you can define subscriptions in namespaces, the generated schema will be valid, but the subscription will not be resolved. +For example, if you try to run such a subscription request, you will get an error. This is the behavior of `graphql-java`. + +[source,java] +---- +@GraphQLApi +@Namepace("resource") +public class ResourceApi { + @Subscription + public Multi resourceChange() { + // + } +} +---- + +.... +subscription { + resource { + resourceChange { + .... + } + } +} +.... + +There is currently no way around this problem. +You must move subscriptions into a separate `@GraphQLApi` class that does not declare any namespace. +++>++ ++[++NOTE++]++ +++>++ Be very careful when designing API with namespace. +++>++ And be aware of all the potential consequences when working with mutations and subscriptions. diff --git a/content/docs/smallrye-graphql/2/server-configuration.adoc b/content/docs/smallrye-graphql/2/server-configuration.adoc new file mode 100644 index 000000000..054c01128 --- /dev/null +++ b/content/docs/smallrye-graphql/2/server-configuration.adoc @@ -0,0 +1,38 @@ += Configuration properties +include::_attributes.adoc[] + +These are properties understood directly by SmallRye GraphQL. If you're using SmallRye GraphQL through +Quarkus, these will generally work, but Quarkus offers its own counterparts for most of these, +and it is recommended to use the `quarkus.++*++` properties. Refer to the Quarkus documentation: +https://quarkus.io/guides/all-config#quarkus-smallrye-graphql_quarkus-smallrye-graphql-smallrye-graphql[Server] and +https://quarkus.io/guides/all-config#quarkus-smallrye-graphql-client_quarkus-smallrye-graphql-client-smallrye-graphql-client[Client] +side Quarkus properties. + +== From MicroProfile GraphQL + +[width="100%",cols="33%,35%,32%",options="header",] +|=== +|Property |Default value |Meaning +|`mp.graphql.defaultErrorMessage` |`Server Error` |Error message for hidden exceptions +|`mp.graphql.hideErrorMessage` |Runtime exceptions |Exceptions that will be hidden +|`mp.graphql.showErrorMessage` |Checked exceptions |Exceptions that will not be hidden. +|=== + +== From SmallRye GraphQL + +[width="100%",cols="33%,35%,32%",options="header",] +|=== +|Property |Default value |Meaning +|`smallrye.graphql.printDataFetcherException` |`false` |Include the stacktrace of the data fetching exception in the log output +|`smallrye.graphql.allowGet` |`false` |Allow HTTP GET Method +|`smallrye.graphql.metrics.enabled` |`false` |Enable metrics +|`smallrye.graphql.tracing.enabled` |`false` |Enable tracing +|`smallrye.graphql.validation.enabled` |`true` if Bean Validation is present |Enable Bean Validation. This property is DEPRECATED, setting to `false` won't actually turn off validation. It will be removed in a future release. +|`smallrye.graphql.events.enabled` |`true` if one of metrics, tracing or bean validation is true |Enable eventing +|`smallrye.graphql.logPayload` |`false` |Log the payload in the log file +|`smallrye.graphql.fieldVisibility` | |To control the field visibility on introspection +|`smallrye.graphql.schema.includeScalars` |`true` |Include Scalar definitions in the schema +|`smallrye.graphql.schema.includeSchemaDefinition` |`false` |Include Schema definition +|`smallrye.graphql.schema.includeDirectives` |`false` |Include directives in the schema +|`smallrye.graphql.schema.includeIntrospectionTypes` |`false` |Include Introspection types in the schema +|=== diff --git a/content/docs/smallrye-graphql/2/typesafe-client-accessing-metadata-of-responses.adoc b/content/docs/smallrye-graphql/2/typesafe-client-accessing-metadata-of-responses.adoc new file mode 100644 index 000000000..cb9875c8d --- /dev/null +++ b/content/docs/smallrye-graphql/2/typesafe-client-accessing-metadata-of-responses.adoc @@ -0,0 +1,81 @@ += Accessing Metadata of GraphQL Responses with TypesafeResponse +include::_attributes.adoc[] + +The TypesafeResponse class provides a wrapper around the "data" +field of GraphQL responses, along with additional metadata such as errors, +extensions, and transport metadata. Here are the methods you can use to access each type of metadata: + +== Transport metadata + +use `getTransportMeta()` method to retrieve a map of transport metadata associated with the response. +++>++ ++[++NOTE++]++ +++>++ Note that the transport metadata referred to in the context of HTTP requests +++>++ and responses is commonly found in the headers of the communication. +++>++ Therefore, the getTransportMeta() method mentioned here can be used to +++>++ retrieve a map of HTTP headers. +### Extensions +use `getExtensions()` method to retrieve a JsonObject of response extensions. +The method returns a JSON object that represents the response extensions. + +=== Data + +use `get()` method to retrieve the data object. The method returns the data +object if it exists and there are no errors. If there are errors, +it throws a NoSuchElementException with the error message. + +=== Errors + +use `getErrors()` method to retrieve a list of GraphQL errors. +The method returns a list of errors if there are errors and no data object. +If there is a data object, it throws a NoSuchElementException with the error message. + +____ +++[++NOTE++]++ +Note that the get() method assumes that the response has a data object, +and will throw an exception if there are errors without a data object. +If you want to check for errors before retrieving the data object, +use the hasErrors() method, which returns true if there are errors and false otherwise. +____ + +You can use these methods to access and process the metadata of GraphQL +responses returned by the TypesafeResponse class. + +Let's say you want to get as a client extensions from the GraphQL response. +First of all you will need to modify your `GraphQLClientApi` interface to have +your return type class (in this case `SuperHero`) wrapped with +`TypesafeResponse` class. +I.e. your `GraphQLClientApi` interface could look like this: + +[source,java] +---- +import examples.typesafeclient.SuperHero; +import io.smallrye.graphql.client.typesafe.api.TypesafeResponse; + +@GraphQLClientApi +public interface SuperHeroApi { + // ... + TypesafeResponse findSuperHeroByName(String name); + // ... +} +---- + +____ +++[++NOTE++]++ +Note that you can have `TypesafeResponse++<++T++>++` only as a result (return) type of your query. +Unlike `ErrorOr` the class cannot wrap individual fields. +If that is the case `IllegalArgumentException` will be thrown. +____ + +And now to get `extensions` from the response your code will look like something like this: + +[source,java] +---- + @Inject + SuperHeroApi client; + + TypesafeResponse superheroResponse = client.findSuperHeroByName("Batman"); + // returns the "data" field + SuperHero superhero = superhero.get(); + // returns the "extensions" field + JsonObject extensions = superhero.getExtensions(); +---- diff --git a/content/docs/smallrye-graphql/2/typesafe-client-custom-scalars.adoc b/content/docs/smallrye-graphql/2/typesafe-client-custom-scalars.adoc new file mode 100644 index 000000000..c6fbc61ce --- /dev/null +++ b/content/docs/smallrye-graphql/2/typesafe-client-custom-scalars.adoc @@ -0,0 +1,38 @@ += Custom scalar types +include::_attributes.adoc[] + +If the remote service contains custom scalar types represented as strings in GraphQL documents, +a typesafe client can learn to understand these types through introducing a class that represents them. +The class has to contain logic necessary for serializing and deserializing from/to raw strings. + +A model class is considered to represent a scalar GraphQL type if it contains a static builder method +named `of`, `valueOf`, or `parse` whose sole parameter is a `java.lang.String` and it returns an +instance of the class itself. The name of the GraphQL type is assumed to be equal to the short name +of the class. + +For example, if the server supports a `URL` type (there is a `scalar URL` clause in the schema), +create a class that represents a value for `URL`: + +[source,java] +---- +public class URL { + + private String value; + + public URL(String value) { + this.value = value; + } + + public static URL valueOf(String value) { // <1> + return new URL(value); + } + + @Override + public String toString() { // <2> + return value; + } +} +---- + +* ++<++1++>++ The static `valueOf(String)` method will be used for deserializing a `URL` from a string. +* ++<++2++>++ The `toString()` will be used for serializing a `URL` into a string to be sent to the GraphQL service diff --git a/content/docs/smallrye-graphql/2/typesafe-client-directives.adoc b/content/docs/smallrye-graphql/2/typesafe-client-directives.adoc new file mode 100644 index 000000000..046e90feb --- /dev/null +++ b/content/docs/smallrye-graphql/2/typesafe-client-directives.adoc @@ -0,0 +1,53 @@ += Typesafe Client Static Directives +include::_attributes.adoc[] + +== Custom Static Directives + +The declaration of custom static directives for the typesafe client follows the same principles as the server-side xref:directives.adoc[directives]. With only exception of using the https://spec.graphql.org/draft/#ExecutableDirectiveLocation[Executable Directive Locations] instead of Type System Directive Locations. + +[source,java] +---- +@Directive(on = { FIELD, VARIABLE_DEFINITION }) +@Retention(RUNTIME) +public @interface MyDirective { + String value() default ""; +} +---- + +Applying this to your API should look like this: + +[source,java] +---- +import io.smallrye.graphql.client.typesafe.api.GraphQLClientApi; + +@GraphQLClientApi +public @interface MyClientApi { + + @Query + @MyDirective("top-level field") + MySuperHero getMySuperHero(@MyDirective("variable definition") String nameOfMySuperHero); + + class MySuperHero { + @MyDirective("field") + String name; + } +} +---- + +Your API is going to generate a query something like this: + +.... +query mySuperHero( + $nameOfMySuperHero: String @myDirective(value: "variable definition") +) { + mySuperHero(nameOfMySuperHero: $nameOfMySuperHero) + @myDirective(value: "top-level field") { + name @myDirective(value: "field") + } +} +.... + +____ +++[++NOTE++]++ +The current implementation supports only FIELD and VARIABLE++_++DEFINITION locations. +____ diff --git a/content/docs/smallrye-graphql/2/typesafe-client-error-handling.adoc b/content/docs/smallrye-graphql/2/typesafe-client-error-handling.adoc new file mode 100644 index 000000000..dd8464f37 --- /dev/null +++ b/content/docs/smallrye-graphql/2/typesafe-client-error-handling.adoc @@ -0,0 +1,60 @@ += Error handling in typesafe clients +include::_attributes.adoc[] + +== System-level errors + +If the service returns a system-level error (which means the response body doesn't contain +a valid GraphQL response document), the client invocation will throw a +`io.smallrye.graphql.client.InvalidResponseException` +whose message contains information about the received status code, status message, and body +contents. + +== Application-level errors + +If the service returns one or more application-level errors (which means that +there is valid GraphQL response in the body that has a non-empty `errors` entry), +the client normally throws a `GraphQLClientException` containing the details in a list of +`GraphQLClientError`. + +____ +++[++NOTE++]++ +An error response is considered application-level regardless of the HTTP status code as long as it +contains a valid GraphQL response. We define a GraphQL response as a well-formed JSON document +that contains at least one of `errors`, `data` and `extensions` entries, and no other entries beyond +that. +____ + +If the error is specific to a `location`, you can use an `ErrorOr` +wrapper on the target field; the client the maps the error to that +wrapper instead of throwing an exception. I.e. your `SuperHero` class +could look like this: + +[source,java] +---- +class SuperHero { + String name; + ErrorOr location; +} +---- + +If the service returns a response like this: + +[source,json] +---- +{ + "data": { + "superHero": { + "name": "Wolverine", + "location": null + } + }, + "errors": [{ + "message":"location unknown", + "path": ["superHero","location"], + "extensions":{"code":"location-unknown"} + }] +} +---- + +Then the `SuperHero#location` wrapper field will not contain a `value` +but only the error above. See the `ErrorOr` class for details. diff --git a/content/docs/smallrye-graphql/2/typesafe-client-headers.adoc b/content/docs/smallrye-graphql/2/typesafe-client-headers.adoc new file mode 100644 index 000000000..d3dea2f6c --- /dev/null +++ b/content/docs/smallrye-graphql/2/typesafe-client-headers.adoc @@ -0,0 +1,84 @@ += Adding headers +include::_attributes.adoc[] + +To add a custom header to the http requests sent out by the GraphQL +Client, annotate your method or the API interface as `@Header`, e.g.: + +[source,java] +---- +@GraphQLClientApi +interface SuperHeroesApi { + @Header(name = "S.H.I.E.L.D.-Clearance", constant = "TOP-SECRET") + List allHeroesIn(String location); +} +---- + +The value can be specified with a `constant` or the name of a `method` for dynamic values, e.g.: + +[source,java] +---- +@GraphQLClientApi +interface SuperHeroesApi { + @Header(name = "S.H.I.E.L.D.-Clearance", method = "establishShieldClearance") + List allHeroesIn(String location); + + static Clearance establishShieldClearance() { + return userIsInRole(MANAGER) ? TOP_SECRET : PUBLIC; + } +} +---- + +* This example uses an enum, but it can be any Object; the GraphQL client +calls `toString()` to convert it. + +The method must be `static` and accessible by the interface, i.e. in the +interface itself or in one of the classes it’s nested in; if it’s in a +different class, prefix it with the fully qualified class name and a dot +`"."`, e.g. +`@Header(name = "S.H.I.E.L.D.-Clearance", method = "org.superheroes.SecurityTools.establishShieldClearance")`. + +A third option is to pass the value of a header as an API method +parameter: + +[source,java] +---- +@GraphQLClientApi +interface SuperHeroesApi { + List allHeroesIn(String location, @Header(name = "S.H.I.E.L.D.-Clearance") Clearance clearance); +} +---- + +The `@Header` parameter will not be part of the GraphQL query. + +The `name` of the header is always static, but can optionally be derived from the name of the parameter or method, i.e. if it has a `@Name` annotation, that annotation value is used. If it's not annotated, the method name (eventually stripped off a leading `get`) or the parameter name (if it's enabled in the compiler settings) is converted from camel case to kebab case, i.e. a parameter `@Header String requestIdentifier` will result in a header named `Request-Identifier`. + +`@Header` annotations can also be defined via `@Stereotype`. + +When instantiating the API with the builder, you can set (or overwrite) +one or more headers there: + +[source,java] +---- +SuperHeroesApi api = TypesafeGraphQLClientBuilder.newBuilder() + .header("S.H.I.E.L.D.-Clearance", "TOP-SECRET") + .build(SuperHeroesApi.class); +---- + +Authorization headers + +To add an `Authorization` header, instead of using the generic `@Header` +annotation, you can also use the special `@AuthorizationHeader` +annotation. It produces a `BASIC` `Authorization` header by default or a +`BEARER` token. You can configure the credentials in MP Config with a +prefix plus `/mp-graphql/` and either `username` and `password` for +`BASIC` or `bearer` for `BEARER`. The config key defaults to the fully +qualified name of the `GraphQLClientApi` interface or its `configKey`. + +You can use a custom prefix by setting the `confPrefix`. The infix +`/mp-graphql/` is still applied, unless you end the `confPrefix` with +`++*++`, e.g. +`@AuthorizationHeader(confPrefix = "org.superheroes.security.basic.++*++` +will use `org.superheroes.security.basic.username`, while `++*++` will use +plain `username`. + +`@AuthorizationHeader` annotations can be defined via `@Stereotype`. diff --git a/content/docs/smallrye-graphql/2/typesafe-client-logging.adoc b/content/docs/smallrye-graphql/2/typesafe-client-logging.adoc new file mode 100644 index 000000000..dad8dffc9 --- /dev/null +++ b/content/docs/smallrye-graphql/2/typesafe-client-logging.adoc @@ -0,0 +1,7 @@ += Logging in typesafe clients +include::_attributes.adoc[] + +The Client implementation logs all GraphQL requests and responses at +level `INFO` with the interface API as the logger name. It also logs the +keys of all headers added at level `DEBUG`; not the values, as they may +be security sensitive. diff --git a/content/docs/smallrye-graphql/2/typesafe-client-multiple-queries.adoc b/content/docs/smallrye-graphql/2/typesafe-client-multiple-queries.adoc new file mode 100644 index 000000000..80b399f00 --- /dev/null +++ b/content/docs/smallrye-graphql/2/typesafe-client-multiple-queries.adoc @@ -0,0 +1,48 @@ += Multiple +include::_attributes.adoc[] + +Say you need the result from several root queries, e.g. all +`superHeroes` and all `superVillains`. Java only supports a single +return value, so you’ll need a wrapper class: + +[source,java] +---- +@GraphQLClientApi +interface SuperHeroesApi { + HeroesAndVillains heroesAndVillains(); +} + +@Multiple +class HeroesAndVillains { + List superHeroes; + List superVillains; +} +---- + +The `@Multiple` annotation 'inlines' the wrapper class, i.e. the actual +query is: + +[source,graphql] +---- +query heroesAndVillains { superHeroes {...} superVillains {...}} +---- + +The actual response below will be mapped to an instance of the +`HeroesAndVillains` wrapper class: + +[source,json] +---- +{ + "data": { + "superHeroes": [ ... ], + "superVillains": [ ... ] + } +} +---- + +If the nested queries require parameters, use `@` annotations to put +them on the field (remember: GraphQL fields can have parameters). + +If you need the same request several times (e.g. with different query +parameters), use `@Name` annotations, so the actual field names are used +as xref:#_name_mapping_aliases[alias]. diff --git a/content/docs/smallrye-graphql/2/typesafe-client-reactive-types.adoc b/content/docs/smallrye-graphql/2/typesafe-client-reactive-types.adoc new file mode 100644 index 000000000..a1c7b98c1 --- /dev/null +++ b/content/docs/smallrye-graphql/2/typesafe-client-reactive-types.adoc @@ -0,0 +1,48 @@ += Reactive types usage in typesafe clients +include::_attributes.adoc[] + +== Reactive types with queries and mutations + +Regardless of the type that a query or mutation returns, you can always wrap the return type in a `Uni` in your +`@GraphQLClientApi` interface. The `Uni` will be completed after the response is received and processed. + +== Subscriptions + +The return type of a subscription operation must always be wrapped in a `Multi`. +The communication with the server runs over a WebSocket. + +=== Important note about errors + +When using the typesafe client for subscriptions, if it is expected that the server might return errors, +then it is highly recommended to wrap the return types into an `ErrorOr` +(for example, turn the API method `@Subscription Multi++<++Person++>++ people` into `@Subscription Multi++<++ErrorOr++<++Person++>>++ people`). +The reason is once there is an error returned from the service and the result can't be turned into a domain object due to that, +the `Multi` will receive a failure as a result. Because of the design of Mutiny, a `Multi` can't continue producing any items after +a failure. That means the subscription will be cancelled after the first error, even though the server might continue +sending more items. In such case, your application would have to detect the error and start a new subscription. +See xref:typesafe-client-error-handling.adoc[Error handling] for more details on how to use `ErrorOr`. + +=== Subscription example + +Given a server-side definition like this: + +[source,java] +---- +@Subscription +public Multi countToFive() { + return Multi.createFrom().range(0, 5); +} +---- + +A client to consume this subscription can look like this: + +[source,java] +---- +client.countToFive + .subscribe() + .with( // onItem: + i -> System.out.println("Received number" + i), + // onFailure: + (t) -> t.printStackTrace() + ); +---- diff --git a/content/docs/smallrye-graphql/2/typesafe-client-unions-and-interfaces.adoc b/content/docs/smallrye-graphql/2/typesafe-client-unions-and-interfaces.adoc new file mode 100644 index 000000000..36bf138b8 --- /dev/null +++ b/content/docs/smallrye-graphql/2/typesafe-client-unions-and-interfaces.adoc @@ -0,0 +1,164 @@ += Unions & Interfaces +include::_attributes.adoc[] + +If you consume a GraphQL service with a schema containing either a `Union` or an `Interface`, +the client needs to request different fields via fragments, depending on the requested sub-type, +and deserialize a polymorphic type. + +This is quite complex on the GraphQL side, but the typesafe client makes it very easy to use. + +Unions + +One common use-case for Unions is returning a business error, e.g. when searching for a super hero, +either you find a super hero and maybe you want the name and other fields, +or you don't find it, and maybe you want a message with the reason. +Such a query could look like this: + +[source,graphql] +---- +query find($name: String) { + find(name: $name){ + __typename + ... on SuperHero {name} + ... on SuperHeroNotFound {message} + } +} +---- + +When it finds your super hero, the service could respond with this: + +[source,json] +---- +{ + "data": { + "find": { + "__typename": "SuperHero", + "name": "Spider-Man" + } + } +} +---- + +Or if it doesn't find it, the response could look like this: + +[source,json] +---- +{ + "data": { + "find": { + "__typename":"SuperHeroNotFound", + "message":"There is no hero named Foo" + } + } +} +---- + +On the Java side, this maps quite nicely to an interface, e.g. `SuperHeroResponse` +implemented by the two possible classes returned, e.g. `SuperHero` and `SuperHeroNotFound`; +you only need to provide the typesafe client with this information about these sub-classes by using the +https://jakarta.ee/specifications/jsonb/3.0/jakarta-jsonb-spec-3.0#polymorphic-types[`JSON-B` Polymorphic Types] +annotations `@JsonbTypeInfo` and `@JsonbSubtype`: + +[source,java] +---- + @Union + @JsonbTypeInfo(key = "__typename", value = { + @JsonbSubtype(alias = "SuperHero", type = SuperHero.class), + @JsonbSubtype(alias = "NotFound", type = NotFound.class) + }) + public interface SuperHeroResponse { + } + + public static class SuperHero implements SuperHeroResponse { + String name; + + String getName() { + return name; + } + } + + @Type("SuperHeroNotFound") + public static class NotFound implements SuperHeroResponse { + String message; + + String getMessage() { + return message; + } + } + + @GraphQLClientApi + interface UnionApi { + SuperHeroResponse find(String name); + } +---- + +Note that we rename the `NotFound` class into the GraphQL type `SuperHeroNotFound` to demo that use-case. + +The `@Union` annotation is actually not used and only helps to document what's happening behind the scenes. + +Interfaces + +If a service returns an GraphQL `interface`, that's visible only in the schema; +queries and responses work exactly like for a `union`. +So, interfaces are very similar to unions; but they can share common fields between the sub-types. + +E.g. a query returning either a `MainCharacter` or a `SideKick`, could look like this: + +[source,graphql] +---- +query find($name: String) { + find(name: $name){ + __typename + ... on MainCharacter {name superPower} + ... on SideKick {name mainCharacter} + } +} +---- + +The Java side looks very similar the Union example above: + +[source,java] +---- +@JsonbTypeInfo(key = "__typename", value = { + @JsonbSubtype(alias = "MainCharacter", type = MainCharacter.class), + @JsonbSubtype(alias = "SideKick", type = SideKick.class) +}) +public interface SearchResult { + String getName(); +} + +public static class MainCharacter implements SearchResult { + String name; + String superPower; + + @Override + public String getName() { + return name; + } + + public String getSuperPower() { + return superPower; + } +} + +public static class SideKick implements SearchResult { + String name; + String mainCharacter; + + @Override + public String getName() { + return name; + } + + public String getMainCharacter() { + return mainCharacter; + } +} + +@GraphQLClientApi +interface InterfaceApi { + SearchResult find(String name); + + List all(); +} +---- diff --git a/content/docs/smallrye-graphql/2/typesafe-client-usage.adoc b/content/docs/smallrye-graphql/2/typesafe-client-usage.adoc new file mode 100644 index 000000000..4dabe814b --- /dev/null +++ b/content/docs/smallrye-graphql/2/typesafe-client-usage.adoc @@ -0,0 +1,301 @@ += Java code-first type-safe GraphQL Client API +include::_attributes.adoc[] + +A Java code-first type-safe GraphQL Client API suggestion for +https://github.com/eclipse/microprofile-graphql/issues/185[Microprofile GraphQL Issue +#185]. + +== Basic Usage + +Creating the client-side counterpart of the GraphQL API: + +[source,java] +---- +include::{examples-dir}/examples/typesafeclient/SuperHeroesApi.java[] +---- + +A model class: + +[source,java] +---- +include::{examples-dir}/examples/typesafeclient/SuperHero.java[] +---- + +Injecting the client using CDI and using it: + +[source,java] +---- +include::{examples-dir}/examples/typesafeclient/MyClientUsage.java[] +---- + +* The default request type is `query`. To make it a mutation, annotate +it `@Mutation`. The parameter name is only available if you compile +the source with the `-parameters` option. Otherwise, you’ll have to +annotate all parameters with `@Name`. + +The example above uses CDI, e.g. when you are in a MicroProfile or +Jakarta EE environment. If you are in an environment without CDI +support, you need to instantiate the API interface by using the builder: + +[source,java] +---- +SuperHeroesApi api = TypesafeGraphQLClientBuilder.newBuilder().build(SuperHeroesApi.class); +---- + +The basic idea of the Java code-first approach is to start by +writing the DTOs and query/mutation methods as you need them in your +client. This ensures that you don’t request fields that you don’t need; +the thinking is inspired by https://martinfowler.com/articles/consumerDrivenContracts.html[Consumer Driven +Contracts]. + +If the server uses names different from yours, you can use annotations to do a mapping: + +=== Name Mapping / Aliases + +If the server defines a different field or parameter name, annotate it +with `@Name`. If the server defines a different query name, annotate the +method as, e.g., `@Query("findHeroesCurrentlyLocatedIn")`. + +By renaming methods, you can also define several variations of the same +request but using different return types or parameters. E.g.: + +[source,java] +---- +public interface SuperHeroesApi { + SuperHero findHeroByName(String name); + + @Query("findHeroByName") + SuperHeroWithTeams findHeroWithTeamsByName(String name); +} +---- + +* The `SuperHero` class has no team affiliations (for this example). +* The `SuperHeroWithTeams` class has a `List++<++Team++>++ teamAffiliations` +field. The actual query name is still `findHeroByName`. The `Team` +class doesn’t contain the members to break recursion. + +If you rename a field or method, the real field or method name will be +used as an alias, so you can select the same data twice (see `and` +below). + +=== Configuration + +If the endpoint is always the same, e.g. a public API of a cloud +service, you can add the URL to your API annotation, e.g.: + +[source,java] +---- +@GraphQLClientApi(endpoint = "https://superheroes.org/graphql") +interface SuperHeroesApi { +} +---- + +When instantiating the API with the builder, you can set (or overwrite) +the endpoint there: + +[source,java] +---- +SuperHeroesApi api = TypesafeGraphQLClientBuilder.newBuilder() + .endpoint("https://superheroes.org/graphql") + .build(SuperHeroesApi.class); +---- + +Commonly you’ll need different endpoints, e.g. when you need one +endpoint for your production system, but a different endpoint for your +test system. Simply use https://download.eclipse.org/microprofile/microprofile-config-1.4/microprofile-config-spec.html[MicroProfile +Config] +to set the endpoint; similar to the https://download.eclipse.org/microprofile/microprofile-rest-client-1.4.1/microprofile-rest-client-1.4.1.html[MicroProfile Rest +Client], +the key for the endpoint is the fully qualified name of the API +interface, plus `/mp-graphql/url`, e.g.: + +[source,properties] +---- +org.superheroes.SuperHeroesApi/mp-graphql/url=https://superheroes.org/graphql +---- + +If you want to use a different key, set the base config key on the +annotation `@GraphQLClientApi(configKey = "superheroes")`; then use this +key for the endpoint `superheroes/mp-graphql/url`. + +When using the builder, you can override the config key as well: +`TypesafeGraphQLClientBuilder.newBuilder().configKey("superheroes")`. + +=== NestedParameter + +Some APIs require parameters beyond the root level, e.g. for filtering +or paginating nested lists. Say you have a schema like this: + +[source,graphql] +---- +type Query { + team(name: String!): Team! +} + +type Team { + members(first: Int!): [SuperHero!]! +} +---- + +To pass the parameter to the nested field/method, annotate it as +`@NestedParameter`, e.g.: + +[source,java] +---- +@GraphQLClientApi +interface TeamsApi { + Team team(String name, @NestedParameter("members") int first); +} +---- + +The value of the `@NestedParameter` annotation is the dot-delimited path +to the nested field/method that the value should be added to. + +Example of server code + +[source,java] +---- +@GraphQLApi +public class RoleApi { + @Query + public List findAllRolesByUserId(@NonNull UUID userId) { + // return roles + } + + public List permission(@Source Roles role, @DefaultValue("5") int limit) { + // return permissions, based on roles + } + + public List permissionType(@Source Permission permission, @DefaultValue("5") int limit) { + // return permissionType, based on permission + } +} +---- + +Query looks like + +.... +query { + findAllRolesByUserId(userId: ...) { + id + permission(limit: 2) { + id + permissionType(limit: 3) { + id + } + } + } +} +.... + +On the client side, you can declare the following code: + +[source,java] +---- +public record PermissionType(Long id) { +} + +public record Permission(Long id, List permissionType) { +} + +public record Role(UUID id, List permission) { +} + +@GraphQLClientApi +public interface ApiClient { + List findAllRolesByUserId( + UUID userId, + @NestedParameter("permission") @Name("limit") int permissionLimit, + @NestedParameter("permission.permissionType") @Name("limit") int permissionTypeLimit + ); +} +---- + +== Namespaces + +There are several ways to work with namespaces in a type-safe client: +1. Using `@Namespace` +2. Using `@Name` (deprecated) + +____ +++[++NOTE++]++ You can only use one of the annotations - `@Name` or `@Namespace` on a `GraphClientQLApi` interface. +____ + +=== Using @Namespace annotation + +The `@Namespace` annotation accepts an array of strings specifying the nesting levels of the namespace. +This flexible approach allows you to create namespaces with any desired level of nesting, combining different levels as needed. + +If the remote GraphQL API has the following schema: + +.... +"Query root" +type Query { + admin: AdminQuery +} + +type AdminQuery { + users: AdminUsersQuery +} + +type AdminUsersQuery { + findAll: User +} + +type User { + id: BigInteger + [...] +} +.... + +While declaring the following interface: + +[source,java] +---- +@Namespace({"admin", "users"}) +@GraphQLClientApi +public interface UsersClient { + List findAll(); +} +---- + +Its outcome will be the following GraphQL query: + +.... +query AminUsersFindAll { + admin { + users { + findAll { + id + } + } + } +} +.... + +=== Using @Name (deprecated) + +____ +++[++NOTE++]++ This feature may be removed in the future. +____ + +The `@Name` annotation functions similarly to `@Namespace`, but it is limited to a single nesting level. + +.... +query { + users { + findAll { + ... + } + } +} +.... + +[source,java] +---- +@Name("users") +@GraphQLClientApi +public interface ApiClient { + List findAll(); +} +---- diff --git a/content/docs/smallrye-graphql/2/using-smallrye-stork.adoc b/content/docs/smallrye-graphql/2/using-smallrye-stork.adoc new file mode 100644 index 000000000..46064219c --- /dev/null +++ b/content/docs/smallrye-graphql/2/using-smallrye-stork.adoc @@ -0,0 +1,53 @@ += Using SmallRye Stork +include::_attributes.adoc[] + +SmallRye GraphQL client (both dynamic and typesafe) supports SmallRye Stork for +service discovery and load balancing. If your client is configured to connect to a URI +with the `stork://` or `storks://` scheme, then Stork will be used to determine the +actual endpoints to connect to. Load balancing, if configured, will be applied in a way that +each HTTP request can be routed to a different service instance. For WebSocket traffic, +once a WebSocket connection is established, the client will keep using that connection +for as long as possible. If that connection is lost for any reason and needs to be +reestablished, then the load balancer can route it to a different instance. + +== Using with Quarkus + +The dependency on Stork is declared as optional, so if you want to use it, you might need +to add it explicitly. If you're using Quarkus, use the Stork extension along with +the desired service discovery implementation, for example: + +.... + + io.quarkus + quarkus-smallrye-stork + + + io.smallrye.stork + smallrye-stork-service-discovery-static-list + +.... + +Example configuration in Quarkus `application.properties`: + +.... +quarkus.smallrye-graphql-client.myclient.url=stork://foo-service/graphql +quarkus.stork.foo-service.service-discovery.type=static +quarkus.stork.foo-service.service-discovery.address-list=server1.com:8080,server2.com:8080 +.... + +== When running as a standalone application + +If you're using the client in a standalone mode outside any container that handles +Stork initialization for you, you might need two additional steps to get it working: + +Add a configuration provider for Stork, such as + +.... + + io.smallrye.stork + stork-microprofile-config + +.... + +And before initializing any GraphQL client instances, make sure that Stork is initialized +by calling `io.smallrye.stork.Stork.initialize()` once. diff --git a/content/docs/smallrye-graphql/latest/_attributes.adoc b/content/docs/smallrye-graphql/latest/_attributes.adoc new file mode 100644 index 000000000..8994ecbf4 --- /dev/null +++ b/content/docs/smallrye-graphql/latest/_attributes.adoc @@ -0,0 +1,20 @@ +:project-version: 3.0.0.Beta8 +// Attributes shared by every page. smallrye.io overrides the ones it needs when it publishes. +// Pages in subdirectories set :version-root: (../ per level) before including this file. +ifndef::version-root[:version-root:] +:idprefix: +:idseparator: - +:sectanchors: +:examples-dir: {version-root}_examples +:partialsdir: {version-root}_partials +:project-name: SmallRye GraphQL +:javadoc-version: latest +:generated-dir: {version-root}_generated + +// Added by smallrye.io (tools/docs/Docs.java sync) +:javadoc-version: 3.0.0.Beta8 +:relfileprefix: /docs/smallrye-graphql/latest/ +:imagesdir: /docs/smallrye-graphql/latest +:docs-project: smallrye-graphql +:docs-version: latest +:docs-ref: 3.0.0.Beta8 diff --git a/content/docs/smallrye-graphql/latest/_examples/examples/dynamicclient/MyClientUsage.java b/content/docs/smallrye-graphql/latest/_examples/examples/dynamicclient/MyClientUsage.java new file mode 100644 index 000000000..1647626af --- /dev/null +++ b/content/docs/smallrye-graphql/latest/_examples/examples/dynamicclient/MyClientUsage.java @@ -0,0 +1,37 @@ +package examples.dynamicclient; + +import examples.typesafeclient.SuperHero; +import io.smallrye.graphql.client.GraphQLClient; +import io.smallrye.graphql.client.Response; +import io.smallrye.graphql.client.core.Document; +import io.smallrye.graphql.client.dynamic.api.DynamicGraphQLClient; + +import jakarta.inject.Inject; +import tools.jackson.databind.node.ArrayNode; +import java.util.List; +import java.util.concurrent.ExecutionException; + +import static io.smallrye.graphql.client.core.Argument.arg; +import static io.smallrye.graphql.client.core.Argument.args; +import static io.smallrye.graphql.client.core.Document.document; +import static io.smallrye.graphql.client.core.Field.field; +import static io.smallrye.graphql.client.core.Operation.operation; + +public class MyClientUsage { + + @Inject + @GraphQLClient("superHeroes") + DynamicGraphQLClient client; + + public void execute() throws ExecutionException, InterruptedException { + Document document = document( // <1> + operation(field("allHeroesIn", + args(arg("location", "Outer Space")), + field("name"), + field("superPowers")))); + Response response = client.executeSync(document); // <2> + + ArrayNode heroesArray = (ArrayNode) response.getData().get("allHeroesIn"); // <3> + List heroes = response.getList(SuperHero.class, "allHeroesIn"); // <4> + } +} diff --git a/content/docs/smallrye-graphql/latest/_examples/examples/dynamicclient/MyClientUsageString.java b/content/docs/smallrye-graphql/latest/_examples/examples/dynamicclient/MyClientUsageString.java new file mode 100644 index 000000000..c761d64b1 --- /dev/null +++ b/content/docs/smallrye-graphql/latest/_examples/examples/dynamicclient/MyClientUsageString.java @@ -0,0 +1,39 @@ +package examples.dynamicclient; + +import io.smallrye.graphql.client.GraphQLClient; +import io.smallrye.graphql.client.Response; +import io.smallrye.graphql.client.dynamic.api.DynamicGraphQLClient; + +import jakarta.inject.Inject; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutionException; + +public class MyClientUsageString { + + @Inject + @GraphQLClient("superheroes") + DynamicGraphQLClient client; + + public void execute() throws ExecutionException, InterruptedException { + String queryWithInlineVariables = // <1> + "query {" + + " allHeroesIn(location: \"Outer Space\") {" + + " name" + + " superPowers" + + " }" + + "}"; + Response response = client.executeSync(queryWithInlineVariables); + + String queryWithExtractedVariables = // <2> + "query($loc: String) {" + + " allHeroesIn(location: $loc) {" + + " name" + + " superPowers" + + " }" + + "}"; + Map variables = new HashMap<>(); // <3> + variables.put("loc", "Outer Space"); + Response response2 = client.executeSync(queryWithExtractedVariables, variables); + } +} diff --git a/content/docs/smallrye-graphql/latest/_examples/examples/typesafeclient/MyClientUsage.java b/content/docs/smallrye-graphql/latest/_examples/examples/typesafeclient/MyClientUsage.java new file mode 100644 index 000000000..be521d196 --- /dev/null +++ b/content/docs/smallrye-graphql/latest/_examples/examples/typesafeclient/MyClientUsage.java @@ -0,0 +1,16 @@ +package examples.typesafeclient; + +import jakarta.inject.Inject; +import java.util.List; + +public class MyClientUsage { + + @Inject + SuperHeroesApi superHeroesApi; + + public void execute() { + List allHeroes = superHeroesApi.allHeroesIn("Outer Space"); + // ... + } + +} diff --git a/content/docs/smallrye-graphql/latest/_examples/examples/typesafeclient/SuperHero.java b/content/docs/smallrye-graphql/latest/_examples/examples/typesafeclient/SuperHero.java new file mode 100644 index 000000000..7c19a1119 --- /dev/null +++ b/content/docs/smallrye-graphql/latest/_examples/examples/typesafeclient/SuperHero.java @@ -0,0 +1,12 @@ +package examples.typesafeclient; + +import java.util.List; + +public class SuperHero { + + private String name; + private List superPowers; + + // plus getters and setters + +} diff --git a/content/docs/smallrye-graphql/latest/_examples/examples/typesafeclient/SuperHeroesApi.java b/content/docs/smallrye-graphql/latest/_examples/examples/typesafeclient/SuperHeroesApi.java new file mode 100644 index 000000000..0fae71966 --- /dev/null +++ b/content/docs/smallrye-graphql/latest/_examples/examples/typesafeclient/SuperHeroesApi.java @@ -0,0 +1,12 @@ +package examples.typesafeclient; + +import io.smallrye.graphql.client.typesafe.api.GraphQLClientApi; + +import java.util.List; + +@GraphQLClientApi +public interface SuperHeroesApi { + + List allHeroesIn(String location); + +} diff --git a/content/docs/smallrye-graphql/latest/_nav.yml b/content/docs/smallrye-graphql/latest/_nav.yml new file mode 100644 index 000000000..bfa9b0569 --- /dev/null +++ b/content/docs/smallrye-graphql/latest/_nav.yml @@ -0,0 +1,76 @@ +title: SmallRye GraphQL +nav: +- title: Overview + page: index +- title: Server side features + items: + - title: Customizing JSON (de)serializers + page: custom-json-serializers-deserializers + - title: Directives + page: directives + - title: Federation + page: federation + - title: Custom error extensions + page: custom-error-extensions + - title: Response extensions + page: extensions + - title: Returning Void from Mutations + page: mutation-void + - title: Handling of the WebSocket's init-payload + page: handling-init-payload-from-the-websocket + - title: Custom scalars + page: custom-scalar + - title: Inspecting executable directives + page: inspecting-executable-directives-on-server-side + - title: Namespaces + page: namespaces-on-server-side + - title: Target input fields + page: target-for-input-types + - title: Kotlin support + page: kotlin +- title: Typesafe client + items: + - title: Basic usage + page: typesafe-client-usage + - title: Unions and Interfaces + page: typesafe-client-unions-and-interfaces + - title: Reactive + page: typesafe-client-reactive-types + - title: Custom scalar types + page: typesafe-client-custom-scalars + - title: Adding headers + page: typesafe-client-headers + - title: Logging + page: typesafe-client-logging + - title: Running multiple queries at once + page: typesafe-client-multiple-queries + - title: Error handling + page: typesafe-client-error-handling + - title: Accessing metadata of responses + page: typesafe-client-accessing-metadata-of-responses + - title: Static Directives + page: typesafe-client-directives +- title: Dynamic client + items: + - title: Basic usage + page: dynamic-client-usage + - title: Error handling + page: dynamic-client-error-handling +- title: Clients in general + items: + - title: Bootstrapping the client + page: client-standalone + - title: Using SmallRye Stork + page: using-smallrye-stork +- title: Developer tools + items: + - title: Maven plugin + page: maven-plugin + - title: Gradle plugin + page: gradle-plugin +- title: Reference material + items: + - title: Server-side configuration + page: server-configuration + - title: Client-side configuration + page: client-configuration diff --git a/content/docs/smallrye-graphql/latest/client-configuration.adoc b/content/docs/smallrye-graphql/latest/client-configuration.adoc new file mode 100644 index 000000000..7da22fddd --- /dev/null +++ b/content/docs/smallrye-graphql/latest/client-configuration.adoc @@ -0,0 +1,28 @@ += Client-side configuration properties +include::_attributes.adoc[] + +Note: if you are using Quarkus, it is recommended to use the `quarkus.++*++` property counterparts instead. +See https://quarkus.io/guides/all-config#quarkus-smallrye-graphql-client_quarkus-smallrye-graphql-client-smallrye-graphql-client[Quarkus Documentation] for more info. + +[width="100%",cols="18%,10%,72%",options="header",] +|=== +|Property |Default value |Meaning +|`CLIENT++_++NAME/mp-graphql/url` |none |Denotes URL to connect to +|`CLIENT++_++NAME/mp-graphql/header/KEY` |none |Adds a HTTP header named `KEY` to all HTTP requests performed by the client +|`CLIENT++_++NAME/mp-graphql/subprotocols` |`graphql-ws,graphql-transport-ws` |Comma-separated list of websocket subprotocols supported by this client. We currently support `graphql-ws` and `graphql-transport-ws`. If multiple subprotocols are provided, choosing the actual subprotocol will be subject to negotiation with the server. +|`CLIENT++_++NAME/mp-graphql/keystore` |none |Path to client's keystore (for example `file:/path/to/keystore` or `classpath:path/to/keystore`) +|`CLIENT++_++NAME/mp-graphql/keystoreType` |`JKS` |Keystore type +|`CLIENT++_++NAME/mp-graphql/keystorePassword` |none |Keystore password +|`CLIENT++_++NAME/mp-graphql/truststore` |none |Path to client's truststore (for example `file:/path/to/truststore` or `classpath:path/to/truststore`) +|`CLIENT++_++NAME/mp-graphql/truststoreType` |`JKS` |Truststore type +|`CLIENT++_++NAME/mp-graphql/truststorePassword` |none |Truststore password +|`CLIENT++_++NAME/mp-graphql/proxyHost` |none |Hostname of the proxy to use +|`CLIENT++_++NAME/mp-graphql/proxyPort` |none |Port of the proxy to use +|`CLIENT++_++NAME/mp-graphql/proxyUsername` |none |Username for the proxy to use +|`CLIENT++_++NAME/mp-graphql/proxyPassword` |none |Password for the proxy to use +|`CLIENT++_++NAME/mp-graphql/maxRedirects` |16 |Max number of redirects to follow. Set to 0 to disable redirects. +|`CLIENT++_++NAME/mp-graphql/websocketInitializationTimeout` |none |Maximum time in milliseconds that will be allowed to wait for the server to acknowledge a websocket connection. +|`CLIENT++_++NAME/mp-graphql/runSingleOperationsOverWebsocket` |`false` |If true, then queries and mutations will run over the websocket transport rather than pure HTTP. Off by default, because it has higher overhead. +|`CLIENT++_++NAME/mp-graphql/initPayload/KEY` |none |Adds a property named `KEY` to the `connection++_++init` message payload when negotiating a websocket connection. All values will be treated as string. For other types instatiate the API with the builder. +|`CLIENT++_++NAME/mp-graphql/allowUnexpectedResponseFields` |`false` |If true and there is an unexpected field in the response payload, it won't throw an error, and it will log a warning message. +|=== diff --git a/content/docs/smallrye-graphql/latest/client-standalone.adoc b/content/docs/smallrye-graphql/latest/client-standalone.adoc new file mode 100644 index 000000000..e991a36b2 --- /dev/null +++ b/content/docs/smallrye-graphql/latest/client-standalone.adoc @@ -0,0 +1,76 @@ += Bootstraping GraphQL clients +include::_attributes.adoc[] + +In general, it is recommended to use SmallRye GraphQL client through the +https://quarkus.io/guides/smallrye-graphql-client[Quarkus extension], +or the https://github.com/wildfly-extras/wildfly-graphql-feature-pack/wiki/GraphQL-client-guide[WildFly feature pack], +each of them having their own documentation about how to set up dependencies and bootstrap a client. +It is however also possible to use SmallRye GraphQL Client in a standalone application outside any container. +The single dependency that you need is `io.smallrye:smallrye-graphql-client-implementation-vertx`, as the +Vert.x based implementation is currently the only supported one. + +If you're using the client as a script and notice that the JVM doesn't exit even after you close the client instance, +it's probably because the client is maintaining a `Vertx` instance that it created automatically. To solve this +issue, you might need to create a `Vertx` instance on your own, pass it to the client builder, and then close it after +closing the client. See the following JBang snippet for an example. + +== Logging with a standalone (Java SE) application using the SmallRye GraphQL Client + +If you want to properly configure logging in a standalone application, you +will also need to add a logging implementation to the classpath. The client +code uses the JBoss Logging facade, so you can, for example, use the JBoss +Logmanager. Add the `org.jboss.logmanager:jboss-logmanager` dependency to +your project(it needs to be on the runtime classpath). You will also need to +run the application with this system property declared: +`-Djava.util.logging.manager=org.jboss.logmanager.LogManager`. + +To customize logging levels etc, it picks up the `logging.properties` file +from the root of the runtime classpath, so, for example if using Maven and +the exec plugin to start the application, you would place the file into +`src/main/resources/logging.properties`. For an example of the contents of +this file, see +https://github.com/jamezp/jboss-logging-example/blob/master/src/main/resources/logging.properties. + +== Using with JBang + +This is a full script runnable directly with https://www.jbang.dev/[JBang] that +uses a dynamic client for connecting to https://countries.trevorblades.com[countries.trevorblades.com] +to obtain a list of countries from its database. + +[source,java] +---- +///usr/bin/env jbang "$0" "$@" ; exit $? +//DEPS io.smallrye:smallrye-graphql-client-implementation-vertx:RELEASE + +import io.smallrye.graphql.client.dynamic.api.DynamicGraphQLClient; +import io.smallrye.graphql.client.Response; +import io.smallrye.graphql.client.vertx.dynamic.VertxDynamicGraphQLClientBuilder; + +import io.vertx.core.Vertx; + +// Has a multiline string literal, requires Java 15+! +class Client { + public static void main(String... args) throws Exception { + Vertx vertx = Vertx.vertx(); + DynamicGraphQLClient client = new VertxDynamicGraphQLClientBuilder() + .url("https://countries.trevorblades.com") + .vertx(vertx) + .build(); + try { + Response response = client.executeSync(""" + query { + countries { + name + } + } + """); + System.out.println(response); + } finally { + client.close(); + vertx.close(); + } + } +} +---- + +Save this file as `Client.java` and execute with `jbang Client.java`. diff --git a/content/docs/smallrye-graphql/latest/custom-error-extensions.adoc b/content/docs/smallrye-graphql/latest/custom-error-extensions.adoc new file mode 100644 index 000000000..82aa86e5b --- /dev/null +++ b/content/docs/smallrye-graphql/latest/custom-error-extensions.adoc @@ -0,0 +1,44 @@ += GraphQL Error Extensions +include::_attributes.adoc[] + +Exceptions are reported in GraphQL in the `errors` array, next to the `data` field, so it's possible to return partial results, e.g.: + +[source,json] +---- +{ + "data": { + "superHero": { + "name": "Wolverine", + "location": null + } + }, + "errors": [{ + "message":"location unknown", + "path": ["superHero","location"], + "extensions":{"code":"location-unknown"} + }] +} +---- + +The `location` field couldn't be returned, and in the `errors`, there's the reason with a few predefined fields, +and a map of `extensions` that can contain custom details about the error. + +You can add your own extensions by implementing the `io.smallrye.graphql.api.ErrorExtensionProvider` interface and +adding your class name to a file `META-INF/services/io.smallrye.graphql.api.ErrorExtensionProvider` (this is a `ServiceLoader`). + +As an example, this class provides an extension named `exception-length` with the length of the simple class name of the exception: + +[source,java] +---- +public class ExceptionLengthErrorExtensionProvider implements ErrorExtensionProvider { + @Override + public String getKey() { + return "exception-length"; + } + + @Override + public JsonNumber mapValueFrom(Throwable exception) { + return Json.createValue(exception.getClass().getSimpleName().length()); + } +} +---- diff --git a/content/docs/smallrye-graphql/latest/custom-json-serializers-deserializers.adoc b/content/docs/smallrye-graphql/latest/custom-json-serializers-deserializers.adoc new file mode 100644 index 000000000..70d87f6b6 --- /dev/null +++ b/content/docs/smallrye-graphql/latest/custom-json-serializers-deserializers.adoc @@ -0,0 +1,36 @@ += Customizing JSON-B serializers/deserializers +include::_attributes.adoc[] + +If your application needs finer-grained control over JSON serialization/deserialization than you can achieve via +formatting annotations like `@JsonbDateFormat`, you may plug in your own custom instances of the `Jsonb` class +for each input type that your GraphQL application exposes. + +To do this, you'll need to create an implementation of `io.smallrye.graphql.spi.EventingService` +and implement its `overrideJsonbConfig` method. +An example that plugs in a custom date format for a particular class that is used as input: + +.... +package org.acme.custom.json.config; + +public class CustomJsonbService implements EventingService { + + @Override + public String getConfigKey() { + return null; // activate this service always regardless of the configuration + } + + @Override + public Map overrideJsonbConfig() { + JsonbConfig config = new JsonbConfig().withDateFormat("MM dd yyyy HH:mm Z", null).withDeserializers(OBJECTID_DESERIALIZER).withSerializers( + OBJECTID_SERIALIZER); + return Collections.singletonMap("MyModelClass.class.getName()", JsonbBuilder.create(config)); + } +} +.... + +We can customize the JsonbConfig as per our need in above example we have registered additional serializer and date formatting for the class MyModelClass. + +As the discovery of eventing services uses the ServiceLoader mechanism, don't forget to add a +`META-INF/services/io.smallrye.graphql.spi.EventingService` file that contains the fully qualified +name of your implementation. +Example `org.acme.custom.json.config.CustomJsonbService` diff --git a/content/docs/smallrye-graphql/latest/custom-scalar.adoc b/content/docs/smallrye-graphql/latest/custom-scalar.adoc new file mode 100644 index 000000000..27be60b11 --- /dev/null +++ b/content/docs/smallrye-graphql/latest/custom-scalar.adoc @@ -0,0 +1,36 @@ += Creating GraphQL Custom Scalars with SmallRye GraphQL +include::_attributes.adoc[] + +While by default *MicroProfile GraphQL* specification doesn't provide direct support for creating +custom scalars for GraphQL literal types of String, Int and Float, *SmallRye GraphQL* has implemented +this feature based on user feedback. To implement a *custom scalar* with *SmallRye GraphQL*, +you can annotate your custom scalar class with `@CustomScalar` in addition to following the pattern below +of including a public constructor that takes one of `String`++|++`BigDecimal`++|++`BigInteger` depending on +the scalar type: + +[source,java] +---- + @CustomScalar("BigDecimalString") + public class BigDecimalString implements CustomStringScalar { + public BigDecimalString(String stringValue) { + ... + } + @Override + public String stringValueForSerialization() { + ... + } +} +---- + +In this example, `BigDecimalString` implements the `CustomStringScalar` which is used to identify the +proper (de) serialization for BigDecimalString. `BigDecimalString` also provides a single argument +constructor which takes a String. Finally, `BigDecimalString` implements +`stringValueForSerialization()` which provides the String representation to be used during +serialization. + +____ +++[++NOTE++]++ +If the user wants to create a literal for GraphQL Int or Float, they would implement either +CustomIntScalar with the intValueForSerialization method or CustomFloatScalar with the +floatValueForSerialization method respectively. +____ diff --git a/content/docs/smallrye-graphql/latest/directives.adoc b/content/docs/smallrye-graphql/latest/directives.adoc new file mode 100644 index 000000000..34b420ca8 --- /dev/null +++ b/content/docs/smallrye-graphql/latest/directives.adoc @@ -0,0 +1,45 @@ += Directives +include::_attributes.adoc[] + +== Custom Directives + +You can add your own https://spec.graphql.org/draft/#sec-Language.Directives[GraphQL Directives] by writing +a corresponding Java Annotation and annotate it as `@Directive`, e.g.: + +[source,java] +---- +@Directive(on = { OBJECT, INTERFACE }) +@Description("Just a test") +@Retention(RUNTIME) +public @interface MyDirective { +} +---- + +Directives can be repeatable, see the `@Key` annotation for an example. + +== Directives generated from Bean Validation annotations + +If your project uses Bean Validation to validate fields on input types and operation arguments, and you enable +inclusion of directives in the schema (by setting `smallrye.graphql.schema.includeDirectives=true`), +then constraints decoded from annotations will be added to your schema as directives. This is currently only +supported for some built-in constraints (annotations from the +`javax.validation.constraints` package), and custom constraints aren't supported at all. + +Each bean validation annotation is mapped to a single `@constraint` directive. The directive is declared as repeatable, +so if you have multiple constraints on an input field, the field will contain multiple `@constraint` directives. +The following table describes the mapping between BV annotations and `@constraint` directives (all currently supported +BV annotations are listed here): + +[cols=",",options="header",] +|=== +|BV annotation |GraphQL directive +|`@Size(MIN, MAX)` |`@constraint(minLength=MIN, maxLength=MAX)` +|`@Email` |`@constraint(format='email')` +|`@Max` |`@constraint(max=VALUE)` +|`@Min` |`@constraint(min=VALUE)` +|`@Pattern(REGEXP)` |`@constraint(pattern=REGEXP)` +|=== + +Note: The `@NotNull` annotation does not map to a directive, instead it makes the GraphQL type non-nullable. + +Constraints will only appear on fields of input types and operation arguments. diff --git a/content/docs/smallrye-graphql/latest/dynamic-client-error-handling.adoc b/content/docs/smallrye-graphql/latest/dynamic-client-error-handling.adoc new file mode 100644 index 000000000..e7a9b5f79 --- /dev/null +++ b/content/docs/smallrye-graphql/latest/dynamic-client-error-handling.adoc @@ -0,0 +1,34 @@ += Error handling with dynamic clients +include::_attributes.adoc[] + +== System-level errors + +If the service returns a system-level error (which means the response body doesn't contain +a valid GraphQL response document), the client invocation will (instead of returning a +`io.smallrye.graphql.client.Response`) throw a `io.smallrye.graphql.client.InvalidResponseException` +whose message contains information about the received status code, status message, and body +contents. + +== Application-level errors + +If the service returns one or more application-level errors (which means that +there is valid GraphQL response in the body that has a non-empty `errors` entry), the +client invocation will return a `io.smallrye.graphql.client.Response` as normal, and that object +will contain information about the errors. Errors are represented as `io.smallrye.graphql.client.GraphQLError` +objects and can be inspected after retrieving a list of all errors using `response.getErrors()`. + +____ +++[++NOTE++]++ +An error response is considered application-level regardless of the HTTP status code as long as it +contains a valid GraphQL response. We define a GraphQL response as a well-formed JSON document +that contains at least one of `errors`, `data` and `extensions` entries, and no other entries beyond +that. +____ + +It is also possible to convert a response's errors into a `io.smallrye.graphql.client.GraphQLClientException` +by calling `throwExceptionIfErrors()` - the response has to be cast to `ResponseImpl` for this as of now. +This method will, if there are any errors in the response, convert the errors into a `GraphQLClientException` +and throw it. + +If you need to access the HTTP response code and message pertaining to a response that contained +application-level errors, refer to xref:dynamic-client-usage.adoc#accessing-http-headers-and-response-codes[Accessing HTTP headers and response codes]. diff --git a/content/docs/smallrye-graphql/latest/dynamic-client-usage.adoc b/content/docs/smallrye-graphql/latest/dynamic-client-usage.adoc new file mode 100644 index 000000000..26832e7d1 --- /dev/null +++ b/content/docs/smallrye-graphql/latest/dynamic-client-usage.adoc @@ -0,0 +1,136 @@ += Dynamic client introduction and basic usage +include::_attributes.adoc[] + +A Java GraphQL client. The main difference from the typesafe client is +that while the typesafe client behaves like a typesafe proxy very +similar to the MicroProfile REST Client, the dynamic client is more like +the JAX-RS client from the `jakarta.ws.rs.client` package. Instead of +working with model classes directly, the dynamic client focuses on +programmatically working with GraphQL documents representing GraphQL +requests and responses. It still offers the option to convert between +documents and model classes when necessary. + +In the current implementation, Vert.x HTTP client is used for handling +the underlying traffic. + +Creating a client instance + +Generally there are two ways to obtain a client instance. + +First, using CDI injection where the configuration values are defined in system +properties: + +[source,java] +---- +@Inject +@GraphQLClient("superheroes") +DynamicGraphQLClient client; + +// assuming that this system property exists: +// superheroes/mp-graphql/url=https://superheroes.org/graphql +---- + +The above example assumes that configuration for the client is present in system properties. For a full list of +supported configuration properties, see xref:client-configuration.adoc[Client configuration reference] + +The other way to build a client is programmatically using a builder: + +[source,java] +---- +DynamicGraphQLClient client = DynamicGraphQLClientBuilder.newBuilder() + .url("https://superheroes.org/graphql") + .build(); +---- + +The usage examples in the following sections will assume using the first approach - injection. + +Basic Usage + +Given the following GraphQL service on the server side: + +[source,java] +---- +@GraphQLApi +class SuperHeroesApi { + @Query + List allHeroesIn(String location) { + // .... + } +} + +class SuperHero { + private String name; + private List superPowers; +} +---- + +Such service can be queried this way: + +[source,java] +---- +include::{examples-dir}/examples/dynamicclient/MyClientUsage.java[] +---- + +* ++<++1++>++ Creating the document representing the request. We used static +imports to make the code easy to read, they all come from the +classes in the `io.smallrye.graphql.client.core` package. +* ++<++2++>++ Executing the request. You can either do that in a blocking way, or +request a `Uni` if you prefer the reactive style. +* ++<++3++>++ Obtaining the resulting list of heroes as a `JsonArray`. +* ++<++4++>++ Obtaining the resulting list of heroes as instances of the model +class. This is optional, you can continue working with the data as a `JsonArray` if you prefer. + +Using plain strings instead of the DSL + +If you don't like the DSL for some reason and want to use plain strings for your queries, these two examples +will serve you: + +[source,java] +---- +include::{examples-dir}/examples/dynamicclient/MyClientUsageString.java[] +---- + +* ++<++1++>++: In this variant, we inline values for query arguments directly into the query string. +* ++<++2++>++: In this variant, argument values are extracted into variables. The `location` argument of the `allHeroesIn` query is +declared to be using the variable `loc` (the variable can also be named `location` same as the argument, if +you prefer). +* ++<++3++>++: Here we create a map that defines the values for each variable. Values of this map are `Object`s, +so you can put in strings, numbers, booleans, or any object that corresponds to a GraphQL type and can be +serialized to JSON. Inserting a `JsonObject` directly is also supported. + +Accessing HTTP headers and response codes + +To access HTTP transport metadata that was passed by the server, you can inspect the `Response` object. + +`Response.getTransportMeta("HEADER-NAME")` returns a `List++<++String++>++` containing (potentially multiple) values of the requested header. + +To get the HTTP status code or status message, these are stored inside the `transportMeta` map too. `ResponseImpl` +contains convenience methods to retrieve them: `ResponseImpl.getStatusCode()` and `ResponseImpl.getStatusMessage()`. +It's also possible to retrieve them directly without casting to `ResponseImpl` by calling +`Integer.valueOf(response.getTransportMeta().get("++<++status-code++>++").get(0))` and + +`response.getTransportMeta().get("++<++status-message++>++").get(0)`. + +HTTP headers, status codes and messages are only available for operations executed over pure HTTP, not via websockets! + +Sending extensions to the server + +The GraphQL over HTTP specification allows clients to send an `extensions` map alongside the query. To do this +with the dynamic client, build a `Request` using the `RequestImpl.builder` and pass it to `executeSync` or +`executeAsync`: + +[source,java] +---- +Map extensions = Map.of("myKey", "myValue", "number", 42); + +Request request = RequestImpl.builder("{ myQuery }") + .variables(variables) // optional + .operationName("MyOperation") // optional + .extensions(extensions) + .build(); + +Response response = client.executeSync(request); +---- + +On the server side, the extensions sent by the client can be read from `SmallRyeContext.getExtensionsFromClient()`. +This is separate from `SmallRyeContext.getAddedExtensions()`, which is used for adding extensions to the +outgoing response. diff --git a/content/docs/smallrye-graphql/latest/extensions.adoc b/content/docs/smallrye-graphql/latest/extensions.adoc new file mode 100644 index 000000000..bad6a5991 --- /dev/null +++ b/content/docs/smallrye-graphql/latest/extensions.adoc @@ -0,0 +1,86 @@ += Extensions in SmallRye GraphQL +include::_attributes.adoc[] + +`SmallRyeContext` provides two separate extension mechanisms: + +* *Outbound extensions* (`addedExtensions`) — extensions added by the server and sent back to the client in the response. +* *Inbound extensions* (`extensionsFromClient`) — extensions received from the client as part of the incoming request. + +== Adding extensions to the response + +*SmallRye GraphQL* allows the user to add their own extensions inside the GraphQL response +which is received from the server. The `extensions` are stored next to the `data` field in the GraphQL response. + +[source,json] +---- +{ + "data": { + "shirt": { + "size": "L" + } + }, + "extensions": { + "country": "Germany", + "pi": 3.1415926535 + } +} +---- + +To add extensions, you need to `@Inject` an instance of `SmallRyeContext` in your `@GraphQLApi` annotated class. +After that, you can add your own extensions via the method `addExtension` +with its input parameters: `key : String` as an identification of the added extension, +and `value: Object` as a value of the extension. Value can be any given object that can be converted into JsonValue. + +As an example, this class below contains the query `getShirt` and during the http request, the query adds these extensions: +`++{++"country": "Germany", "pi": 3.1415926535}`. +These extensions will be sent back via response in the `extensions` field. + +[source,java] +---- +import jakarta.inject.Inject; + +@GraphQLApi +public class ShirtResources { + @Inject + SmallRyeContext smallRyeContext; + + @Query + public Shirt getShirt() { + smallRyeContext.addExtension("country", "Germany"); + smallRyeContext.addExtension("pi", 3.1415926535); + //... + } +} +---- + +____ +++[++NOTE++]++ +You can also use `getAddedExtensions()` to access the underlying map directly. +____ + +== Reading extensions from the client request + +Clients can send an `extensions` map alongside the query (as defined by the GraphQL over HTTP specification). +On the server side, these can be read via `SmallRyeContext.getExtensionsFromClient()`, which returns a +`Map++<++String, Object++>++` (or `null` if the client did not send any extensions). + +[source,java] +---- +import jakarta.inject.Inject; + +@GraphQLApi +public class MyApi { + @Inject + SmallRyeContext smallRyeContext; + + @Query + public String process() { + Map clientExtensions = smallRyeContext.getExtensionsFromClient(); + if (clientExtensions != null) { + String traceId = (String) clientExtensions.get("traceId"); + // ... + } + //... + } +} +---- diff --git a/content/docs/smallrye-graphql/latest/federation.adoc b/content/docs/smallrye-graphql/latest/federation.adoc new file mode 100644 index 000000000..827f544eb --- /dev/null +++ b/content/docs/smallrye-graphql/latest/federation.adoc @@ -0,0 +1,213 @@ += Federation +include::_attributes.adoc[] + +Support for https://www.apollographql.com/docs/federation[GraphQL Federation] is enabled by default. If you add one of the federation annotations, the corresponding directives will be declared in your schema, and the additional Federation queries will be added automatically. You can also disable Federation completely by setting the `smallrye.graphql.federation.enabled` config key to `false`. + +You can add the Federation directives by using the equivalent Java annotation, e.g. to extend a `Product` entity with a `price` field, you can write a class: + +[source,java] +---- +package org.example.price; + +import org.eclipse.microprofile.graphql.Id; + +import io.smallrye.graphql.api.federation.Extends; +import io.smallrye.graphql.api.federation.Key; + +@Extends @Key(fields = @FieldSet("id")) +public class Product { + @Id + private String id; + + @Description("The price in cent") + private Integer price; + + // getters and setters omitted +} +---- + +And a normal query method that takes the `key` fields as a parameter and returns the requested type: + +[source,java] +---- +package org.example.price; + +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Id; +import org.eclipse.microprofile.graphql.Query; + +@GraphQLApi +public class Prices { + @Query + public Product product(@Id String id) { + return [...] + } +} +---- + +The GraphQL Schema then contains: + +[source,graphql] +---- +type Product @extends @key(fields : "id") { + id: ID + price: Int +} + +union _Entity = Product + +type Query { + _entities(representations: [_Any!]!): [_Entity]! + _service: _Service! + product(id: ID): Product +} +---- + +If you can resolve, e.g., the product with different types of ids, you can add multiple `@Key` annotations. +## Federation Batch Resolver + +For better performance, there is the option to use batch resolvers with federation. This is not activated by default. It can be activated by setting `smallrye.graphql.federation.batchResolving.enabled` config key to `true`. + +It is unnecessary to provide batch resolvers for all entities, if there is no batch resolver, a non-batch resolver is used. + +[source,java] +---- +package org.example.price; + +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Id; +import org.eclipse.microprofile.graphql.Query; + +@GraphQLApi +public class Prices { + @Query + public List product(@Id List id) { + return [...] + } +} +---- + +It is crucial that the sequence of the argument list matches the order of the result list. Currently, the name of the Argument `id` must match with the property name in the type. + +== Federation Reference Resolver + +In federation, you may also want to extend the external type by some fields without publishing queries into the schema. You can do it by using the annotation `@Resolver`. + +[source,java] +---- +@Extends +@Key(fields = @FieldSet("upc")) +public final class Product { + @External + @NonNull + private String upc; + @External + private Integer weight; + @External + private Integer price; + private Boolean inStock; + @Requires(fields = @FieldSet("price weight")) + private Integer shippingPrice; +} + +@GraphQLApi +public class Api { + @Query // 0 query, that will be added into schema + public Product findByUPC(String upc) { + return new Product(upc , ...etc); + } + + @Resolver // 1 You dont receive external fields price weight here, just key + public Product resolveByUPC(String upc) { + return new Product(upc , ...etc); + } + + @Resolver // 2 The order of variables doesn't matter + public Product resolveByUPCForShipping(int price, String upc, @Name("weight") int someWeight) { + return new Product(upc , someWeight, price, (price * someWeight) /*calculate shippingPrice */, ...etc); + } + + @Resolver // 3 + public Product resolveByUPCForSource(int price, String upc) { + return new Product(upc, price, ...etc); + } + + @Requires(fields = @FieldSet("price")) + public int anotherWeight(@Source Product product) { + return product.price() * 2; + } +} +---- + +Generates the following schema: + +.... +type Product @extends @key(fields : "upc") { + anotherWeight: Int! @requires(fields : "price") + inStock: Boolean + price: Int @external + shippingPrice: Int @requires(fields : "price weight") + upc: String! @external + weight: Int @external +} + +type Query { + _entities(representations: [_Any!]!): [_Entity]! + _service: _Service! +} +.... + +These methods will only be available to the federation router, which may send requests like the following examples: + +.... +// request 1 +query { + _entities(representations: [{ + "__typename": "Product", + "upc": "1" // just id key + }]) { + __typename + ... on Product { + inStock + } + } +} + +// request 2 +query { + _entities(representations: [{ + "__typename": "Product", + "upc": "1", // id key + "price": 100, // shippingPrice requires this field + "weight": 100 // shippingPrice requires this field + }]) { + __typename + ... on Product { + inStock + shippingPrice + } + } +} + +// request 3 +query { + _entities(representations: [{ + "__typename": "Product", + "upc": "2", + "price": 1299 // anotherWeight requires this field + } + ]) { + __typename + ... on Product { + anotherWeight + } + } +} +.... + +Unfortunately, you must make separate methods with different `@External` parameters. + +It is not currently possible to combine them into one separate type. + +You also can use `@Query` (if you want to add queries into the schema) or `@Resolver` (requests 0 and 1). +And if it was requested, `++_++entities` - `@Resolvers` methods are checked first (they have higher priority). diff --git a/content/docs/smallrye-graphql/latest/gradle-plugin.adoc b/content/docs/smallrye-graphql/latest/gradle-plugin.adoc new file mode 100644 index 000000000..843bf33df --- /dev/null +++ b/content/docs/smallrye-graphql/latest/gradle-plugin.adoc @@ -0,0 +1,84 @@ += SmallRye GraphQL Gradle plugin +include::_attributes.adoc[] + +This Gradle plugin allows you to generate the GraphQL Schema on build and save it as a text file. + +Add this to your build.gradle: + +.... +plugins { + id 'io.smallrye.graphql' version 'PLUGIN_VERSION' +} +.... + +The list of existing plugin versions (to substitute the +`PLUGIN++_++VERSION` placeholder) can be found +https://plugins.gradle.org/plugin/io.smallrye.graphql[here]. + +The schema will appear as `build/generated/schema.graphql` by default. + +The plugin’s `generateSchema` task can customized like: + +.... +generateSchema { + destination = null + includeTransitiveDependencies = true + includeScalars = true +} +.... + +This will include the project’s dependencies (including the transitive +dependencies) to scan for GraphQL model classes. As the destination is +set to `null` the generated schema will only be output to the console +and saved to a file. + +In case external dependencies need to be scanned for GraphQL model +classes a more advanced configuration could look like: + +.... +configrations { + graphQLSchema +} + +dependencies { + graphQLSchema .. +} + +generateSchema { + includeDependencies = true + configurations = ["graphQLSchema"] +} +.... + +This defines a separate `graphQLSchema` configuration. In the +dependencies block the external dependencies containing the GraphQL mode +class should then be registered for the `graphQLSchema` configuration. +The `generateSchema` task is then customized to only include +dependencies from the `graphQLSchema` configuration. Also note that we +set `includeDependencies` to `true` (and not +`includeTransitiveDependencies`). This will make sure only direct +dependencies will be be scanned and not possible transitive +dependencies. + +Configuration options + +* `destination` - To override the default +`build/generated/schema.graphql` destination. +* `includeDependencies` - Scan project’s dependencies for GraphQL +model classes too. Default false. +* `includeTransitiveDependencies` - Scan project’s dependencies +(including the transitive dependencies) for GraphQL model classes +too. Default false. +* `configurations` - If the above `includeDependencies` or +`includeTransitiveDependencies` is true, you can control what +configurations should be included. Default is `implementation`. +* `dependencyExtensions` - If the above `includeDependencies` or +`includeTransitiveDependencies` is true, you can control what +dependency extensions should be included. Default is `jar`. +* `includeScalars` - Include scalars in the schema. Default false. +* `includeDirectives` - Include directives in the schema. Default +false. +* `includeSchemaDefinition` - Include the schema definition. Default +false. +* `includeIntrospectionTypes` - Include the introspection types in the +schema. Default false. diff --git a/content/docs/smallrye-graphql/latest/handling-init-payload-from-the-websocket.adoc b/content/docs/smallrye-graphql/latest/handling-init-payload-from-the-websocket.adoc new file mode 100644 index 000000000..a691d6802 --- /dev/null +++ b/content/docs/smallrye-graphql/latest/handling-init-payload-from-the-websocket.adoc @@ -0,0 +1,25 @@ += Handling of the WebSocket's init-payload +include::_attributes.adoc[] + +When executing GraphQL operations over WebSockets (both with the `graphql-ws` and `graphql-transport-ws` protocols), +the client can send an initial payload +to the server when establishing the WebSocket connection. This payload can contain +any information that the server might need to process GraphQL requests, like client agent identification (similar to the `User-Agent` header) or data specific to the client. On the server-side, you can access this payload +via the `GraphQLContext`, which is available through the `DataFetchingEnvironment` object. + +[source,java] +---- +@GraphQLApi +public class HeroResources { + + @Inject + SmallryeContext context; + + // ... + Map initPayload = smallRyeContext + .getDataFetchingEnvironment() + .getGraphQlContext() + .get("init-payload"); + // ... +} +---- diff --git a/content/docs/smallrye-graphql/latest/images/redhat_reversed.svg b/content/docs/smallrye-graphql/latest/images/redhat_reversed.svg new file mode 100644 index 000000000..37fe83c48 --- /dev/null +++ b/content/docs/smallrye-graphql/latest/images/redhat_reversed.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/content/docs/smallrye-graphql/latest/index.adoc b/content/docs/smallrye-graphql/latest/index.adoc new file mode 100644 index 000000000..f277286e0 --- /dev/null +++ b/content/docs/smallrye-graphql/latest/index.adoc @@ -0,0 +1,6 @@ += Overview +include::_attributes.adoc[] + +SmallRye GraphQL is a library that allows you to easily use GraphQL (both server-side endpoints +and client-side code) in your Java-based projects. It implements +the MicroProfile GraphQL specification. diff --git a/content/docs/smallrye-graphql/latest/inspecting-executable-directives-on-server-side.adoc b/content/docs/smallrye-graphql/latest/inspecting-executable-directives-on-server-side.adoc new file mode 100644 index 000000000..cd37d1852 --- /dev/null +++ b/content/docs/smallrye-graphql/latest/inspecting-executable-directives-on-server-side.adoc @@ -0,0 +1,39 @@ += Inspecting executable directives on the server side +include::_attributes.adoc[] + +Let's say you have this _GraphQL query_ with an executable directive on a field _location_. + +.... +{ + hero(name: $nameValue) { + name + location @customDirective(arg: true) + } +} +.... + +To inspect your directive on the server side, you must inject a `SmallRyeContext` object and get the relevant information like this. + +[source,java] +---- + @Inject + SmallRyeContext context; + + @Query + public SuperHero getHero(String name) { + // ... + SelectionSet selectionSet = (SelectionSet) context.getDataFetchingEnvironment().getField().getChildren().get(0); + Optional locationNode = selectionSet.getChildren().stream() + .filter(node -> node instanceof Field && ((Field) node).getName().equals("location")).findAny(); // "location" is the field name where the directive is applied. + locationNode.ifPresent(node -> { + List directives = ((Field) node).getDirectives(); + System.out.println(directives); + }); + // the rest of the query... + } +---- + +____ +++[++NOTE++]++ +that `Field` refers to `graphql.language.Field`, not `io.smallrye.graphql.schema.model.Field` +____ diff --git a/content/docs/smallrye-graphql/latest/kotlin.adoc b/content/docs/smallrye-graphql/latest/kotlin.adoc new file mode 100644 index 000000000..b6ccf535d --- /dev/null +++ b/content/docs/smallrye-graphql/latest/kotlin.adoc @@ -0,0 +1,4 @@ += SmallRye Graphql in Kotlin +include::_attributes.adoc[] + +When working with SmallRye GraphQL in Kotlin, it is very important to use the `-Xemit-jvm-type-annotations` flag for the Kotlin compiler. This flag is important to generate the necessary annotations for reflection to work properly. The flag is supported as of Kotlin 1.3.70. diff --git a/content/docs/smallrye-graphql/latest/maven-plugin.adoc b/content/docs/smallrye-graphql/latest/maven-plugin.adoc new file mode 100644 index 000000000..959ef8596 --- /dev/null +++ b/content/docs/smallrye-graphql/latest/maven-plugin.adoc @@ -0,0 +1,48 @@ += SmallRye GraphQL Maven plugin +include::_attributes.adoc[] + +This Maven plugin allows you to generate the GraphQL Schema on build and save it as a text file. + +Add this to your pom.xml: + +[source,xml] +---- + + smallrye-graphql-maven-plugin + io.smallrye + + + + generate-schema + + + + +---- + +The schema will appear as `target/generated/schema.graphql` by default. The goal is bound to the `process-classes` +build phase. + +Configuration options + +* `destination` - To override the default `target/generated/schema.graphql` destination +* `includeDependencies` - Scan project’s dependencies for GraphQL +model classes too. This is necessary if your GraphQL endpoint exposes +classes that are not in the application's main module. Off (`false`) by default, because +scanning a potentially large dependency tree slows down execution substantially. +Usage of reactive types does not require turning this option on. +* `includeDependenciesScopes` - If the above `includeDependencies` is +true, you can control what scopes should be included. Default is +`compile, system` +* `includeDependenciesTypes` - If the above `includeDependencies` is +true, you can control what types should be included. Default is +`jar` +* `includeScalars` - Include scalars in the schema. Default false. +* `includeDirectives` - Include directives in the schema. Default false. +* `includeSchemaDefinition` - Include the schema definition. Default false. +* `includeIntrospectionTypes` - Include the introspection types in the schema. Default false. +* `federationEnabled` - Enable GraphQL Federation. This is automatically +enabled if any Federation-related annotations are found in your application, otherwise the default is false. +You should generally use this in conjunction with `includeDirectives`. +* `typeAutoNameStrategy` - Strategy for transforming class names into GraphQL type names. +Valid values are `MergeInnerClass`, `Full` and`Default`. diff --git a/content/docs/smallrye-graphql/latest/mutation-void.adoc b/content/docs/smallrye-graphql/latest/mutation-void.adoc new file mode 100644 index 000000000..4fc047e25 --- /dev/null +++ b/content/docs/smallrye-graphql/latest/mutation-void.adoc @@ -0,0 +1,24 @@ += Returning Void Type in Mutations with SmallRye GraphQL +include::_attributes.adoc[] + +While by default *MicroProfile GraphQL* specification doesn't provide direct support for returning `void` type from mutation operations, *SmallRye GraphQL* has implemented this feature based on user feedback. + +To implement a *mutation* operation that returns `void` or `Void` type, you can use the following pattern in your annotated `@GraphQLApi` class: + +[source,java] +---- + @Mutation + public void createHero(SuperHero hero) { + superHeroService.create(hero); + } +---- + +In this example, `createHero()` receives a SuperHero object as input, +performs some operation with it (in this case, creating it using a +`superHeroService`). + +____ +++[++NOTE++]++ +On the GraphQL level, this is achieved by returning a special scalar type named Void from the mutation. +This scalar type will always have a value of null. +____ diff --git a/content/docs/smallrye-graphql/latest/namespaces-on-server-side.adoc b/content/docs/smallrye-graphql/latest/namespaces-on-server-side.adoc new file mode 100644 index 000000000..896ad6537 --- /dev/null +++ b/content/docs/smallrye-graphql/latest/namespaces-on-server-side.adoc @@ -0,0 +1,167 @@ += Namespacing on the server side +include::_attributes.adoc[] + +____ +++[++NOTE++]++ +Using approaches to form namespaces in the schema can be useful for large APIs. There are several ways to do this. +However, read the documentation carefully, especially the limitations and possible problems. +____ + +____ +++[++NOTE++]++ You can only use one of the annotations - `@Name` or `@Namespace` over the GraphQLApi classes. +____ + +== Using @Namespace annotation + +The annotation accepts an array of strings containing the nesting of the namespace. +This method allows you to create any nesting of namespaces. +You can use any nesting and also combine different levels. + +[source,java] +---- +@GraphQLApi +@Namespace({"admin", "users"}) +@Description("Admin users operations") +public class AdminUsersApi { + @Query + public List findAll() { + // + } +} + +@GraphQLApi +@Namespace({"admin"}) +@Description("Admin operations") +public class AdminApi { + @Query + public List findAll() { + // + } +} +---- + +Will generate schema + +.... +"Query root" +type Query { + admin: AdminQuery +} + +"Admin operations" +type AdminQuery { + users: AdminUsersQuery + findAll: [Admin] +} + +"Admin users operations" +type AdminUsersQuery { + findAll: [User] +} + +type Admin { + id: BigInteger + ... +} + +type User { + id: BigInteger + ... +} +.... + +And you can send GraphQL request like: + +.... +query { + admin { + users { + findAll { + id + } + } + } +} +.... + +== Using @Name annotation (deprecated) + +____ +++[++NOTE++]++ This feature may be removed in the future. +____ + +It does the same thing as `@Namespace`, the only difference is that there can only be one nesting level. + +[source,java] +---- +@GraphQLApi +@Name("users") +@Description("Users operations") +public class UserApi { + @Query + public List findAll() { + // + } +} +---- + +As a result, you can get methods with the same names. + +.... +query { + users { + findAll { + .... + } + } +} +.... + +== Problems + +While dividing APIs into namespaces may seem convenient, several issues are important to be aware of. + +=== Mutations + +On the client side, be careful when working with mutations. +This violates the GraphQL specification since mutations in this form can be executed in parallel. +Read more here about namespaces https://www.apollographql.com/docs/technotes/TN0012-namespacing-by-separation-of-concern/[Namespacing by Separation of Concerns]. +This article describes how to work with namespaces, what problems you may encounter, and how to solve them. + +What does GraphQL say about this - https://benjie.dev/graphql/nested-mutations["GraphQL" Nested Mutations] + +In summary, you can use nested mutations, but with some overhead on the client side. Be careful with mutations. + +==== Subscriptions + +GraphQL does not guarantee subscription resolution within namespaces. +While you can define subscriptions in namespaces, the generated schema will be valid, but the subscription will not be resolved. +For example, if you try to run such a subscription request, you will get an error. This is the behavior of `graphql-java`. + +[source,java] +---- +@GraphQLApi +@Namepace("resource") +public class ResourceApi { + @Subscription + public Multi resourceChange() { + // + } +} +---- + +.... +subscription { + resource { + resourceChange { + .... + } + } +} +.... + +There is currently no way around this problem. +You must move subscriptions into a separate `@GraphQLApi` class that does not declare any namespace. +++>++ ++[++NOTE++]++ +++>++ Be very careful when designing API with namespace. +++>++ And be aware of all the potential consequences when working with mutations and subscriptions. diff --git a/content/docs/smallrye-graphql/latest/server-configuration.adoc b/content/docs/smallrye-graphql/latest/server-configuration.adoc new file mode 100644 index 000000000..054c01128 --- /dev/null +++ b/content/docs/smallrye-graphql/latest/server-configuration.adoc @@ -0,0 +1,38 @@ += Configuration properties +include::_attributes.adoc[] + +These are properties understood directly by SmallRye GraphQL. If you're using SmallRye GraphQL through +Quarkus, these will generally work, but Quarkus offers its own counterparts for most of these, +and it is recommended to use the `quarkus.++*++` properties. Refer to the Quarkus documentation: +https://quarkus.io/guides/all-config#quarkus-smallrye-graphql_quarkus-smallrye-graphql-smallrye-graphql[Server] and +https://quarkus.io/guides/all-config#quarkus-smallrye-graphql-client_quarkus-smallrye-graphql-client-smallrye-graphql-client[Client] +side Quarkus properties. + +== From MicroProfile GraphQL + +[width="100%",cols="33%,35%,32%",options="header",] +|=== +|Property |Default value |Meaning +|`mp.graphql.defaultErrorMessage` |`Server Error` |Error message for hidden exceptions +|`mp.graphql.hideErrorMessage` |Runtime exceptions |Exceptions that will be hidden +|`mp.graphql.showErrorMessage` |Checked exceptions |Exceptions that will not be hidden. +|=== + +== From SmallRye GraphQL + +[width="100%",cols="33%,35%,32%",options="header",] +|=== +|Property |Default value |Meaning +|`smallrye.graphql.printDataFetcherException` |`false` |Include the stacktrace of the data fetching exception in the log output +|`smallrye.graphql.allowGet` |`false` |Allow HTTP GET Method +|`smallrye.graphql.metrics.enabled` |`false` |Enable metrics +|`smallrye.graphql.tracing.enabled` |`false` |Enable tracing +|`smallrye.graphql.validation.enabled` |`true` if Bean Validation is present |Enable Bean Validation. This property is DEPRECATED, setting to `false` won't actually turn off validation. It will be removed in a future release. +|`smallrye.graphql.events.enabled` |`true` if one of metrics, tracing or bean validation is true |Enable eventing +|`smallrye.graphql.logPayload` |`false` |Log the payload in the log file +|`smallrye.graphql.fieldVisibility` | |To control the field visibility on introspection +|`smallrye.graphql.schema.includeScalars` |`true` |Include Scalar definitions in the schema +|`smallrye.graphql.schema.includeSchemaDefinition` |`false` |Include Schema definition +|`smallrye.graphql.schema.includeDirectives` |`false` |Include directives in the schema +|`smallrye.graphql.schema.includeIntrospectionTypes` |`false` |Include Introspection types in the schema +|=== diff --git a/content/docs/smallrye-graphql/latest/target-for-input-types.adoc b/content/docs/smallrye-graphql/latest/target-for-input-types.adoc new file mode 100644 index 000000000..0b990f0c1 --- /dev/null +++ b/content/docs/smallrye-graphql/latest/target-for-input-types.adoc @@ -0,0 +1,72 @@ += Target fields for input types +include::_attributes.adoc[] + +The `@Source` annotation extends *output* types by adding externally-defined fields. Its +counterpart for *input* types is `@Target` (`io.smallrye.graphql.api.Target`). + +With `@Target`, you can add fields to a GraphQL input type the same way `@Source` adds fields to +an output type. The annotated parameter receives the input object, and a sibling parameter +receives the value sent by the client for that field. The method name becomes the input field +name in the schema (overridable with `@Name`), and `@Description` can be added on the method. + +Server-side example: + +[source,java] +---- +import io.smallrye.graphql.api.Target; + +@GraphQLApi +public class CharacterService { + + @Mutation + public Character save(Character input) { + // the input may have been modified by @Target methods below + return input; + } + + public void applyTwitterHandle(@Target Character character, String twitterHandle) { + character.setTwitterHandle(twitterHandle); + } +} +---- + +This generates an extended input type in the schema: + +[source,graphql] +---- +input CharacterInput { + # ... regular fields ... + twitterHandle: String # added by the applyTwitterHandle method +} +---- + +When a client sends a mutation that includes the `twitterHandle` field, the corresponding +`@Target` method is invoked at runtime. If the field is omitted, the method is not called. + +Multiple `@Target` methods on the same input type are supported: + +[source,java] +---- +public void applyTwitterHandle(@Target Character character, String twitterHandle) { + character.setTwitterHandle(twitterHandle); +} + +public void applyNumber(@Target Character character, Integer rating) { + character.setRating(rating); +} +---- + +A `Context` parameter can be injected into target methods: + +[source,java] +---- +public void applyWithContext(@Target Character character, String value, Context context) { + String fieldName = context.getFieldName(); + // ... +} +---- + +____ +++[++NOTE++]++ +`@Target` is currently marked as experimental. +____ diff --git a/content/docs/smallrye-graphql/latest/typesafe-client-accessing-metadata-of-responses.adoc b/content/docs/smallrye-graphql/latest/typesafe-client-accessing-metadata-of-responses.adoc new file mode 100644 index 000000000..cb9875c8d --- /dev/null +++ b/content/docs/smallrye-graphql/latest/typesafe-client-accessing-metadata-of-responses.adoc @@ -0,0 +1,81 @@ += Accessing Metadata of GraphQL Responses with TypesafeResponse +include::_attributes.adoc[] + +The TypesafeResponse class provides a wrapper around the "data" +field of GraphQL responses, along with additional metadata such as errors, +extensions, and transport metadata. Here are the methods you can use to access each type of metadata: + +== Transport metadata + +use `getTransportMeta()` method to retrieve a map of transport metadata associated with the response. +++>++ ++[++NOTE++]++ +++>++ Note that the transport metadata referred to in the context of HTTP requests +++>++ and responses is commonly found in the headers of the communication. +++>++ Therefore, the getTransportMeta() method mentioned here can be used to +++>++ retrieve a map of HTTP headers. +### Extensions +use `getExtensions()` method to retrieve a JsonObject of response extensions. +The method returns a JSON object that represents the response extensions. + +=== Data + +use `get()` method to retrieve the data object. The method returns the data +object if it exists and there are no errors. If there are errors, +it throws a NoSuchElementException with the error message. + +=== Errors + +use `getErrors()` method to retrieve a list of GraphQL errors. +The method returns a list of errors if there are errors and no data object. +If there is a data object, it throws a NoSuchElementException with the error message. + +____ +++[++NOTE++]++ +Note that the get() method assumes that the response has a data object, +and will throw an exception if there are errors without a data object. +If you want to check for errors before retrieving the data object, +use the hasErrors() method, which returns true if there are errors and false otherwise. +____ + +You can use these methods to access and process the metadata of GraphQL +responses returned by the TypesafeResponse class. + +Let's say you want to get as a client extensions from the GraphQL response. +First of all you will need to modify your `GraphQLClientApi` interface to have +your return type class (in this case `SuperHero`) wrapped with +`TypesafeResponse` class. +I.e. your `GraphQLClientApi` interface could look like this: + +[source,java] +---- +import examples.typesafeclient.SuperHero; +import io.smallrye.graphql.client.typesafe.api.TypesafeResponse; + +@GraphQLClientApi +public interface SuperHeroApi { + // ... + TypesafeResponse findSuperHeroByName(String name); + // ... +} +---- + +____ +++[++NOTE++]++ +Note that you can have `TypesafeResponse++<++T++>++` only as a result (return) type of your query. +Unlike `ErrorOr` the class cannot wrap individual fields. +If that is the case `IllegalArgumentException` will be thrown. +____ + +And now to get `extensions` from the response your code will look like something like this: + +[source,java] +---- + @Inject + SuperHeroApi client; + + TypesafeResponse superheroResponse = client.findSuperHeroByName("Batman"); + // returns the "data" field + SuperHero superhero = superhero.get(); + // returns the "extensions" field + JsonObject extensions = superhero.getExtensions(); +---- diff --git a/content/docs/smallrye-graphql/latest/typesafe-client-custom-scalars.adoc b/content/docs/smallrye-graphql/latest/typesafe-client-custom-scalars.adoc new file mode 100644 index 000000000..c6fbc61ce --- /dev/null +++ b/content/docs/smallrye-graphql/latest/typesafe-client-custom-scalars.adoc @@ -0,0 +1,38 @@ += Custom scalar types +include::_attributes.adoc[] + +If the remote service contains custom scalar types represented as strings in GraphQL documents, +a typesafe client can learn to understand these types through introducing a class that represents them. +The class has to contain logic necessary for serializing and deserializing from/to raw strings. + +A model class is considered to represent a scalar GraphQL type if it contains a static builder method +named `of`, `valueOf`, or `parse` whose sole parameter is a `java.lang.String` and it returns an +instance of the class itself. The name of the GraphQL type is assumed to be equal to the short name +of the class. + +For example, if the server supports a `URL` type (there is a `scalar URL` clause in the schema), +create a class that represents a value for `URL`: + +[source,java] +---- +public class URL { + + private String value; + + public URL(String value) { + this.value = value; + } + + public static URL valueOf(String value) { // <1> + return new URL(value); + } + + @Override + public String toString() { // <2> + return value; + } +} +---- + +* ++<++1++>++ The static `valueOf(String)` method will be used for deserializing a `URL` from a string. +* ++<++2++>++ The `toString()` will be used for serializing a `URL` into a string to be sent to the GraphQL service diff --git a/content/docs/smallrye-graphql/latest/typesafe-client-directives.adoc b/content/docs/smallrye-graphql/latest/typesafe-client-directives.adoc new file mode 100644 index 000000000..046e90feb --- /dev/null +++ b/content/docs/smallrye-graphql/latest/typesafe-client-directives.adoc @@ -0,0 +1,53 @@ += Typesafe Client Static Directives +include::_attributes.adoc[] + +== Custom Static Directives + +The declaration of custom static directives for the typesafe client follows the same principles as the server-side xref:directives.adoc[directives]. With only exception of using the https://spec.graphql.org/draft/#ExecutableDirectiveLocation[Executable Directive Locations] instead of Type System Directive Locations. + +[source,java] +---- +@Directive(on = { FIELD, VARIABLE_DEFINITION }) +@Retention(RUNTIME) +public @interface MyDirective { + String value() default ""; +} +---- + +Applying this to your API should look like this: + +[source,java] +---- +import io.smallrye.graphql.client.typesafe.api.GraphQLClientApi; + +@GraphQLClientApi +public @interface MyClientApi { + + @Query + @MyDirective("top-level field") + MySuperHero getMySuperHero(@MyDirective("variable definition") String nameOfMySuperHero); + + class MySuperHero { + @MyDirective("field") + String name; + } +} +---- + +Your API is going to generate a query something like this: + +.... +query mySuperHero( + $nameOfMySuperHero: String @myDirective(value: "variable definition") +) { + mySuperHero(nameOfMySuperHero: $nameOfMySuperHero) + @myDirective(value: "top-level field") { + name @myDirective(value: "field") + } +} +.... + +____ +++[++NOTE++]++ +The current implementation supports only FIELD and VARIABLE++_++DEFINITION locations. +____ diff --git a/content/docs/smallrye-graphql/latest/typesafe-client-error-handling.adoc b/content/docs/smallrye-graphql/latest/typesafe-client-error-handling.adoc new file mode 100644 index 000000000..dd8464f37 --- /dev/null +++ b/content/docs/smallrye-graphql/latest/typesafe-client-error-handling.adoc @@ -0,0 +1,60 @@ += Error handling in typesafe clients +include::_attributes.adoc[] + +== System-level errors + +If the service returns a system-level error (which means the response body doesn't contain +a valid GraphQL response document), the client invocation will throw a +`io.smallrye.graphql.client.InvalidResponseException` +whose message contains information about the received status code, status message, and body +contents. + +== Application-level errors + +If the service returns one or more application-level errors (which means that +there is valid GraphQL response in the body that has a non-empty `errors` entry), +the client normally throws a `GraphQLClientException` containing the details in a list of +`GraphQLClientError`. + +____ +++[++NOTE++]++ +An error response is considered application-level regardless of the HTTP status code as long as it +contains a valid GraphQL response. We define a GraphQL response as a well-formed JSON document +that contains at least one of `errors`, `data` and `extensions` entries, and no other entries beyond +that. +____ + +If the error is specific to a `location`, you can use an `ErrorOr` +wrapper on the target field; the client the maps the error to that +wrapper instead of throwing an exception. I.e. your `SuperHero` class +could look like this: + +[source,java] +---- +class SuperHero { + String name; + ErrorOr location; +} +---- + +If the service returns a response like this: + +[source,json] +---- +{ + "data": { + "superHero": { + "name": "Wolverine", + "location": null + } + }, + "errors": [{ + "message":"location unknown", + "path": ["superHero","location"], + "extensions":{"code":"location-unknown"} + }] +} +---- + +Then the `SuperHero#location` wrapper field will not contain a `value` +but only the error above. See the `ErrorOr` class for details. diff --git a/content/docs/smallrye-graphql/latest/typesafe-client-headers.adoc b/content/docs/smallrye-graphql/latest/typesafe-client-headers.adoc new file mode 100644 index 000000000..d3dea2f6c --- /dev/null +++ b/content/docs/smallrye-graphql/latest/typesafe-client-headers.adoc @@ -0,0 +1,84 @@ += Adding headers +include::_attributes.adoc[] + +To add a custom header to the http requests sent out by the GraphQL +Client, annotate your method or the API interface as `@Header`, e.g.: + +[source,java] +---- +@GraphQLClientApi +interface SuperHeroesApi { + @Header(name = "S.H.I.E.L.D.-Clearance", constant = "TOP-SECRET") + List allHeroesIn(String location); +} +---- + +The value can be specified with a `constant` or the name of a `method` for dynamic values, e.g.: + +[source,java] +---- +@GraphQLClientApi +interface SuperHeroesApi { + @Header(name = "S.H.I.E.L.D.-Clearance", method = "establishShieldClearance") + List allHeroesIn(String location); + + static Clearance establishShieldClearance() { + return userIsInRole(MANAGER) ? TOP_SECRET : PUBLIC; + } +} +---- + +* This example uses an enum, but it can be any Object; the GraphQL client +calls `toString()` to convert it. + +The method must be `static` and accessible by the interface, i.e. in the +interface itself or in one of the classes it’s nested in; if it’s in a +different class, prefix it with the fully qualified class name and a dot +`"."`, e.g. +`@Header(name = "S.H.I.E.L.D.-Clearance", method = "org.superheroes.SecurityTools.establishShieldClearance")`. + +A third option is to pass the value of a header as an API method +parameter: + +[source,java] +---- +@GraphQLClientApi +interface SuperHeroesApi { + List allHeroesIn(String location, @Header(name = "S.H.I.E.L.D.-Clearance") Clearance clearance); +} +---- + +The `@Header` parameter will not be part of the GraphQL query. + +The `name` of the header is always static, but can optionally be derived from the name of the parameter or method, i.e. if it has a `@Name` annotation, that annotation value is used. If it's not annotated, the method name (eventually stripped off a leading `get`) or the parameter name (if it's enabled in the compiler settings) is converted from camel case to kebab case, i.e. a parameter `@Header String requestIdentifier` will result in a header named `Request-Identifier`. + +`@Header` annotations can also be defined via `@Stereotype`. + +When instantiating the API with the builder, you can set (or overwrite) +one or more headers there: + +[source,java] +---- +SuperHeroesApi api = TypesafeGraphQLClientBuilder.newBuilder() + .header("S.H.I.E.L.D.-Clearance", "TOP-SECRET") + .build(SuperHeroesApi.class); +---- + +Authorization headers + +To add an `Authorization` header, instead of using the generic `@Header` +annotation, you can also use the special `@AuthorizationHeader` +annotation. It produces a `BASIC` `Authorization` header by default or a +`BEARER` token. You can configure the credentials in MP Config with a +prefix plus `/mp-graphql/` and either `username` and `password` for +`BASIC` or `bearer` for `BEARER`. The config key defaults to the fully +qualified name of the `GraphQLClientApi` interface or its `configKey`. + +You can use a custom prefix by setting the `confPrefix`. The infix +`/mp-graphql/` is still applied, unless you end the `confPrefix` with +`++*++`, e.g. +`@AuthorizationHeader(confPrefix = "org.superheroes.security.basic.++*++` +will use `org.superheroes.security.basic.username`, while `++*++` will use +plain `username`. + +`@AuthorizationHeader` annotations can be defined via `@Stereotype`. diff --git a/content/docs/smallrye-graphql/latest/typesafe-client-logging.adoc b/content/docs/smallrye-graphql/latest/typesafe-client-logging.adoc new file mode 100644 index 000000000..dad8dffc9 --- /dev/null +++ b/content/docs/smallrye-graphql/latest/typesafe-client-logging.adoc @@ -0,0 +1,7 @@ += Logging in typesafe clients +include::_attributes.adoc[] + +The Client implementation logs all GraphQL requests and responses at +level `INFO` with the interface API as the logger name. It also logs the +keys of all headers added at level `DEBUG`; not the values, as they may +be security sensitive. diff --git a/content/docs/smallrye-graphql/latest/typesafe-client-multiple-queries.adoc b/content/docs/smallrye-graphql/latest/typesafe-client-multiple-queries.adoc new file mode 100644 index 000000000..80b399f00 --- /dev/null +++ b/content/docs/smallrye-graphql/latest/typesafe-client-multiple-queries.adoc @@ -0,0 +1,48 @@ += Multiple +include::_attributes.adoc[] + +Say you need the result from several root queries, e.g. all +`superHeroes` and all `superVillains`. Java only supports a single +return value, so you’ll need a wrapper class: + +[source,java] +---- +@GraphQLClientApi +interface SuperHeroesApi { + HeroesAndVillains heroesAndVillains(); +} + +@Multiple +class HeroesAndVillains { + List superHeroes; + List superVillains; +} +---- + +The `@Multiple` annotation 'inlines' the wrapper class, i.e. the actual +query is: + +[source,graphql] +---- +query heroesAndVillains { superHeroes {...} superVillains {...}} +---- + +The actual response below will be mapped to an instance of the +`HeroesAndVillains` wrapper class: + +[source,json] +---- +{ + "data": { + "superHeroes": [ ... ], + "superVillains": [ ... ] + } +} +---- + +If the nested queries require parameters, use `@` annotations to put +them on the field (remember: GraphQL fields can have parameters). + +If you need the same request several times (e.g. with different query +parameters), use `@Name` annotations, so the actual field names are used +as xref:#_name_mapping_aliases[alias]. diff --git a/content/docs/smallrye-graphql/latest/typesafe-client-reactive-types.adoc b/content/docs/smallrye-graphql/latest/typesafe-client-reactive-types.adoc new file mode 100644 index 000000000..a1c7b98c1 --- /dev/null +++ b/content/docs/smallrye-graphql/latest/typesafe-client-reactive-types.adoc @@ -0,0 +1,48 @@ += Reactive types usage in typesafe clients +include::_attributes.adoc[] + +== Reactive types with queries and mutations + +Regardless of the type that a query or mutation returns, you can always wrap the return type in a `Uni` in your +`@GraphQLClientApi` interface. The `Uni` will be completed after the response is received and processed. + +== Subscriptions + +The return type of a subscription operation must always be wrapped in a `Multi`. +The communication with the server runs over a WebSocket. + +=== Important note about errors + +When using the typesafe client for subscriptions, if it is expected that the server might return errors, +then it is highly recommended to wrap the return types into an `ErrorOr` +(for example, turn the API method `@Subscription Multi++<++Person++>++ people` into `@Subscription Multi++<++ErrorOr++<++Person++>>++ people`). +The reason is once there is an error returned from the service and the result can't be turned into a domain object due to that, +the `Multi` will receive a failure as a result. Because of the design of Mutiny, a `Multi` can't continue producing any items after +a failure. That means the subscription will be cancelled after the first error, even though the server might continue +sending more items. In such case, your application would have to detect the error and start a new subscription. +See xref:typesafe-client-error-handling.adoc[Error handling] for more details on how to use `ErrorOr`. + +=== Subscription example + +Given a server-side definition like this: + +[source,java] +---- +@Subscription +public Multi countToFive() { + return Multi.createFrom().range(0, 5); +} +---- + +A client to consume this subscription can look like this: + +[source,java] +---- +client.countToFive + .subscribe() + .with( // onItem: + i -> System.out.println("Received number" + i), + // onFailure: + (t) -> t.printStackTrace() + ); +---- diff --git a/content/docs/smallrye-graphql/latest/typesafe-client-unions-and-interfaces.adoc b/content/docs/smallrye-graphql/latest/typesafe-client-unions-and-interfaces.adoc new file mode 100644 index 000000000..36bf138b8 --- /dev/null +++ b/content/docs/smallrye-graphql/latest/typesafe-client-unions-and-interfaces.adoc @@ -0,0 +1,164 @@ += Unions & Interfaces +include::_attributes.adoc[] + +If you consume a GraphQL service with a schema containing either a `Union` or an `Interface`, +the client needs to request different fields via fragments, depending on the requested sub-type, +and deserialize a polymorphic type. + +This is quite complex on the GraphQL side, but the typesafe client makes it very easy to use. + +Unions + +One common use-case for Unions is returning a business error, e.g. when searching for a super hero, +either you find a super hero and maybe you want the name and other fields, +or you don't find it, and maybe you want a message with the reason. +Such a query could look like this: + +[source,graphql] +---- +query find($name: String) { + find(name: $name){ + __typename + ... on SuperHero {name} + ... on SuperHeroNotFound {message} + } +} +---- + +When it finds your super hero, the service could respond with this: + +[source,json] +---- +{ + "data": { + "find": { + "__typename": "SuperHero", + "name": "Spider-Man" + } + } +} +---- + +Or if it doesn't find it, the response could look like this: + +[source,json] +---- +{ + "data": { + "find": { + "__typename":"SuperHeroNotFound", + "message":"There is no hero named Foo" + } + } +} +---- + +On the Java side, this maps quite nicely to an interface, e.g. `SuperHeroResponse` +implemented by the two possible classes returned, e.g. `SuperHero` and `SuperHeroNotFound`; +you only need to provide the typesafe client with this information about these sub-classes by using the +https://jakarta.ee/specifications/jsonb/3.0/jakarta-jsonb-spec-3.0#polymorphic-types[`JSON-B` Polymorphic Types] +annotations `@JsonbTypeInfo` and `@JsonbSubtype`: + +[source,java] +---- + @Union + @JsonbTypeInfo(key = "__typename", value = { + @JsonbSubtype(alias = "SuperHero", type = SuperHero.class), + @JsonbSubtype(alias = "NotFound", type = NotFound.class) + }) + public interface SuperHeroResponse { + } + + public static class SuperHero implements SuperHeroResponse { + String name; + + String getName() { + return name; + } + } + + @Type("SuperHeroNotFound") + public static class NotFound implements SuperHeroResponse { + String message; + + String getMessage() { + return message; + } + } + + @GraphQLClientApi + interface UnionApi { + SuperHeroResponse find(String name); + } +---- + +Note that we rename the `NotFound` class into the GraphQL type `SuperHeroNotFound` to demo that use-case. + +The `@Union` annotation is actually not used and only helps to document what's happening behind the scenes. + +Interfaces + +If a service returns an GraphQL `interface`, that's visible only in the schema; +queries and responses work exactly like for a `union`. +So, interfaces are very similar to unions; but they can share common fields between the sub-types. + +E.g. a query returning either a `MainCharacter` or a `SideKick`, could look like this: + +[source,graphql] +---- +query find($name: String) { + find(name: $name){ + __typename + ... on MainCharacter {name superPower} + ... on SideKick {name mainCharacter} + } +} +---- + +The Java side looks very similar the Union example above: + +[source,java] +---- +@JsonbTypeInfo(key = "__typename", value = { + @JsonbSubtype(alias = "MainCharacter", type = MainCharacter.class), + @JsonbSubtype(alias = "SideKick", type = SideKick.class) +}) +public interface SearchResult { + String getName(); +} + +public static class MainCharacter implements SearchResult { + String name; + String superPower; + + @Override + public String getName() { + return name; + } + + public String getSuperPower() { + return superPower; + } +} + +public static class SideKick implements SearchResult { + String name; + String mainCharacter; + + @Override + public String getName() { + return name; + } + + public String getMainCharacter() { + return mainCharacter; + } +} + +@GraphQLClientApi +interface InterfaceApi { + SearchResult find(String name); + + List all(); +} +---- diff --git a/content/docs/smallrye-graphql/latest/typesafe-client-usage.adoc b/content/docs/smallrye-graphql/latest/typesafe-client-usage.adoc new file mode 100644 index 000000000..4dabe814b --- /dev/null +++ b/content/docs/smallrye-graphql/latest/typesafe-client-usage.adoc @@ -0,0 +1,301 @@ += Java code-first type-safe GraphQL Client API +include::_attributes.adoc[] + +A Java code-first type-safe GraphQL Client API suggestion for +https://github.com/eclipse/microprofile-graphql/issues/185[Microprofile GraphQL Issue +#185]. + +== Basic Usage + +Creating the client-side counterpart of the GraphQL API: + +[source,java] +---- +include::{examples-dir}/examples/typesafeclient/SuperHeroesApi.java[] +---- + +A model class: + +[source,java] +---- +include::{examples-dir}/examples/typesafeclient/SuperHero.java[] +---- + +Injecting the client using CDI and using it: + +[source,java] +---- +include::{examples-dir}/examples/typesafeclient/MyClientUsage.java[] +---- + +* The default request type is `query`. To make it a mutation, annotate +it `@Mutation`. The parameter name is only available if you compile +the source with the `-parameters` option. Otherwise, you’ll have to +annotate all parameters with `@Name`. + +The example above uses CDI, e.g. when you are in a MicroProfile or +Jakarta EE environment. If you are in an environment without CDI +support, you need to instantiate the API interface by using the builder: + +[source,java] +---- +SuperHeroesApi api = TypesafeGraphQLClientBuilder.newBuilder().build(SuperHeroesApi.class); +---- + +The basic idea of the Java code-first approach is to start by +writing the DTOs and query/mutation methods as you need them in your +client. This ensures that you don’t request fields that you don’t need; +the thinking is inspired by https://martinfowler.com/articles/consumerDrivenContracts.html[Consumer Driven +Contracts]. + +If the server uses names different from yours, you can use annotations to do a mapping: + +=== Name Mapping / Aliases + +If the server defines a different field or parameter name, annotate it +with `@Name`. If the server defines a different query name, annotate the +method as, e.g., `@Query("findHeroesCurrentlyLocatedIn")`. + +By renaming methods, you can also define several variations of the same +request but using different return types or parameters. E.g.: + +[source,java] +---- +public interface SuperHeroesApi { + SuperHero findHeroByName(String name); + + @Query("findHeroByName") + SuperHeroWithTeams findHeroWithTeamsByName(String name); +} +---- + +* The `SuperHero` class has no team affiliations (for this example). +* The `SuperHeroWithTeams` class has a `List++<++Team++>++ teamAffiliations` +field. The actual query name is still `findHeroByName`. The `Team` +class doesn’t contain the members to break recursion. + +If you rename a field or method, the real field or method name will be +used as an alias, so you can select the same data twice (see `and` +below). + +=== Configuration + +If the endpoint is always the same, e.g. a public API of a cloud +service, you can add the URL to your API annotation, e.g.: + +[source,java] +---- +@GraphQLClientApi(endpoint = "https://superheroes.org/graphql") +interface SuperHeroesApi { +} +---- + +When instantiating the API with the builder, you can set (or overwrite) +the endpoint there: + +[source,java] +---- +SuperHeroesApi api = TypesafeGraphQLClientBuilder.newBuilder() + .endpoint("https://superheroes.org/graphql") + .build(SuperHeroesApi.class); +---- + +Commonly you’ll need different endpoints, e.g. when you need one +endpoint for your production system, but a different endpoint for your +test system. Simply use https://download.eclipse.org/microprofile/microprofile-config-1.4/microprofile-config-spec.html[MicroProfile +Config] +to set the endpoint; similar to the https://download.eclipse.org/microprofile/microprofile-rest-client-1.4.1/microprofile-rest-client-1.4.1.html[MicroProfile Rest +Client], +the key for the endpoint is the fully qualified name of the API +interface, plus `/mp-graphql/url`, e.g.: + +[source,properties] +---- +org.superheroes.SuperHeroesApi/mp-graphql/url=https://superheroes.org/graphql +---- + +If you want to use a different key, set the base config key on the +annotation `@GraphQLClientApi(configKey = "superheroes")`; then use this +key for the endpoint `superheroes/mp-graphql/url`. + +When using the builder, you can override the config key as well: +`TypesafeGraphQLClientBuilder.newBuilder().configKey("superheroes")`. + +=== NestedParameter + +Some APIs require parameters beyond the root level, e.g. for filtering +or paginating nested lists. Say you have a schema like this: + +[source,graphql] +---- +type Query { + team(name: String!): Team! +} + +type Team { + members(first: Int!): [SuperHero!]! +} +---- + +To pass the parameter to the nested field/method, annotate it as +`@NestedParameter`, e.g.: + +[source,java] +---- +@GraphQLClientApi +interface TeamsApi { + Team team(String name, @NestedParameter("members") int first); +} +---- + +The value of the `@NestedParameter` annotation is the dot-delimited path +to the nested field/method that the value should be added to. + +Example of server code + +[source,java] +---- +@GraphQLApi +public class RoleApi { + @Query + public List findAllRolesByUserId(@NonNull UUID userId) { + // return roles + } + + public List permission(@Source Roles role, @DefaultValue("5") int limit) { + // return permissions, based on roles + } + + public List permissionType(@Source Permission permission, @DefaultValue("5") int limit) { + // return permissionType, based on permission + } +} +---- + +Query looks like + +.... +query { + findAllRolesByUserId(userId: ...) { + id + permission(limit: 2) { + id + permissionType(limit: 3) { + id + } + } + } +} +.... + +On the client side, you can declare the following code: + +[source,java] +---- +public record PermissionType(Long id) { +} + +public record Permission(Long id, List permissionType) { +} + +public record Role(UUID id, List permission) { +} + +@GraphQLClientApi +public interface ApiClient { + List findAllRolesByUserId( + UUID userId, + @NestedParameter("permission") @Name("limit") int permissionLimit, + @NestedParameter("permission.permissionType") @Name("limit") int permissionTypeLimit + ); +} +---- + +== Namespaces + +There are several ways to work with namespaces in a type-safe client: +1. Using `@Namespace` +2. Using `@Name` (deprecated) + +____ +++[++NOTE++]++ You can only use one of the annotations - `@Name` or `@Namespace` on a `GraphClientQLApi` interface. +____ + +=== Using @Namespace annotation + +The `@Namespace` annotation accepts an array of strings specifying the nesting levels of the namespace. +This flexible approach allows you to create namespaces with any desired level of nesting, combining different levels as needed. + +If the remote GraphQL API has the following schema: + +.... +"Query root" +type Query { + admin: AdminQuery +} + +type AdminQuery { + users: AdminUsersQuery +} + +type AdminUsersQuery { + findAll: User +} + +type User { + id: BigInteger + [...] +} +.... + +While declaring the following interface: + +[source,java] +---- +@Namespace({"admin", "users"}) +@GraphQLClientApi +public interface UsersClient { + List findAll(); +} +---- + +Its outcome will be the following GraphQL query: + +.... +query AminUsersFindAll { + admin { + users { + findAll { + id + } + } + } +} +.... + +=== Using @Name (deprecated) + +____ +++[++NOTE++]++ This feature may be removed in the future. +____ + +The `@Name` annotation functions similarly to `@Namespace`, but it is limited to a single nesting level. + +.... +query { + users { + findAll { + ... + } + } +} +.... + +[source,java] +---- +@Name("users") +@GraphQLClientApi +public interface ApiClient { + List findAll(); +} +---- diff --git a/content/docs/smallrye-graphql/latest/using-smallrye-stork.adoc b/content/docs/smallrye-graphql/latest/using-smallrye-stork.adoc new file mode 100644 index 000000000..46064219c --- /dev/null +++ b/content/docs/smallrye-graphql/latest/using-smallrye-stork.adoc @@ -0,0 +1,53 @@ += Using SmallRye Stork +include::_attributes.adoc[] + +SmallRye GraphQL client (both dynamic and typesafe) supports SmallRye Stork for +service discovery and load balancing. If your client is configured to connect to a URI +with the `stork://` or `storks://` scheme, then Stork will be used to determine the +actual endpoints to connect to. Load balancing, if configured, will be applied in a way that +each HTTP request can be routed to a different service instance. For WebSocket traffic, +once a WebSocket connection is established, the client will keep using that connection +for as long as possible. If that connection is lost for any reason and needs to be +reestablished, then the load balancer can route it to a different instance. + +== Using with Quarkus + +The dependency on Stork is declared as optional, so if you want to use it, you might need +to add it explicitly. If you're using Quarkus, use the Stork extension along with +the desired service discovery implementation, for example: + +.... + + io.quarkus + quarkus-smallrye-stork + + + io.smallrye.stork + smallrye-stork-service-discovery-static-list + +.... + +Example configuration in Quarkus `application.properties`: + +.... +quarkus.smallrye-graphql-client.myclient.url=stork://foo-service/graphql +quarkus.stork.foo-service.service-discovery.type=static +quarkus.stork.foo-service.service-discovery.address-list=server1.com:8080,server2.com:8080 +.... + +== When running as a standalone application + +If you're using the client in a standalone mode outside any container that handles +Stork initialization for you, you might need two additional steps to get it working: + +Add a configuration provider for Stork, such as + +.... + + io.smallrye.stork + stork-microprofile-config + +.... + +And before initializing any GraphQL client instances, make sure that Stork is initialized +by calling `io.smallrye.stork.Stork.initialize()` once. diff --git a/content/docs/smallrye-graphql/main/_attributes.adoc b/content/docs/smallrye-graphql/main/_attributes.adoc new file mode 100644 index 000000000..2e27c956b --- /dev/null +++ b/content/docs/smallrye-graphql/main/_attributes.adoc @@ -0,0 +1,20 @@ +:project-version: 3.0.0.Beta8 +// Attributes shared by every page. smallrye.io overrides the ones it needs when it publishes. +// Pages in subdirectories set :version-root: (../ per level) before including this file. +ifndef::version-root[:version-root:] +:idprefix: +:idseparator: - +:sectanchors: +:examples-dir: {version-root}_examples +:partialsdir: {version-root}_partials +:project-name: SmallRye GraphQL +:javadoc-version: latest +:generated-dir: {version-root}_generated + +// Added by smallrye.io (tools/docs/Docs.java sync) +:javadoc-version: 3.0.0.Beta8 +:relfileprefix: /docs/smallrye-graphql/main/ +:imagesdir: /docs/smallrye-graphql/main +:docs-project: smallrye-graphql +:docs-version: main +:docs-ref: main diff --git a/content/docs/smallrye-graphql/main/_examples/examples/dynamicclient/MyClientUsage.java b/content/docs/smallrye-graphql/main/_examples/examples/dynamicclient/MyClientUsage.java new file mode 100644 index 000000000..1647626af --- /dev/null +++ b/content/docs/smallrye-graphql/main/_examples/examples/dynamicclient/MyClientUsage.java @@ -0,0 +1,37 @@ +package examples.dynamicclient; + +import examples.typesafeclient.SuperHero; +import io.smallrye.graphql.client.GraphQLClient; +import io.smallrye.graphql.client.Response; +import io.smallrye.graphql.client.core.Document; +import io.smallrye.graphql.client.dynamic.api.DynamicGraphQLClient; + +import jakarta.inject.Inject; +import tools.jackson.databind.node.ArrayNode; +import java.util.List; +import java.util.concurrent.ExecutionException; + +import static io.smallrye.graphql.client.core.Argument.arg; +import static io.smallrye.graphql.client.core.Argument.args; +import static io.smallrye.graphql.client.core.Document.document; +import static io.smallrye.graphql.client.core.Field.field; +import static io.smallrye.graphql.client.core.Operation.operation; + +public class MyClientUsage { + + @Inject + @GraphQLClient("superHeroes") + DynamicGraphQLClient client; + + public void execute() throws ExecutionException, InterruptedException { + Document document = document( // <1> + operation(field("allHeroesIn", + args(arg("location", "Outer Space")), + field("name"), + field("superPowers")))); + Response response = client.executeSync(document); // <2> + + ArrayNode heroesArray = (ArrayNode) response.getData().get("allHeroesIn"); // <3> + List heroes = response.getList(SuperHero.class, "allHeroesIn"); // <4> + } +} diff --git a/content/docs/smallrye-graphql/main/_examples/examples/dynamicclient/MyClientUsageString.java b/content/docs/smallrye-graphql/main/_examples/examples/dynamicclient/MyClientUsageString.java new file mode 100644 index 000000000..c761d64b1 --- /dev/null +++ b/content/docs/smallrye-graphql/main/_examples/examples/dynamicclient/MyClientUsageString.java @@ -0,0 +1,39 @@ +package examples.dynamicclient; + +import io.smallrye.graphql.client.GraphQLClient; +import io.smallrye.graphql.client.Response; +import io.smallrye.graphql.client.dynamic.api.DynamicGraphQLClient; + +import jakarta.inject.Inject; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutionException; + +public class MyClientUsageString { + + @Inject + @GraphQLClient("superheroes") + DynamicGraphQLClient client; + + public void execute() throws ExecutionException, InterruptedException { + String queryWithInlineVariables = // <1> + "query {" + + " allHeroesIn(location: \"Outer Space\") {" + + " name" + + " superPowers" + + " }" + + "}"; + Response response = client.executeSync(queryWithInlineVariables); + + String queryWithExtractedVariables = // <2> + "query($loc: String) {" + + " allHeroesIn(location: $loc) {" + + " name" + + " superPowers" + + " }" + + "}"; + Map variables = new HashMap<>(); // <3> + variables.put("loc", "Outer Space"); + Response response2 = client.executeSync(queryWithExtractedVariables, variables); + } +} diff --git a/content/docs/smallrye-graphql/main/_examples/examples/typesafeclient/MyClientUsage.java b/content/docs/smallrye-graphql/main/_examples/examples/typesafeclient/MyClientUsage.java new file mode 100644 index 000000000..be521d196 --- /dev/null +++ b/content/docs/smallrye-graphql/main/_examples/examples/typesafeclient/MyClientUsage.java @@ -0,0 +1,16 @@ +package examples.typesafeclient; + +import jakarta.inject.Inject; +import java.util.List; + +public class MyClientUsage { + + @Inject + SuperHeroesApi superHeroesApi; + + public void execute() { + List allHeroes = superHeroesApi.allHeroesIn("Outer Space"); + // ... + } + +} diff --git a/content/docs/smallrye-graphql/main/_examples/examples/typesafeclient/SuperHero.java b/content/docs/smallrye-graphql/main/_examples/examples/typesafeclient/SuperHero.java new file mode 100644 index 000000000..7c19a1119 --- /dev/null +++ b/content/docs/smallrye-graphql/main/_examples/examples/typesafeclient/SuperHero.java @@ -0,0 +1,12 @@ +package examples.typesafeclient; + +import java.util.List; + +public class SuperHero { + + private String name; + private List superPowers; + + // plus getters and setters + +} diff --git a/content/docs/smallrye-graphql/main/_examples/examples/typesafeclient/SuperHeroesApi.java b/content/docs/smallrye-graphql/main/_examples/examples/typesafeclient/SuperHeroesApi.java new file mode 100644 index 000000000..0fae71966 --- /dev/null +++ b/content/docs/smallrye-graphql/main/_examples/examples/typesafeclient/SuperHeroesApi.java @@ -0,0 +1,12 @@ +package examples.typesafeclient; + +import io.smallrye.graphql.client.typesafe.api.GraphQLClientApi; + +import java.util.List; + +@GraphQLClientApi +public interface SuperHeroesApi { + + List allHeroesIn(String location); + +} diff --git a/content/docs/smallrye-graphql/main/_nav.yml b/content/docs/smallrye-graphql/main/_nav.yml new file mode 100644 index 000000000..bfa9b0569 --- /dev/null +++ b/content/docs/smallrye-graphql/main/_nav.yml @@ -0,0 +1,76 @@ +title: SmallRye GraphQL +nav: +- title: Overview + page: index +- title: Server side features + items: + - title: Customizing JSON (de)serializers + page: custom-json-serializers-deserializers + - title: Directives + page: directives + - title: Federation + page: federation + - title: Custom error extensions + page: custom-error-extensions + - title: Response extensions + page: extensions + - title: Returning Void from Mutations + page: mutation-void + - title: Handling of the WebSocket's init-payload + page: handling-init-payload-from-the-websocket + - title: Custom scalars + page: custom-scalar + - title: Inspecting executable directives + page: inspecting-executable-directives-on-server-side + - title: Namespaces + page: namespaces-on-server-side + - title: Target input fields + page: target-for-input-types + - title: Kotlin support + page: kotlin +- title: Typesafe client + items: + - title: Basic usage + page: typesafe-client-usage + - title: Unions and Interfaces + page: typesafe-client-unions-and-interfaces + - title: Reactive + page: typesafe-client-reactive-types + - title: Custom scalar types + page: typesafe-client-custom-scalars + - title: Adding headers + page: typesafe-client-headers + - title: Logging + page: typesafe-client-logging + - title: Running multiple queries at once + page: typesafe-client-multiple-queries + - title: Error handling + page: typesafe-client-error-handling + - title: Accessing metadata of responses + page: typesafe-client-accessing-metadata-of-responses + - title: Static Directives + page: typesafe-client-directives +- title: Dynamic client + items: + - title: Basic usage + page: dynamic-client-usage + - title: Error handling + page: dynamic-client-error-handling +- title: Clients in general + items: + - title: Bootstrapping the client + page: client-standalone + - title: Using SmallRye Stork + page: using-smallrye-stork +- title: Developer tools + items: + - title: Maven plugin + page: maven-plugin + - title: Gradle plugin + page: gradle-plugin +- title: Reference material + items: + - title: Server-side configuration + page: server-configuration + - title: Client-side configuration + page: client-configuration diff --git a/content/docs/smallrye-graphql/main/client-configuration.adoc b/content/docs/smallrye-graphql/main/client-configuration.adoc new file mode 100644 index 000000000..7da22fddd --- /dev/null +++ b/content/docs/smallrye-graphql/main/client-configuration.adoc @@ -0,0 +1,28 @@ += Client-side configuration properties +include::_attributes.adoc[] + +Note: if you are using Quarkus, it is recommended to use the `quarkus.++*++` property counterparts instead. +See https://quarkus.io/guides/all-config#quarkus-smallrye-graphql-client_quarkus-smallrye-graphql-client-smallrye-graphql-client[Quarkus Documentation] for more info. + +[width="100%",cols="18%,10%,72%",options="header",] +|=== +|Property |Default value |Meaning +|`CLIENT++_++NAME/mp-graphql/url` |none |Denotes URL to connect to +|`CLIENT++_++NAME/mp-graphql/header/KEY` |none |Adds a HTTP header named `KEY` to all HTTP requests performed by the client +|`CLIENT++_++NAME/mp-graphql/subprotocols` |`graphql-ws,graphql-transport-ws` |Comma-separated list of websocket subprotocols supported by this client. We currently support `graphql-ws` and `graphql-transport-ws`. If multiple subprotocols are provided, choosing the actual subprotocol will be subject to negotiation with the server. +|`CLIENT++_++NAME/mp-graphql/keystore` |none |Path to client's keystore (for example `file:/path/to/keystore` or `classpath:path/to/keystore`) +|`CLIENT++_++NAME/mp-graphql/keystoreType` |`JKS` |Keystore type +|`CLIENT++_++NAME/mp-graphql/keystorePassword` |none |Keystore password +|`CLIENT++_++NAME/mp-graphql/truststore` |none |Path to client's truststore (for example `file:/path/to/truststore` or `classpath:path/to/truststore`) +|`CLIENT++_++NAME/mp-graphql/truststoreType` |`JKS` |Truststore type +|`CLIENT++_++NAME/mp-graphql/truststorePassword` |none |Truststore password +|`CLIENT++_++NAME/mp-graphql/proxyHost` |none |Hostname of the proxy to use +|`CLIENT++_++NAME/mp-graphql/proxyPort` |none |Port of the proxy to use +|`CLIENT++_++NAME/mp-graphql/proxyUsername` |none |Username for the proxy to use +|`CLIENT++_++NAME/mp-graphql/proxyPassword` |none |Password for the proxy to use +|`CLIENT++_++NAME/mp-graphql/maxRedirects` |16 |Max number of redirects to follow. Set to 0 to disable redirects. +|`CLIENT++_++NAME/mp-graphql/websocketInitializationTimeout` |none |Maximum time in milliseconds that will be allowed to wait for the server to acknowledge a websocket connection. +|`CLIENT++_++NAME/mp-graphql/runSingleOperationsOverWebsocket` |`false` |If true, then queries and mutations will run over the websocket transport rather than pure HTTP. Off by default, because it has higher overhead. +|`CLIENT++_++NAME/mp-graphql/initPayload/KEY` |none |Adds a property named `KEY` to the `connection++_++init` message payload when negotiating a websocket connection. All values will be treated as string. For other types instatiate the API with the builder. +|`CLIENT++_++NAME/mp-graphql/allowUnexpectedResponseFields` |`false` |If true and there is an unexpected field in the response payload, it won't throw an error, and it will log a warning message. +|=== diff --git a/content/docs/smallrye-graphql/main/client-standalone.adoc b/content/docs/smallrye-graphql/main/client-standalone.adoc new file mode 100644 index 000000000..e991a36b2 --- /dev/null +++ b/content/docs/smallrye-graphql/main/client-standalone.adoc @@ -0,0 +1,76 @@ += Bootstraping GraphQL clients +include::_attributes.adoc[] + +In general, it is recommended to use SmallRye GraphQL client through the +https://quarkus.io/guides/smallrye-graphql-client[Quarkus extension], +or the https://github.com/wildfly-extras/wildfly-graphql-feature-pack/wiki/GraphQL-client-guide[WildFly feature pack], +each of them having their own documentation about how to set up dependencies and bootstrap a client. +It is however also possible to use SmallRye GraphQL Client in a standalone application outside any container. +The single dependency that you need is `io.smallrye:smallrye-graphql-client-implementation-vertx`, as the +Vert.x based implementation is currently the only supported one. + +If you're using the client as a script and notice that the JVM doesn't exit even after you close the client instance, +it's probably because the client is maintaining a `Vertx` instance that it created automatically. To solve this +issue, you might need to create a `Vertx` instance on your own, pass it to the client builder, and then close it after +closing the client. See the following JBang snippet for an example. + +== Logging with a standalone (Java SE) application using the SmallRye GraphQL Client + +If you want to properly configure logging in a standalone application, you +will also need to add a logging implementation to the classpath. The client +code uses the JBoss Logging facade, so you can, for example, use the JBoss +Logmanager. Add the `org.jboss.logmanager:jboss-logmanager` dependency to +your project(it needs to be on the runtime classpath). You will also need to +run the application with this system property declared: +`-Djava.util.logging.manager=org.jboss.logmanager.LogManager`. + +To customize logging levels etc, it picks up the `logging.properties` file +from the root of the runtime classpath, so, for example if using Maven and +the exec plugin to start the application, you would place the file into +`src/main/resources/logging.properties`. For an example of the contents of +this file, see +https://github.com/jamezp/jboss-logging-example/blob/master/src/main/resources/logging.properties. + +== Using with JBang + +This is a full script runnable directly with https://www.jbang.dev/[JBang] that +uses a dynamic client for connecting to https://countries.trevorblades.com[countries.trevorblades.com] +to obtain a list of countries from its database. + +[source,java] +---- +///usr/bin/env jbang "$0" "$@" ; exit $? +//DEPS io.smallrye:smallrye-graphql-client-implementation-vertx:RELEASE + +import io.smallrye.graphql.client.dynamic.api.DynamicGraphQLClient; +import io.smallrye.graphql.client.Response; +import io.smallrye.graphql.client.vertx.dynamic.VertxDynamicGraphQLClientBuilder; + +import io.vertx.core.Vertx; + +// Has a multiline string literal, requires Java 15+! +class Client { + public static void main(String... args) throws Exception { + Vertx vertx = Vertx.vertx(); + DynamicGraphQLClient client = new VertxDynamicGraphQLClientBuilder() + .url("https://countries.trevorblades.com") + .vertx(vertx) + .build(); + try { + Response response = client.executeSync(""" + query { + countries { + name + } + } + """); + System.out.println(response); + } finally { + client.close(); + vertx.close(); + } + } +} +---- + +Save this file as `Client.java` and execute with `jbang Client.java`. diff --git a/content/docs/smallrye-graphql/main/custom-error-extensions.adoc b/content/docs/smallrye-graphql/main/custom-error-extensions.adoc new file mode 100644 index 000000000..82aa86e5b --- /dev/null +++ b/content/docs/smallrye-graphql/main/custom-error-extensions.adoc @@ -0,0 +1,44 @@ += GraphQL Error Extensions +include::_attributes.adoc[] + +Exceptions are reported in GraphQL in the `errors` array, next to the `data` field, so it's possible to return partial results, e.g.: + +[source,json] +---- +{ + "data": { + "superHero": { + "name": "Wolverine", + "location": null + } + }, + "errors": [{ + "message":"location unknown", + "path": ["superHero","location"], + "extensions":{"code":"location-unknown"} + }] +} +---- + +The `location` field couldn't be returned, and in the `errors`, there's the reason with a few predefined fields, +and a map of `extensions` that can contain custom details about the error. + +You can add your own extensions by implementing the `io.smallrye.graphql.api.ErrorExtensionProvider` interface and +adding your class name to a file `META-INF/services/io.smallrye.graphql.api.ErrorExtensionProvider` (this is a `ServiceLoader`). + +As an example, this class provides an extension named `exception-length` with the length of the simple class name of the exception: + +[source,java] +---- +public class ExceptionLengthErrorExtensionProvider implements ErrorExtensionProvider { + @Override + public String getKey() { + return "exception-length"; + } + + @Override + public JsonNumber mapValueFrom(Throwable exception) { + return Json.createValue(exception.getClass().getSimpleName().length()); + } +} +---- diff --git a/content/docs/smallrye-graphql/main/custom-json-serializers-deserializers.adoc b/content/docs/smallrye-graphql/main/custom-json-serializers-deserializers.adoc new file mode 100644 index 000000000..70d87f6b6 --- /dev/null +++ b/content/docs/smallrye-graphql/main/custom-json-serializers-deserializers.adoc @@ -0,0 +1,36 @@ += Customizing JSON-B serializers/deserializers +include::_attributes.adoc[] + +If your application needs finer-grained control over JSON serialization/deserialization than you can achieve via +formatting annotations like `@JsonbDateFormat`, you may plug in your own custom instances of the `Jsonb` class +for each input type that your GraphQL application exposes. + +To do this, you'll need to create an implementation of `io.smallrye.graphql.spi.EventingService` +and implement its `overrideJsonbConfig` method. +An example that plugs in a custom date format for a particular class that is used as input: + +.... +package org.acme.custom.json.config; + +public class CustomJsonbService implements EventingService { + + @Override + public String getConfigKey() { + return null; // activate this service always regardless of the configuration + } + + @Override + public Map overrideJsonbConfig() { + JsonbConfig config = new JsonbConfig().withDateFormat("MM dd yyyy HH:mm Z", null).withDeserializers(OBJECTID_DESERIALIZER).withSerializers( + OBJECTID_SERIALIZER); + return Collections.singletonMap("MyModelClass.class.getName()", JsonbBuilder.create(config)); + } +} +.... + +We can customize the JsonbConfig as per our need in above example we have registered additional serializer and date formatting for the class MyModelClass. + +As the discovery of eventing services uses the ServiceLoader mechanism, don't forget to add a +`META-INF/services/io.smallrye.graphql.spi.EventingService` file that contains the fully qualified +name of your implementation. +Example `org.acme.custom.json.config.CustomJsonbService` diff --git a/content/docs/smallrye-graphql/main/custom-scalar.adoc b/content/docs/smallrye-graphql/main/custom-scalar.adoc new file mode 100644 index 000000000..27be60b11 --- /dev/null +++ b/content/docs/smallrye-graphql/main/custom-scalar.adoc @@ -0,0 +1,36 @@ += Creating GraphQL Custom Scalars with SmallRye GraphQL +include::_attributes.adoc[] + +While by default *MicroProfile GraphQL* specification doesn't provide direct support for creating +custom scalars for GraphQL literal types of String, Int and Float, *SmallRye GraphQL* has implemented +this feature based on user feedback. To implement a *custom scalar* with *SmallRye GraphQL*, +you can annotate your custom scalar class with `@CustomScalar` in addition to following the pattern below +of including a public constructor that takes one of `String`++|++`BigDecimal`++|++`BigInteger` depending on +the scalar type: + +[source,java] +---- + @CustomScalar("BigDecimalString") + public class BigDecimalString implements CustomStringScalar { + public BigDecimalString(String stringValue) { + ... + } + @Override + public String stringValueForSerialization() { + ... + } +} +---- + +In this example, `BigDecimalString` implements the `CustomStringScalar` which is used to identify the +proper (de) serialization for BigDecimalString. `BigDecimalString` also provides a single argument +constructor which takes a String. Finally, `BigDecimalString` implements +`stringValueForSerialization()` which provides the String representation to be used during +serialization. + +____ +++[++NOTE++]++ +If the user wants to create a literal for GraphQL Int or Float, they would implement either +CustomIntScalar with the intValueForSerialization method or CustomFloatScalar with the +floatValueForSerialization method respectively. +____ diff --git a/content/docs/smallrye-graphql/main/directives.adoc b/content/docs/smallrye-graphql/main/directives.adoc new file mode 100644 index 000000000..34b420ca8 --- /dev/null +++ b/content/docs/smallrye-graphql/main/directives.adoc @@ -0,0 +1,45 @@ += Directives +include::_attributes.adoc[] + +== Custom Directives + +You can add your own https://spec.graphql.org/draft/#sec-Language.Directives[GraphQL Directives] by writing +a corresponding Java Annotation and annotate it as `@Directive`, e.g.: + +[source,java] +---- +@Directive(on = { OBJECT, INTERFACE }) +@Description("Just a test") +@Retention(RUNTIME) +public @interface MyDirective { +} +---- + +Directives can be repeatable, see the `@Key` annotation for an example. + +== Directives generated from Bean Validation annotations + +If your project uses Bean Validation to validate fields on input types and operation arguments, and you enable +inclusion of directives in the schema (by setting `smallrye.graphql.schema.includeDirectives=true`), +then constraints decoded from annotations will be added to your schema as directives. This is currently only +supported for some built-in constraints (annotations from the +`javax.validation.constraints` package), and custom constraints aren't supported at all. + +Each bean validation annotation is mapped to a single `@constraint` directive. The directive is declared as repeatable, +so if you have multiple constraints on an input field, the field will contain multiple `@constraint` directives. +The following table describes the mapping between BV annotations and `@constraint` directives (all currently supported +BV annotations are listed here): + +[cols=",",options="header",] +|=== +|BV annotation |GraphQL directive +|`@Size(MIN, MAX)` |`@constraint(minLength=MIN, maxLength=MAX)` +|`@Email` |`@constraint(format='email')` +|`@Max` |`@constraint(max=VALUE)` +|`@Min` |`@constraint(min=VALUE)` +|`@Pattern(REGEXP)` |`@constraint(pattern=REGEXP)` +|=== + +Note: The `@NotNull` annotation does not map to a directive, instead it makes the GraphQL type non-nullable. + +Constraints will only appear on fields of input types and operation arguments. diff --git a/content/docs/smallrye-graphql/main/dynamic-client-error-handling.adoc b/content/docs/smallrye-graphql/main/dynamic-client-error-handling.adoc new file mode 100644 index 000000000..e7a9b5f79 --- /dev/null +++ b/content/docs/smallrye-graphql/main/dynamic-client-error-handling.adoc @@ -0,0 +1,34 @@ += Error handling with dynamic clients +include::_attributes.adoc[] + +== System-level errors + +If the service returns a system-level error (which means the response body doesn't contain +a valid GraphQL response document), the client invocation will (instead of returning a +`io.smallrye.graphql.client.Response`) throw a `io.smallrye.graphql.client.InvalidResponseException` +whose message contains information about the received status code, status message, and body +contents. + +== Application-level errors + +If the service returns one or more application-level errors (which means that +there is valid GraphQL response in the body that has a non-empty `errors` entry), the +client invocation will return a `io.smallrye.graphql.client.Response` as normal, and that object +will contain information about the errors. Errors are represented as `io.smallrye.graphql.client.GraphQLError` +objects and can be inspected after retrieving a list of all errors using `response.getErrors()`. + +____ +++[++NOTE++]++ +An error response is considered application-level regardless of the HTTP status code as long as it +contains a valid GraphQL response. We define a GraphQL response as a well-formed JSON document +that contains at least one of `errors`, `data` and `extensions` entries, and no other entries beyond +that. +____ + +It is also possible to convert a response's errors into a `io.smallrye.graphql.client.GraphQLClientException` +by calling `throwExceptionIfErrors()` - the response has to be cast to `ResponseImpl` for this as of now. +This method will, if there are any errors in the response, convert the errors into a `GraphQLClientException` +and throw it. + +If you need to access the HTTP response code and message pertaining to a response that contained +application-level errors, refer to xref:dynamic-client-usage.adoc#accessing-http-headers-and-response-codes[Accessing HTTP headers and response codes]. diff --git a/content/docs/smallrye-graphql/main/dynamic-client-usage.adoc b/content/docs/smallrye-graphql/main/dynamic-client-usage.adoc new file mode 100644 index 000000000..26832e7d1 --- /dev/null +++ b/content/docs/smallrye-graphql/main/dynamic-client-usage.adoc @@ -0,0 +1,136 @@ += Dynamic client introduction and basic usage +include::_attributes.adoc[] + +A Java GraphQL client. The main difference from the typesafe client is +that while the typesafe client behaves like a typesafe proxy very +similar to the MicroProfile REST Client, the dynamic client is more like +the JAX-RS client from the `jakarta.ws.rs.client` package. Instead of +working with model classes directly, the dynamic client focuses on +programmatically working with GraphQL documents representing GraphQL +requests and responses. It still offers the option to convert between +documents and model classes when necessary. + +In the current implementation, Vert.x HTTP client is used for handling +the underlying traffic. + +Creating a client instance + +Generally there are two ways to obtain a client instance. + +First, using CDI injection where the configuration values are defined in system +properties: + +[source,java] +---- +@Inject +@GraphQLClient("superheroes") +DynamicGraphQLClient client; + +// assuming that this system property exists: +// superheroes/mp-graphql/url=https://superheroes.org/graphql +---- + +The above example assumes that configuration for the client is present in system properties. For a full list of +supported configuration properties, see xref:client-configuration.adoc[Client configuration reference] + +The other way to build a client is programmatically using a builder: + +[source,java] +---- +DynamicGraphQLClient client = DynamicGraphQLClientBuilder.newBuilder() + .url("https://superheroes.org/graphql") + .build(); +---- + +The usage examples in the following sections will assume using the first approach - injection. + +Basic Usage + +Given the following GraphQL service on the server side: + +[source,java] +---- +@GraphQLApi +class SuperHeroesApi { + @Query + List allHeroesIn(String location) { + // .... + } +} + +class SuperHero { + private String name; + private List superPowers; +} +---- + +Such service can be queried this way: + +[source,java] +---- +include::{examples-dir}/examples/dynamicclient/MyClientUsage.java[] +---- + +* ++<++1++>++ Creating the document representing the request. We used static +imports to make the code easy to read, they all come from the +classes in the `io.smallrye.graphql.client.core` package. +* ++<++2++>++ Executing the request. You can either do that in a blocking way, or +request a `Uni` if you prefer the reactive style. +* ++<++3++>++ Obtaining the resulting list of heroes as a `JsonArray`. +* ++<++4++>++ Obtaining the resulting list of heroes as instances of the model +class. This is optional, you can continue working with the data as a `JsonArray` if you prefer. + +Using plain strings instead of the DSL + +If you don't like the DSL for some reason and want to use plain strings for your queries, these two examples +will serve you: + +[source,java] +---- +include::{examples-dir}/examples/dynamicclient/MyClientUsageString.java[] +---- + +* ++<++1++>++: In this variant, we inline values for query arguments directly into the query string. +* ++<++2++>++: In this variant, argument values are extracted into variables. The `location` argument of the `allHeroesIn` query is +declared to be using the variable `loc` (the variable can also be named `location` same as the argument, if +you prefer). +* ++<++3++>++: Here we create a map that defines the values for each variable. Values of this map are `Object`s, +so you can put in strings, numbers, booleans, or any object that corresponds to a GraphQL type and can be +serialized to JSON. Inserting a `JsonObject` directly is also supported. + +Accessing HTTP headers and response codes + +To access HTTP transport metadata that was passed by the server, you can inspect the `Response` object. + +`Response.getTransportMeta("HEADER-NAME")` returns a `List++<++String++>++` containing (potentially multiple) values of the requested header. + +To get the HTTP status code or status message, these are stored inside the `transportMeta` map too. `ResponseImpl` +contains convenience methods to retrieve them: `ResponseImpl.getStatusCode()` and `ResponseImpl.getStatusMessage()`. +It's also possible to retrieve them directly without casting to `ResponseImpl` by calling +`Integer.valueOf(response.getTransportMeta().get("++<++status-code++>++").get(0))` and + +`response.getTransportMeta().get("++<++status-message++>++").get(0)`. + +HTTP headers, status codes and messages are only available for operations executed over pure HTTP, not via websockets! + +Sending extensions to the server + +The GraphQL over HTTP specification allows clients to send an `extensions` map alongside the query. To do this +with the dynamic client, build a `Request` using the `RequestImpl.builder` and pass it to `executeSync` or +`executeAsync`: + +[source,java] +---- +Map extensions = Map.of("myKey", "myValue", "number", 42); + +Request request = RequestImpl.builder("{ myQuery }") + .variables(variables) // optional + .operationName("MyOperation") // optional + .extensions(extensions) + .build(); + +Response response = client.executeSync(request); +---- + +On the server side, the extensions sent by the client can be read from `SmallRyeContext.getExtensionsFromClient()`. +This is separate from `SmallRyeContext.getAddedExtensions()`, which is used for adding extensions to the +outgoing response. diff --git a/content/docs/smallrye-graphql/main/extensions.adoc b/content/docs/smallrye-graphql/main/extensions.adoc new file mode 100644 index 000000000..bad6a5991 --- /dev/null +++ b/content/docs/smallrye-graphql/main/extensions.adoc @@ -0,0 +1,86 @@ += Extensions in SmallRye GraphQL +include::_attributes.adoc[] + +`SmallRyeContext` provides two separate extension mechanisms: + +* *Outbound extensions* (`addedExtensions`) — extensions added by the server and sent back to the client in the response. +* *Inbound extensions* (`extensionsFromClient`) — extensions received from the client as part of the incoming request. + +== Adding extensions to the response + +*SmallRye GraphQL* allows the user to add their own extensions inside the GraphQL response +which is received from the server. The `extensions` are stored next to the `data` field in the GraphQL response. + +[source,json] +---- +{ + "data": { + "shirt": { + "size": "L" + } + }, + "extensions": { + "country": "Germany", + "pi": 3.1415926535 + } +} +---- + +To add extensions, you need to `@Inject` an instance of `SmallRyeContext` in your `@GraphQLApi` annotated class. +After that, you can add your own extensions via the method `addExtension` +with its input parameters: `key : String` as an identification of the added extension, +and `value: Object` as a value of the extension. Value can be any given object that can be converted into JsonValue. + +As an example, this class below contains the query `getShirt` and during the http request, the query adds these extensions: +`++{++"country": "Germany", "pi": 3.1415926535}`. +These extensions will be sent back via response in the `extensions` field. + +[source,java] +---- +import jakarta.inject.Inject; + +@GraphQLApi +public class ShirtResources { + @Inject + SmallRyeContext smallRyeContext; + + @Query + public Shirt getShirt() { + smallRyeContext.addExtension("country", "Germany"); + smallRyeContext.addExtension("pi", 3.1415926535); + //... + } +} +---- + +____ +++[++NOTE++]++ +You can also use `getAddedExtensions()` to access the underlying map directly. +____ + +== Reading extensions from the client request + +Clients can send an `extensions` map alongside the query (as defined by the GraphQL over HTTP specification). +On the server side, these can be read via `SmallRyeContext.getExtensionsFromClient()`, which returns a +`Map++<++String, Object++>++` (or `null` if the client did not send any extensions). + +[source,java] +---- +import jakarta.inject.Inject; + +@GraphQLApi +public class MyApi { + @Inject + SmallRyeContext smallRyeContext; + + @Query + public String process() { + Map clientExtensions = smallRyeContext.getExtensionsFromClient(); + if (clientExtensions != null) { + String traceId = (String) clientExtensions.get("traceId"); + // ... + } + //... + } +} +---- diff --git a/content/docs/smallrye-graphql/main/federation.adoc b/content/docs/smallrye-graphql/main/federation.adoc new file mode 100644 index 000000000..827f544eb --- /dev/null +++ b/content/docs/smallrye-graphql/main/federation.adoc @@ -0,0 +1,213 @@ += Federation +include::_attributes.adoc[] + +Support for https://www.apollographql.com/docs/federation[GraphQL Federation] is enabled by default. If you add one of the federation annotations, the corresponding directives will be declared in your schema, and the additional Federation queries will be added automatically. You can also disable Federation completely by setting the `smallrye.graphql.federation.enabled` config key to `false`. + +You can add the Federation directives by using the equivalent Java annotation, e.g. to extend a `Product` entity with a `price` field, you can write a class: + +[source,java] +---- +package org.example.price; + +import org.eclipse.microprofile.graphql.Id; + +import io.smallrye.graphql.api.federation.Extends; +import io.smallrye.graphql.api.federation.Key; + +@Extends @Key(fields = @FieldSet("id")) +public class Product { + @Id + private String id; + + @Description("The price in cent") + private Integer price; + + // getters and setters omitted +} +---- + +And a normal query method that takes the `key` fields as a parameter and returns the requested type: + +[source,java] +---- +package org.example.price; + +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Id; +import org.eclipse.microprofile.graphql.Query; + +@GraphQLApi +public class Prices { + @Query + public Product product(@Id String id) { + return [...] + } +} +---- + +The GraphQL Schema then contains: + +[source,graphql] +---- +type Product @extends @key(fields : "id") { + id: ID + price: Int +} + +union _Entity = Product + +type Query { + _entities(representations: [_Any!]!): [_Entity]! + _service: _Service! + product(id: ID): Product +} +---- + +If you can resolve, e.g., the product with different types of ids, you can add multiple `@Key` annotations. +## Federation Batch Resolver + +For better performance, there is the option to use batch resolvers with federation. This is not activated by default. It can be activated by setting `smallrye.graphql.federation.batchResolving.enabled` config key to `true`. + +It is unnecessary to provide batch resolvers for all entities, if there is no batch resolver, a non-batch resolver is used. + +[source,java] +---- +package org.example.price; + +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Id; +import org.eclipse.microprofile.graphql.Query; + +@GraphQLApi +public class Prices { + @Query + public List product(@Id List id) { + return [...] + } +} +---- + +It is crucial that the sequence of the argument list matches the order of the result list. Currently, the name of the Argument `id` must match with the property name in the type. + +== Federation Reference Resolver + +In federation, you may also want to extend the external type by some fields without publishing queries into the schema. You can do it by using the annotation `@Resolver`. + +[source,java] +---- +@Extends +@Key(fields = @FieldSet("upc")) +public final class Product { + @External + @NonNull + private String upc; + @External + private Integer weight; + @External + private Integer price; + private Boolean inStock; + @Requires(fields = @FieldSet("price weight")) + private Integer shippingPrice; +} + +@GraphQLApi +public class Api { + @Query // 0 query, that will be added into schema + public Product findByUPC(String upc) { + return new Product(upc , ...etc); + } + + @Resolver // 1 You dont receive external fields price weight here, just key + public Product resolveByUPC(String upc) { + return new Product(upc , ...etc); + } + + @Resolver // 2 The order of variables doesn't matter + public Product resolveByUPCForShipping(int price, String upc, @Name("weight") int someWeight) { + return new Product(upc , someWeight, price, (price * someWeight) /*calculate shippingPrice */, ...etc); + } + + @Resolver // 3 + public Product resolveByUPCForSource(int price, String upc) { + return new Product(upc, price, ...etc); + } + + @Requires(fields = @FieldSet("price")) + public int anotherWeight(@Source Product product) { + return product.price() * 2; + } +} +---- + +Generates the following schema: + +.... +type Product @extends @key(fields : "upc") { + anotherWeight: Int! @requires(fields : "price") + inStock: Boolean + price: Int @external + shippingPrice: Int @requires(fields : "price weight") + upc: String! @external + weight: Int @external +} + +type Query { + _entities(representations: [_Any!]!): [_Entity]! + _service: _Service! +} +.... + +These methods will only be available to the federation router, which may send requests like the following examples: + +.... +// request 1 +query { + _entities(representations: [{ + "__typename": "Product", + "upc": "1" // just id key + }]) { + __typename + ... on Product { + inStock + } + } +} + +// request 2 +query { + _entities(representations: [{ + "__typename": "Product", + "upc": "1", // id key + "price": 100, // shippingPrice requires this field + "weight": 100 // shippingPrice requires this field + }]) { + __typename + ... on Product { + inStock + shippingPrice + } + } +} + +// request 3 +query { + _entities(representations: [{ + "__typename": "Product", + "upc": "2", + "price": 1299 // anotherWeight requires this field + } + ]) { + __typename + ... on Product { + anotherWeight + } + } +} +.... + +Unfortunately, you must make separate methods with different `@External` parameters. + +It is not currently possible to combine them into one separate type. + +You also can use `@Query` (if you want to add queries into the schema) or `@Resolver` (requests 0 and 1). +And if it was requested, `++_++entities` - `@Resolvers` methods are checked first (they have higher priority). diff --git a/content/docs/smallrye-graphql/main/gradle-plugin.adoc b/content/docs/smallrye-graphql/main/gradle-plugin.adoc new file mode 100644 index 000000000..843bf33df --- /dev/null +++ b/content/docs/smallrye-graphql/main/gradle-plugin.adoc @@ -0,0 +1,84 @@ += SmallRye GraphQL Gradle plugin +include::_attributes.adoc[] + +This Gradle plugin allows you to generate the GraphQL Schema on build and save it as a text file. + +Add this to your build.gradle: + +.... +plugins { + id 'io.smallrye.graphql' version 'PLUGIN_VERSION' +} +.... + +The list of existing plugin versions (to substitute the +`PLUGIN++_++VERSION` placeholder) can be found +https://plugins.gradle.org/plugin/io.smallrye.graphql[here]. + +The schema will appear as `build/generated/schema.graphql` by default. + +The plugin’s `generateSchema` task can customized like: + +.... +generateSchema { + destination = null + includeTransitiveDependencies = true + includeScalars = true +} +.... + +This will include the project’s dependencies (including the transitive +dependencies) to scan for GraphQL model classes. As the destination is +set to `null` the generated schema will only be output to the console +and saved to a file. + +In case external dependencies need to be scanned for GraphQL model +classes a more advanced configuration could look like: + +.... +configrations { + graphQLSchema +} + +dependencies { + graphQLSchema .. +} + +generateSchema { + includeDependencies = true + configurations = ["graphQLSchema"] +} +.... + +This defines a separate `graphQLSchema` configuration. In the +dependencies block the external dependencies containing the GraphQL mode +class should then be registered for the `graphQLSchema` configuration. +The `generateSchema` task is then customized to only include +dependencies from the `graphQLSchema` configuration. Also note that we +set `includeDependencies` to `true` (and not +`includeTransitiveDependencies`). This will make sure only direct +dependencies will be be scanned and not possible transitive +dependencies. + +Configuration options + +* `destination` - To override the default +`build/generated/schema.graphql` destination. +* `includeDependencies` - Scan project’s dependencies for GraphQL +model classes too. Default false. +* `includeTransitiveDependencies` - Scan project’s dependencies +(including the transitive dependencies) for GraphQL model classes +too. Default false. +* `configurations` - If the above `includeDependencies` or +`includeTransitiveDependencies` is true, you can control what +configurations should be included. Default is `implementation`. +* `dependencyExtensions` - If the above `includeDependencies` or +`includeTransitiveDependencies` is true, you can control what +dependency extensions should be included. Default is `jar`. +* `includeScalars` - Include scalars in the schema. Default false. +* `includeDirectives` - Include directives in the schema. Default +false. +* `includeSchemaDefinition` - Include the schema definition. Default +false. +* `includeIntrospectionTypes` - Include the introspection types in the +schema. Default false. diff --git a/content/docs/smallrye-graphql/main/handling-init-payload-from-the-websocket.adoc b/content/docs/smallrye-graphql/main/handling-init-payload-from-the-websocket.adoc new file mode 100644 index 000000000..a691d6802 --- /dev/null +++ b/content/docs/smallrye-graphql/main/handling-init-payload-from-the-websocket.adoc @@ -0,0 +1,25 @@ += Handling of the WebSocket's init-payload +include::_attributes.adoc[] + +When executing GraphQL operations over WebSockets (both with the `graphql-ws` and `graphql-transport-ws` protocols), +the client can send an initial payload +to the server when establishing the WebSocket connection. This payload can contain +any information that the server might need to process GraphQL requests, like client agent identification (similar to the `User-Agent` header) or data specific to the client. On the server-side, you can access this payload +via the `GraphQLContext`, which is available through the `DataFetchingEnvironment` object. + +[source,java] +---- +@GraphQLApi +public class HeroResources { + + @Inject + SmallryeContext context; + + // ... + Map initPayload = smallRyeContext + .getDataFetchingEnvironment() + .getGraphQlContext() + .get("init-payload"); + // ... +} +---- diff --git a/content/docs/smallrye-graphql/main/images/redhat_reversed.svg b/content/docs/smallrye-graphql/main/images/redhat_reversed.svg new file mode 100644 index 000000000..37fe83c48 --- /dev/null +++ b/content/docs/smallrye-graphql/main/images/redhat_reversed.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/content/docs/smallrye-graphql/main/index.adoc b/content/docs/smallrye-graphql/main/index.adoc new file mode 100644 index 000000000..f277286e0 --- /dev/null +++ b/content/docs/smallrye-graphql/main/index.adoc @@ -0,0 +1,6 @@ += Overview +include::_attributes.adoc[] + +SmallRye GraphQL is a library that allows you to easily use GraphQL (both server-side endpoints +and client-side code) in your Java-based projects. It implements +the MicroProfile GraphQL specification. diff --git a/content/docs/smallrye-graphql/main/inspecting-executable-directives-on-server-side.adoc b/content/docs/smallrye-graphql/main/inspecting-executable-directives-on-server-side.adoc new file mode 100644 index 000000000..cd37d1852 --- /dev/null +++ b/content/docs/smallrye-graphql/main/inspecting-executable-directives-on-server-side.adoc @@ -0,0 +1,39 @@ += Inspecting executable directives on the server side +include::_attributes.adoc[] + +Let's say you have this _GraphQL query_ with an executable directive on a field _location_. + +.... +{ + hero(name: $nameValue) { + name + location @customDirective(arg: true) + } +} +.... + +To inspect your directive on the server side, you must inject a `SmallRyeContext` object and get the relevant information like this. + +[source,java] +---- + @Inject + SmallRyeContext context; + + @Query + public SuperHero getHero(String name) { + // ... + SelectionSet selectionSet = (SelectionSet) context.getDataFetchingEnvironment().getField().getChildren().get(0); + Optional locationNode = selectionSet.getChildren().stream() + .filter(node -> node instanceof Field && ((Field) node).getName().equals("location")).findAny(); // "location" is the field name where the directive is applied. + locationNode.ifPresent(node -> { + List directives = ((Field) node).getDirectives(); + System.out.println(directives); + }); + // the rest of the query... + } +---- + +____ +++[++NOTE++]++ +that `Field` refers to `graphql.language.Field`, not `io.smallrye.graphql.schema.model.Field` +____ diff --git a/content/docs/smallrye-graphql/main/kotlin.adoc b/content/docs/smallrye-graphql/main/kotlin.adoc new file mode 100644 index 000000000..b6ccf535d --- /dev/null +++ b/content/docs/smallrye-graphql/main/kotlin.adoc @@ -0,0 +1,4 @@ += SmallRye Graphql in Kotlin +include::_attributes.adoc[] + +When working with SmallRye GraphQL in Kotlin, it is very important to use the `-Xemit-jvm-type-annotations` flag for the Kotlin compiler. This flag is important to generate the necessary annotations for reflection to work properly. The flag is supported as of Kotlin 1.3.70. diff --git a/content/docs/smallrye-graphql/main/maven-plugin.adoc b/content/docs/smallrye-graphql/main/maven-plugin.adoc new file mode 100644 index 000000000..959ef8596 --- /dev/null +++ b/content/docs/smallrye-graphql/main/maven-plugin.adoc @@ -0,0 +1,48 @@ += SmallRye GraphQL Maven plugin +include::_attributes.adoc[] + +This Maven plugin allows you to generate the GraphQL Schema on build and save it as a text file. + +Add this to your pom.xml: + +[source,xml] +---- + + smallrye-graphql-maven-plugin + io.smallrye + + + + generate-schema + + + + +---- + +The schema will appear as `target/generated/schema.graphql` by default. The goal is bound to the `process-classes` +build phase. + +Configuration options + +* `destination` - To override the default `target/generated/schema.graphql` destination +* `includeDependencies` - Scan project’s dependencies for GraphQL +model classes too. This is necessary if your GraphQL endpoint exposes +classes that are not in the application's main module. Off (`false`) by default, because +scanning a potentially large dependency tree slows down execution substantially. +Usage of reactive types does not require turning this option on. +* `includeDependenciesScopes` - If the above `includeDependencies` is +true, you can control what scopes should be included. Default is +`compile, system` +* `includeDependenciesTypes` - If the above `includeDependencies` is +true, you can control what types should be included. Default is +`jar` +* `includeScalars` - Include scalars in the schema. Default false. +* `includeDirectives` - Include directives in the schema. Default false. +* `includeSchemaDefinition` - Include the schema definition. Default false. +* `includeIntrospectionTypes` - Include the introspection types in the schema. Default false. +* `federationEnabled` - Enable GraphQL Federation. This is automatically +enabled if any Federation-related annotations are found in your application, otherwise the default is false. +You should generally use this in conjunction with `includeDirectives`. +* `typeAutoNameStrategy` - Strategy for transforming class names into GraphQL type names. +Valid values are `MergeInnerClass`, `Full` and`Default`. diff --git a/content/docs/smallrye-graphql/main/mutation-void.adoc b/content/docs/smallrye-graphql/main/mutation-void.adoc new file mode 100644 index 000000000..4fc047e25 --- /dev/null +++ b/content/docs/smallrye-graphql/main/mutation-void.adoc @@ -0,0 +1,24 @@ += Returning Void Type in Mutations with SmallRye GraphQL +include::_attributes.adoc[] + +While by default *MicroProfile GraphQL* specification doesn't provide direct support for returning `void` type from mutation operations, *SmallRye GraphQL* has implemented this feature based on user feedback. + +To implement a *mutation* operation that returns `void` or `Void` type, you can use the following pattern in your annotated `@GraphQLApi` class: + +[source,java] +---- + @Mutation + public void createHero(SuperHero hero) { + superHeroService.create(hero); + } +---- + +In this example, `createHero()` receives a SuperHero object as input, +performs some operation with it (in this case, creating it using a +`superHeroService`). + +____ +++[++NOTE++]++ +On the GraphQL level, this is achieved by returning a special scalar type named Void from the mutation. +This scalar type will always have a value of null. +____ diff --git a/content/docs/smallrye-graphql/main/namespaces-on-server-side.adoc b/content/docs/smallrye-graphql/main/namespaces-on-server-side.adoc new file mode 100644 index 000000000..896ad6537 --- /dev/null +++ b/content/docs/smallrye-graphql/main/namespaces-on-server-side.adoc @@ -0,0 +1,167 @@ += Namespacing on the server side +include::_attributes.adoc[] + +____ +++[++NOTE++]++ +Using approaches to form namespaces in the schema can be useful for large APIs. There are several ways to do this. +However, read the documentation carefully, especially the limitations and possible problems. +____ + +____ +++[++NOTE++]++ You can only use one of the annotations - `@Name` or `@Namespace` over the GraphQLApi classes. +____ + +== Using @Namespace annotation + +The annotation accepts an array of strings containing the nesting of the namespace. +This method allows you to create any nesting of namespaces. +You can use any nesting and also combine different levels. + +[source,java] +---- +@GraphQLApi +@Namespace({"admin", "users"}) +@Description("Admin users operations") +public class AdminUsersApi { + @Query + public List findAll() { + // + } +} + +@GraphQLApi +@Namespace({"admin"}) +@Description("Admin operations") +public class AdminApi { + @Query + public List findAll() { + // + } +} +---- + +Will generate schema + +.... +"Query root" +type Query { + admin: AdminQuery +} + +"Admin operations" +type AdminQuery { + users: AdminUsersQuery + findAll: [Admin] +} + +"Admin users operations" +type AdminUsersQuery { + findAll: [User] +} + +type Admin { + id: BigInteger + ... +} + +type User { + id: BigInteger + ... +} +.... + +And you can send GraphQL request like: + +.... +query { + admin { + users { + findAll { + id + } + } + } +} +.... + +== Using @Name annotation (deprecated) + +____ +++[++NOTE++]++ This feature may be removed in the future. +____ + +It does the same thing as `@Namespace`, the only difference is that there can only be one nesting level. + +[source,java] +---- +@GraphQLApi +@Name("users") +@Description("Users operations") +public class UserApi { + @Query + public List findAll() { + // + } +} +---- + +As a result, you can get methods with the same names. + +.... +query { + users { + findAll { + .... + } + } +} +.... + +== Problems + +While dividing APIs into namespaces may seem convenient, several issues are important to be aware of. + +=== Mutations + +On the client side, be careful when working with mutations. +This violates the GraphQL specification since mutations in this form can be executed in parallel. +Read more here about namespaces https://www.apollographql.com/docs/technotes/TN0012-namespacing-by-separation-of-concern/[Namespacing by Separation of Concerns]. +This article describes how to work with namespaces, what problems you may encounter, and how to solve them. + +What does GraphQL say about this - https://benjie.dev/graphql/nested-mutations["GraphQL" Nested Mutations] + +In summary, you can use nested mutations, but with some overhead on the client side. Be careful with mutations. + +==== Subscriptions + +GraphQL does not guarantee subscription resolution within namespaces. +While you can define subscriptions in namespaces, the generated schema will be valid, but the subscription will not be resolved. +For example, if you try to run such a subscription request, you will get an error. This is the behavior of `graphql-java`. + +[source,java] +---- +@GraphQLApi +@Namepace("resource") +public class ResourceApi { + @Subscription + public Multi resourceChange() { + // + } +} +---- + +.... +subscription { + resource { + resourceChange { + .... + } + } +} +.... + +There is currently no way around this problem. +You must move subscriptions into a separate `@GraphQLApi` class that does not declare any namespace. +++>++ ++[++NOTE++]++ +++>++ Be very careful when designing API with namespace. +++>++ And be aware of all the potential consequences when working with mutations and subscriptions. diff --git a/content/docs/smallrye-graphql/main/server-configuration.adoc b/content/docs/smallrye-graphql/main/server-configuration.adoc new file mode 100644 index 000000000..054c01128 --- /dev/null +++ b/content/docs/smallrye-graphql/main/server-configuration.adoc @@ -0,0 +1,38 @@ += Configuration properties +include::_attributes.adoc[] + +These are properties understood directly by SmallRye GraphQL. If you're using SmallRye GraphQL through +Quarkus, these will generally work, but Quarkus offers its own counterparts for most of these, +and it is recommended to use the `quarkus.++*++` properties. Refer to the Quarkus documentation: +https://quarkus.io/guides/all-config#quarkus-smallrye-graphql_quarkus-smallrye-graphql-smallrye-graphql[Server] and +https://quarkus.io/guides/all-config#quarkus-smallrye-graphql-client_quarkus-smallrye-graphql-client-smallrye-graphql-client[Client] +side Quarkus properties. + +== From MicroProfile GraphQL + +[width="100%",cols="33%,35%,32%",options="header",] +|=== +|Property |Default value |Meaning +|`mp.graphql.defaultErrorMessage` |`Server Error` |Error message for hidden exceptions +|`mp.graphql.hideErrorMessage` |Runtime exceptions |Exceptions that will be hidden +|`mp.graphql.showErrorMessage` |Checked exceptions |Exceptions that will not be hidden. +|=== + +== From SmallRye GraphQL + +[width="100%",cols="33%,35%,32%",options="header",] +|=== +|Property |Default value |Meaning +|`smallrye.graphql.printDataFetcherException` |`false` |Include the stacktrace of the data fetching exception in the log output +|`smallrye.graphql.allowGet` |`false` |Allow HTTP GET Method +|`smallrye.graphql.metrics.enabled` |`false` |Enable metrics +|`smallrye.graphql.tracing.enabled` |`false` |Enable tracing +|`smallrye.graphql.validation.enabled` |`true` if Bean Validation is present |Enable Bean Validation. This property is DEPRECATED, setting to `false` won't actually turn off validation. It will be removed in a future release. +|`smallrye.graphql.events.enabled` |`true` if one of metrics, tracing or bean validation is true |Enable eventing +|`smallrye.graphql.logPayload` |`false` |Log the payload in the log file +|`smallrye.graphql.fieldVisibility` | |To control the field visibility on introspection +|`smallrye.graphql.schema.includeScalars` |`true` |Include Scalar definitions in the schema +|`smallrye.graphql.schema.includeSchemaDefinition` |`false` |Include Schema definition +|`smallrye.graphql.schema.includeDirectives` |`false` |Include directives in the schema +|`smallrye.graphql.schema.includeIntrospectionTypes` |`false` |Include Introspection types in the schema +|=== diff --git a/content/docs/smallrye-graphql/main/target-for-input-types.adoc b/content/docs/smallrye-graphql/main/target-for-input-types.adoc new file mode 100644 index 000000000..0b990f0c1 --- /dev/null +++ b/content/docs/smallrye-graphql/main/target-for-input-types.adoc @@ -0,0 +1,72 @@ += Target fields for input types +include::_attributes.adoc[] + +The `@Source` annotation extends *output* types by adding externally-defined fields. Its +counterpart for *input* types is `@Target` (`io.smallrye.graphql.api.Target`). + +With `@Target`, you can add fields to a GraphQL input type the same way `@Source` adds fields to +an output type. The annotated parameter receives the input object, and a sibling parameter +receives the value sent by the client for that field. The method name becomes the input field +name in the schema (overridable with `@Name`), and `@Description` can be added on the method. + +Server-side example: + +[source,java] +---- +import io.smallrye.graphql.api.Target; + +@GraphQLApi +public class CharacterService { + + @Mutation + public Character save(Character input) { + // the input may have been modified by @Target methods below + return input; + } + + public void applyTwitterHandle(@Target Character character, String twitterHandle) { + character.setTwitterHandle(twitterHandle); + } +} +---- + +This generates an extended input type in the schema: + +[source,graphql] +---- +input CharacterInput { + # ... regular fields ... + twitterHandle: String # added by the applyTwitterHandle method +} +---- + +When a client sends a mutation that includes the `twitterHandle` field, the corresponding +`@Target` method is invoked at runtime. If the field is omitted, the method is not called. + +Multiple `@Target` methods on the same input type are supported: + +[source,java] +---- +public void applyTwitterHandle(@Target Character character, String twitterHandle) { + character.setTwitterHandle(twitterHandle); +} + +public void applyNumber(@Target Character character, Integer rating) { + character.setRating(rating); +} +---- + +A `Context` parameter can be injected into target methods: + +[source,java] +---- +public void applyWithContext(@Target Character character, String value, Context context) { + String fieldName = context.getFieldName(); + // ... +} +---- + +____ +++[++NOTE++]++ +`@Target` is currently marked as experimental. +____ diff --git a/content/docs/smallrye-graphql/main/typesafe-client-accessing-metadata-of-responses.adoc b/content/docs/smallrye-graphql/main/typesafe-client-accessing-metadata-of-responses.adoc new file mode 100644 index 000000000..cb9875c8d --- /dev/null +++ b/content/docs/smallrye-graphql/main/typesafe-client-accessing-metadata-of-responses.adoc @@ -0,0 +1,81 @@ += Accessing Metadata of GraphQL Responses with TypesafeResponse +include::_attributes.adoc[] + +The TypesafeResponse class provides a wrapper around the "data" +field of GraphQL responses, along with additional metadata such as errors, +extensions, and transport metadata. Here are the methods you can use to access each type of metadata: + +== Transport metadata + +use `getTransportMeta()` method to retrieve a map of transport metadata associated with the response. +++>++ ++[++NOTE++]++ +++>++ Note that the transport metadata referred to in the context of HTTP requests +++>++ and responses is commonly found in the headers of the communication. +++>++ Therefore, the getTransportMeta() method mentioned here can be used to +++>++ retrieve a map of HTTP headers. +### Extensions +use `getExtensions()` method to retrieve a JsonObject of response extensions. +The method returns a JSON object that represents the response extensions. + +=== Data + +use `get()` method to retrieve the data object. The method returns the data +object if it exists and there are no errors. If there are errors, +it throws a NoSuchElementException with the error message. + +=== Errors + +use `getErrors()` method to retrieve a list of GraphQL errors. +The method returns a list of errors if there are errors and no data object. +If there is a data object, it throws a NoSuchElementException with the error message. + +____ +++[++NOTE++]++ +Note that the get() method assumes that the response has a data object, +and will throw an exception if there are errors without a data object. +If you want to check for errors before retrieving the data object, +use the hasErrors() method, which returns true if there are errors and false otherwise. +____ + +You can use these methods to access and process the metadata of GraphQL +responses returned by the TypesafeResponse class. + +Let's say you want to get as a client extensions from the GraphQL response. +First of all you will need to modify your `GraphQLClientApi` interface to have +your return type class (in this case `SuperHero`) wrapped with +`TypesafeResponse` class. +I.e. your `GraphQLClientApi` interface could look like this: + +[source,java] +---- +import examples.typesafeclient.SuperHero; +import io.smallrye.graphql.client.typesafe.api.TypesafeResponse; + +@GraphQLClientApi +public interface SuperHeroApi { + // ... + TypesafeResponse findSuperHeroByName(String name); + // ... +} +---- + +____ +++[++NOTE++]++ +Note that you can have `TypesafeResponse++<++T++>++` only as a result (return) type of your query. +Unlike `ErrorOr` the class cannot wrap individual fields. +If that is the case `IllegalArgumentException` will be thrown. +____ + +And now to get `extensions` from the response your code will look like something like this: + +[source,java] +---- + @Inject + SuperHeroApi client; + + TypesafeResponse superheroResponse = client.findSuperHeroByName("Batman"); + // returns the "data" field + SuperHero superhero = superhero.get(); + // returns the "extensions" field + JsonObject extensions = superhero.getExtensions(); +---- diff --git a/content/docs/smallrye-graphql/main/typesafe-client-custom-scalars.adoc b/content/docs/smallrye-graphql/main/typesafe-client-custom-scalars.adoc new file mode 100644 index 000000000..c6fbc61ce --- /dev/null +++ b/content/docs/smallrye-graphql/main/typesafe-client-custom-scalars.adoc @@ -0,0 +1,38 @@ += Custom scalar types +include::_attributes.adoc[] + +If the remote service contains custom scalar types represented as strings in GraphQL documents, +a typesafe client can learn to understand these types through introducing a class that represents them. +The class has to contain logic necessary for serializing and deserializing from/to raw strings. + +A model class is considered to represent a scalar GraphQL type if it contains a static builder method +named `of`, `valueOf`, or `parse` whose sole parameter is a `java.lang.String` and it returns an +instance of the class itself. The name of the GraphQL type is assumed to be equal to the short name +of the class. + +For example, if the server supports a `URL` type (there is a `scalar URL` clause in the schema), +create a class that represents a value for `URL`: + +[source,java] +---- +public class URL { + + private String value; + + public URL(String value) { + this.value = value; + } + + public static URL valueOf(String value) { // <1> + return new URL(value); + } + + @Override + public String toString() { // <2> + return value; + } +} +---- + +* ++<++1++>++ The static `valueOf(String)` method will be used for deserializing a `URL` from a string. +* ++<++2++>++ The `toString()` will be used for serializing a `URL` into a string to be sent to the GraphQL service diff --git a/content/docs/smallrye-graphql/main/typesafe-client-directives.adoc b/content/docs/smallrye-graphql/main/typesafe-client-directives.adoc new file mode 100644 index 000000000..046e90feb --- /dev/null +++ b/content/docs/smallrye-graphql/main/typesafe-client-directives.adoc @@ -0,0 +1,53 @@ += Typesafe Client Static Directives +include::_attributes.adoc[] + +== Custom Static Directives + +The declaration of custom static directives for the typesafe client follows the same principles as the server-side xref:directives.adoc[directives]. With only exception of using the https://spec.graphql.org/draft/#ExecutableDirectiveLocation[Executable Directive Locations] instead of Type System Directive Locations. + +[source,java] +---- +@Directive(on = { FIELD, VARIABLE_DEFINITION }) +@Retention(RUNTIME) +public @interface MyDirective { + String value() default ""; +} +---- + +Applying this to your API should look like this: + +[source,java] +---- +import io.smallrye.graphql.client.typesafe.api.GraphQLClientApi; + +@GraphQLClientApi +public @interface MyClientApi { + + @Query + @MyDirective("top-level field") + MySuperHero getMySuperHero(@MyDirective("variable definition") String nameOfMySuperHero); + + class MySuperHero { + @MyDirective("field") + String name; + } +} +---- + +Your API is going to generate a query something like this: + +.... +query mySuperHero( + $nameOfMySuperHero: String @myDirective(value: "variable definition") +) { + mySuperHero(nameOfMySuperHero: $nameOfMySuperHero) + @myDirective(value: "top-level field") { + name @myDirective(value: "field") + } +} +.... + +____ +++[++NOTE++]++ +The current implementation supports only FIELD and VARIABLE++_++DEFINITION locations. +____ diff --git a/content/docs/smallrye-graphql/main/typesafe-client-error-handling.adoc b/content/docs/smallrye-graphql/main/typesafe-client-error-handling.adoc new file mode 100644 index 000000000..dd8464f37 --- /dev/null +++ b/content/docs/smallrye-graphql/main/typesafe-client-error-handling.adoc @@ -0,0 +1,60 @@ += Error handling in typesafe clients +include::_attributes.adoc[] + +== System-level errors + +If the service returns a system-level error (which means the response body doesn't contain +a valid GraphQL response document), the client invocation will throw a +`io.smallrye.graphql.client.InvalidResponseException` +whose message contains information about the received status code, status message, and body +contents. + +== Application-level errors + +If the service returns one or more application-level errors (which means that +there is valid GraphQL response in the body that has a non-empty `errors` entry), +the client normally throws a `GraphQLClientException` containing the details in a list of +`GraphQLClientError`. + +____ +++[++NOTE++]++ +An error response is considered application-level regardless of the HTTP status code as long as it +contains a valid GraphQL response. We define a GraphQL response as a well-formed JSON document +that contains at least one of `errors`, `data` and `extensions` entries, and no other entries beyond +that. +____ + +If the error is specific to a `location`, you can use an `ErrorOr` +wrapper on the target field; the client the maps the error to that +wrapper instead of throwing an exception. I.e. your `SuperHero` class +could look like this: + +[source,java] +---- +class SuperHero { + String name; + ErrorOr location; +} +---- + +If the service returns a response like this: + +[source,json] +---- +{ + "data": { + "superHero": { + "name": "Wolverine", + "location": null + } + }, + "errors": [{ + "message":"location unknown", + "path": ["superHero","location"], + "extensions":{"code":"location-unknown"} + }] +} +---- + +Then the `SuperHero#location` wrapper field will not contain a `value` +but only the error above. See the `ErrorOr` class for details. diff --git a/content/docs/smallrye-graphql/main/typesafe-client-headers.adoc b/content/docs/smallrye-graphql/main/typesafe-client-headers.adoc new file mode 100644 index 000000000..d3dea2f6c --- /dev/null +++ b/content/docs/smallrye-graphql/main/typesafe-client-headers.adoc @@ -0,0 +1,84 @@ += Adding headers +include::_attributes.adoc[] + +To add a custom header to the http requests sent out by the GraphQL +Client, annotate your method or the API interface as `@Header`, e.g.: + +[source,java] +---- +@GraphQLClientApi +interface SuperHeroesApi { + @Header(name = "S.H.I.E.L.D.-Clearance", constant = "TOP-SECRET") + List allHeroesIn(String location); +} +---- + +The value can be specified with a `constant` or the name of a `method` for dynamic values, e.g.: + +[source,java] +---- +@GraphQLClientApi +interface SuperHeroesApi { + @Header(name = "S.H.I.E.L.D.-Clearance", method = "establishShieldClearance") + List allHeroesIn(String location); + + static Clearance establishShieldClearance() { + return userIsInRole(MANAGER) ? TOP_SECRET : PUBLIC; + } +} +---- + +* This example uses an enum, but it can be any Object; the GraphQL client +calls `toString()` to convert it. + +The method must be `static` and accessible by the interface, i.e. in the +interface itself or in one of the classes it’s nested in; if it’s in a +different class, prefix it with the fully qualified class name and a dot +`"."`, e.g. +`@Header(name = "S.H.I.E.L.D.-Clearance", method = "org.superheroes.SecurityTools.establishShieldClearance")`. + +A third option is to pass the value of a header as an API method +parameter: + +[source,java] +---- +@GraphQLClientApi +interface SuperHeroesApi { + List allHeroesIn(String location, @Header(name = "S.H.I.E.L.D.-Clearance") Clearance clearance); +} +---- + +The `@Header` parameter will not be part of the GraphQL query. + +The `name` of the header is always static, but can optionally be derived from the name of the parameter or method, i.e. if it has a `@Name` annotation, that annotation value is used. If it's not annotated, the method name (eventually stripped off a leading `get`) or the parameter name (if it's enabled in the compiler settings) is converted from camel case to kebab case, i.e. a parameter `@Header String requestIdentifier` will result in a header named `Request-Identifier`. + +`@Header` annotations can also be defined via `@Stereotype`. + +When instantiating the API with the builder, you can set (or overwrite) +one or more headers there: + +[source,java] +---- +SuperHeroesApi api = TypesafeGraphQLClientBuilder.newBuilder() + .header("S.H.I.E.L.D.-Clearance", "TOP-SECRET") + .build(SuperHeroesApi.class); +---- + +Authorization headers + +To add an `Authorization` header, instead of using the generic `@Header` +annotation, you can also use the special `@AuthorizationHeader` +annotation. It produces a `BASIC` `Authorization` header by default or a +`BEARER` token. You can configure the credentials in MP Config with a +prefix plus `/mp-graphql/` and either `username` and `password` for +`BASIC` or `bearer` for `BEARER`. The config key defaults to the fully +qualified name of the `GraphQLClientApi` interface or its `configKey`. + +You can use a custom prefix by setting the `confPrefix`. The infix +`/mp-graphql/` is still applied, unless you end the `confPrefix` with +`++*++`, e.g. +`@AuthorizationHeader(confPrefix = "org.superheroes.security.basic.++*++` +will use `org.superheroes.security.basic.username`, while `++*++` will use +plain `username`. + +`@AuthorizationHeader` annotations can be defined via `@Stereotype`. diff --git a/content/docs/smallrye-graphql/main/typesafe-client-logging.adoc b/content/docs/smallrye-graphql/main/typesafe-client-logging.adoc new file mode 100644 index 000000000..dad8dffc9 --- /dev/null +++ b/content/docs/smallrye-graphql/main/typesafe-client-logging.adoc @@ -0,0 +1,7 @@ += Logging in typesafe clients +include::_attributes.adoc[] + +The Client implementation logs all GraphQL requests and responses at +level `INFO` with the interface API as the logger name. It also logs the +keys of all headers added at level `DEBUG`; not the values, as they may +be security sensitive. diff --git a/content/docs/smallrye-graphql/main/typesafe-client-multiple-queries.adoc b/content/docs/smallrye-graphql/main/typesafe-client-multiple-queries.adoc new file mode 100644 index 000000000..80b399f00 --- /dev/null +++ b/content/docs/smallrye-graphql/main/typesafe-client-multiple-queries.adoc @@ -0,0 +1,48 @@ += Multiple +include::_attributes.adoc[] + +Say you need the result from several root queries, e.g. all +`superHeroes` and all `superVillains`. Java only supports a single +return value, so you’ll need a wrapper class: + +[source,java] +---- +@GraphQLClientApi +interface SuperHeroesApi { + HeroesAndVillains heroesAndVillains(); +} + +@Multiple +class HeroesAndVillains { + List superHeroes; + List superVillains; +} +---- + +The `@Multiple` annotation 'inlines' the wrapper class, i.e. the actual +query is: + +[source,graphql] +---- +query heroesAndVillains { superHeroes {...} superVillains {...}} +---- + +The actual response below will be mapped to an instance of the +`HeroesAndVillains` wrapper class: + +[source,json] +---- +{ + "data": { + "superHeroes": [ ... ], + "superVillains": [ ... ] + } +} +---- + +If the nested queries require parameters, use `@` annotations to put +them on the field (remember: GraphQL fields can have parameters). + +If you need the same request several times (e.g. with different query +parameters), use `@Name` annotations, so the actual field names are used +as xref:#_name_mapping_aliases[alias]. diff --git a/content/docs/smallrye-graphql/main/typesafe-client-reactive-types.adoc b/content/docs/smallrye-graphql/main/typesafe-client-reactive-types.adoc new file mode 100644 index 000000000..a1c7b98c1 --- /dev/null +++ b/content/docs/smallrye-graphql/main/typesafe-client-reactive-types.adoc @@ -0,0 +1,48 @@ += Reactive types usage in typesafe clients +include::_attributes.adoc[] + +== Reactive types with queries and mutations + +Regardless of the type that a query or mutation returns, you can always wrap the return type in a `Uni` in your +`@GraphQLClientApi` interface. The `Uni` will be completed after the response is received and processed. + +== Subscriptions + +The return type of a subscription operation must always be wrapped in a `Multi`. +The communication with the server runs over a WebSocket. + +=== Important note about errors + +When using the typesafe client for subscriptions, if it is expected that the server might return errors, +then it is highly recommended to wrap the return types into an `ErrorOr` +(for example, turn the API method `@Subscription Multi++<++Person++>++ people` into `@Subscription Multi++<++ErrorOr++<++Person++>>++ people`). +The reason is once there is an error returned from the service and the result can't be turned into a domain object due to that, +the `Multi` will receive a failure as a result. Because of the design of Mutiny, a `Multi` can't continue producing any items after +a failure. That means the subscription will be cancelled after the first error, even though the server might continue +sending more items. In such case, your application would have to detect the error and start a new subscription. +See xref:typesafe-client-error-handling.adoc[Error handling] for more details on how to use `ErrorOr`. + +=== Subscription example + +Given a server-side definition like this: + +[source,java] +---- +@Subscription +public Multi countToFive() { + return Multi.createFrom().range(0, 5); +} +---- + +A client to consume this subscription can look like this: + +[source,java] +---- +client.countToFive + .subscribe() + .with( // onItem: + i -> System.out.println("Received number" + i), + // onFailure: + (t) -> t.printStackTrace() + ); +---- diff --git a/content/docs/smallrye-graphql/main/typesafe-client-unions-and-interfaces.adoc b/content/docs/smallrye-graphql/main/typesafe-client-unions-and-interfaces.adoc new file mode 100644 index 000000000..36bf138b8 --- /dev/null +++ b/content/docs/smallrye-graphql/main/typesafe-client-unions-and-interfaces.adoc @@ -0,0 +1,164 @@ += Unions & Interfaces +include::_attributes.adoc[] + +If you consume a GraphQL service with a schema containing either a `Union` or an `Interface`, +the client needs to request different fields via fragments, depending on the requested sub-type, +and deserialize a polymorphic type. + +This is quite complex on the GraphQL side, but the typesafe client makes it very easy to use. + +Unions + +One common use-case for Unions is returning a business error, e.g. when searching for a super hero, +either you find a super hero and maybe you want the name and other fields, +or you don't find it, and maybe you want a message with the reason. +Such a query could look like this: + +[source,graphql] +---- +query find($name: String) { + find(name: $name){ + __typename + ... on SuperHero {name} + ... on SuperHeroNotFound {message} + } +} +---- + +When it finds your super hero, the service could respond with this: + +[source,json] +---- +{ + "data": { + "find": { + "__typename": "SuperHero", + "name": "Spider-Man" + } + } +} +---- + +Or if it doesn't find it, the response could look like this: + +[source,json] +---- +{ + "data": { + "find": { + "__typename":"SuperHeroNotFound", + "message":"There is no hero named Foo" + } + } +} +---- + +On the Java side, this maps quite nicely to an interface, e.g. `SuperHeroResponse` +implemented by the two possible classes returned, e.g. `SuperHero` and `SuperHeroNotFound`; +you only need to provide the typesafe client with this information about these sub-classes by using the +https://jakarta.ee/specifications/jsonb/3.0/jakarta-jsonb-spec-3.0#polymorphic-types[`JSON-B` Polymorphic Types] +annotations `@JsonbTypeInfo` and `@JsonbSubtype`: + +[source,java] +---- + @Union + @JsonbTypeInfo(key = "__typename", value = { + @JsonbSubtype(alias = "SuperHero", type = SuperHero.class), + @JsonbSubtype(alias = "NotFound", type = NotFound.class) + }) + public interface SuperHeroResponse { + } + + public static class SuperHero implements SuperHeroResponse { + String name; + + String getName() { + return name; + } + } + + @Type("SuperHeroNotFound") + public static class NotFound implements SuperHeroResponse { + String message; + + String getMessage() { + return message; + } + } + + @GraphQLClientApi + interface UnionApi { + SuperHeroResponse find(String name); + } +---- + +Note that we rename the `NotFound` class into the GraphQL type `SuperHeroNotFound` to demo that use-case. + +The `@Union` annotation is actually not used and only helps to document what's happening behind the scenes. + +Interfaces + +If a service returns an GraphQL `interface`, that's visible only in the schema; +queries and responses work exactly like for a `union`. +So, interfaces are very similar to unions; but they can share common fields between the sub-types. + +E.g. a query returning either a `MainCharacter` or a `SideKick`, could look like this: + +[source,graphql] +---- +query find($name: String) { + find(name: $name){ + __typename + ... on MainCharacter {name superPower} + ... on SideKick {name mainCharacter} + } +} +---- + +The Java side looks very similar the Union example above: + +[source,java] +---- +@JsonbTypeInfo(key = "__typename", value = { + @JsonbSubtype(alias = "MainCharacter", type = MainCharacter.class), + @JsonbSubtype(alias = "SideKick", type = SideKick.class) +}) +public interface SearchResult { + String getName(); +} + +public static class MainCharacter implements SearchResult { + String name; + String superPower; + + @Override + public String getName() { + return name; + } + + public String getSuperPower() { + return superPower; + } +} + +public static class SideKick implements SearchResult { + String name; + String mainCharacter; + + @Override + public String getName() { + return name; + } + + public String getMainCharacter() { + return mainCharacter; + } +} + +@GraphQLClientApi +interface InterfaceApi { + SearchResult find(String name); + + List all(); +} +---- diff --git a/content/docs/smallrye-graphql/main/typesafe-client-usage.adoc b/content/docs/smallrye-graphql/main/typesafe-client-usage.adoc new file mode 100644 index 000000000..4dabe814b --- /dev/null +++ b/content/docs/smallrye-graphql/main/typesafe-client-usage.adoc @@ -0,0 +1,301 @@ += Java code-first type-safe GraphQL Client API +include::_attributes.adoc[] + +A Java code-first type-safe GraphQL Client API suggestion for +https://github.com/eclipse/microprofile-graphql/issues/185[Microprofile GraphQL Issue +#185]. + +== Basic Usage + +Creating the client-side counterpart of the GraphQL API: + +[source,java] +---- +include::{examples-dir}/examples/typesafeclient/SuperHeroesApi.java[] +---- + +A model class: + +[source,java] +---- +include::{examples-dir}/examples/typesafeclient/SuperHero.java[] +---- + +Injecting the client using CDI and using it: + +[source,java] +---- +include::{examples-dir}/examples/typesafeclient/MyClientUsage.java[] +---- + +* The default request type is `query`. To make it a mutation, annotate +it `@Mutation`. The parameter name is only available if you compile +the source with the `-parameters` option. Otherwise, you’ll have to +annotate all parameters with `@Name`. + +The example above uses CDI, e.g. when you are in a MicroProfile or +Jakarta EE environment. If you are in an environment without CDI +support, you need to instantiate the API interface by using the builder: + +[source,java] +---- +SuperHeroesApi api = TypesafeGraphQLClientBuilder.newBuilder().build(SuperHeroesApi.class); +---- + +The basic idea of the Java code-first approach is to start by +writing the DTOs and query/mutation methods as you need them in your +client. This ensures that you don’t request fields that you don’t need; +the thinking is inspired by https://martinfowler.com/articles/consumerDrivenContracts.html[Consumer Driven +Contracts]. + +If the server uses names different from yours, you can use annotations to do a mapping: + +=== Name Mapping / Aliases + +If the server defines a different field or parameter name, annotate it +with `@Name`. If the server defines a different query name, annotate the +method as, e.g., `@Query("findHeroesCurrentlyLocatedIn")`. + +By renaming methods, you can also define several variations of the same +request but using different return types or parameters. E.g.: + +[source,java] +---- +public interface SuperHeroesApi { + SuperHero findHeroByName(String name); + + @Query("findHeroByName") + SuperHeroWithTeams findHeroWithTeamsByName(String name); +} +---- + +* The `SuperHero` class has no team affiliations (for this example). +* The `SuperHeroWithTeams` class has a `List++<++Team++>++ teamAffiliations` +field. The actual query name is still `findHeroByName`. The `Team` +class doesn’t contain the members to break recursion. + +If you rename a field or method, the real field or method name will be +used as an alias, so you can select the same data twice (see `and` +below). + +=== Configuration + +If the endpoint is always the same, e.g. a public API of a cloud +service, you can add the URL to your API annotation, e.g.: + +[source,java] +---- +@GraphQLClientApi(endpoint = "https://superheroes.org/graphql") +interface SuperHeroesApi { +} +---- + +When instantiating the API with the builder, you can set (or overwrite) +the endpoint there: + +[source,java] +---- +SuperHeroesApi api = TypesafeGraphQLClientBuilder.newBuilder() + .endpoint("https://superheroes.org/graphql") + .build(SuperHeroesApi.class); +---- + +Commonly you’ll need different endpoints, e.g. when you need one +endpoint for your production system, but a different endpoint for your +test system. Simply use https://download.eclipse.org/microprofile/microprofile-config-1.4/microprofile-config-spec.html[MicroProfile +Config] +to set the endpoint; similar to the https://download.eclipse.org/microprofile/microprofile-rest-client-1.4.1/microprofile-rest-client-1.4.1.html[MicroProfile Rest +Client], +the key for the endpoint is the fully qualified name of the API +interface, plus `/mp-graphql/url`, e.g.: + +[source,properties] +---- +org.superheroes.SuperHeroesApi/mp-graphql/url=https://superheroes.org/graphql +---- + +If you want to use a different key, set the base config key on the +annotation `@GraphQLClientApi(configKey = "superheroes")`; then use this +key for the endpoint `superheroes/mp-graphql/url`. + +When using the builder, you can override the config key as well: +`TypesafeGraphQLClientBuilder.newBuilder().configKey("superheroes")`. + +=== NestedParameter + +Some APIs require parameters beyond the root level, e.g. for filtering +or paginating nested lists. Say you have a schema like this: + +[source,graphql] +---- +type Query { + team(name: String!): Team! +} + +type Team { + members(first: Int!): [SuperHero!]! +} +---- + +To pass the parameter to the nested field/method, annotate it as +`@NestedParameter`, e.g.: + +[source,java] +---- +@GraphQLClientApi +interface TeamsApi { + Team team(String name, @NestedParameter("members") int first); +} +---- + +The value of the `@NestedParameter` annotation is the dot-delimited path +to the nested field/method that the value should be added to. + +Example of server code + +[source,java] +---- +@GraphQLApi +public class RoleApi { + @Query + public List findAllRolesByUserId(@NonNull UUID userId) { + // return roles + } + + public List permission(@Source Roles role, @DefaultValue("5") int limit) { + // return permissions, based on roles + } + + public List permissionType(@Source Permission permission, @DefaultValue("5") int limit) { + // return permissionType, based on permission + } +} +---- + +Query looks like + +.... +query { + findAllRolesByUserId(userId: ...) { + id + permission(limit: 2) { + id + permissionType(limit: 3) { + id + } + } + } +} +.... + +On the client side, you can declare the following code: + +[source,java] +---- +public record PermissionType(Long id) { +} + +public record Permission(Long id, List permissionType) { +} + +public record Role(UUID id, List permission) { +} + +@GraphQLClientApi +public interface ApiClient { + List findAllRolesByUserId( + UUID userId, + @NestedParameter("permission") @Name("limit") int permissionLimit, + @NestedParameter("permission.permissionType") @Name("limit") int permissionTypeLimit + ); +} +---- + +== Namespaces + +There are several ways to work with namespaces in a type-safe client: +1. Using `@Namespace` +2. Using `@Name` (deprecated) + +____ +++[++NOTE++]++ You can only use one of the annotations - `@Name` or `@Namespace` on a `GraphClientQLApi` interface. +____ + +=== Using @Namespace annotation + +The `@Namespace` annotation accepts an array of strings specifying the nesting levels of the namespace. +This flexible approach allows you to create namespaces with any desired level of nesting, combining different levels as needed. + +If the remote GraphQL API has the following schema: + +.... +"Query root" +type Query { + admin: AdminQuery +} + +type AdminQuery { + users: AdminUsersQuery +} + +type AdminUsersQuery { + findAll: User +} + +type User { + id: BigInteger + [...] +} +.... + +While declaring the following interface: + +[source,java] +---- +@Namespace({"admin", "users"}) +@GraphQLClientApi +public interface UsersClient { + List findAll(); +} +---- + +Its outcome will be the following GraphQL query: + +.... +query AminUsersFindAll { + admin { + users { + findAll { + id + } + } + } +} +.... + +=== Using @Name (deprecated) + +____ +++[++NOTE++]++ This feature may be removed in the future. +____ + +The `@Name` annotation functions similarly to `@Namespace`, but it is limited to a single nesting level. + +.... +query { + users { + findAll { + ... + } + } +} +.... + +[source,java] +---- +@Name("users") +@GraphQLClientApi +public interface ApiClient { + List findAll(); +} +---- diff --git a/content/docs/smallrye-graphql/main/using-smallrye-stork.adoc b/content/docs/smallrye-graphql/main/using-smallrye-stork.adoc new file mode 100644 index 000000000..46064219c --- /dev/null +++ b/content/docs/smallrye-graphql/main/using-smallrye-stork.adoc @@ -0,0 +1,53 @@ += Using SmallRye Stork +include::_attributes.adoc[] + +SmallRye GraphQL client (both dynamic and typesafe) supports SmallRye Stork for +service discovery and load balancing. If your client is configured to connect to a URI +with the `stork://` or `storks://` scheme, then Stork will be used to determine the +actual endpoints to connect to. Load balancing, if configured, will be applied in a way that +each HTTP request can be routed to a different service instance. For WebSocket traffic, +once a WebSocket connection is established, the client will keep using that connection +for as long as possible. If that connection is lost for any reason and needs to be +reestablished, then the load balancer can route it to a different instance. + +== Using with Quarkus + +The dependency on Stork is declared as optional, so if you want to use it, you might need +to add it explicitly. If you're using Quarkus, use the Stork extension along with +the desired service discovery implementation, for example: + +.... + + io.quarkus + quarkus-smallrye-stork + + + io.smallrye.stork + smallrye-stork-service-discovery-static-list + +.... + +Example configuration in Quarkus `application.properties`: + +.... +quarkus.smallrye-graphql-client.myclient.url=stork://foo-service/graphql +quarkus.stork.foo-service.service-discovery.type=static +quarkus.stork.foo-service.service-discovery.address-list=server1.com:8080,server2.com:8080 +.... + +== When running as a standalone application + +If you're using the client in a standalone mode outside any container that handles +Stork initialization for you, you might need two additional steps to get it working: + +Add a configuration provider for Stork, such as + +.... + + io.smallrye.stork + stork-microprofile-config + +.... + +And before initializing any GraphQL client instances, make sure that Stork is initialized +by calling `io.smallrye.stork.Stork.initialize()` once. diff --git a/content/docs/smallrye-health/latest/_attributes.adoc b/content/docs/smallrye-health/latest/_attributes.adoc new file mode 100644 index 000000000..0894420be --- /dev/null +++ b/content/docs/smallrye-health/latest/_attributes.adoc @@ -0,0 +1,18 @@ +:project-version: 4.3.0 +// Attributes shared by every page. smallrye.io overrides the ones it needs when it publishes. +// Pages in subdirectories set :version-root: (../ per level) before including this file. +ifndef::version-root[:version-root:] +:idprefix: +:idseparator: - +:sectanchors: +:partialsdir: {version-root}_partials +:examples-dir: {version-root}_examples +:generated-dir: {version-root}_generated + +// Added by smallrye.io (tools/docs/Docs.java sync) +:javadoc-version: 4.3.0 +:relfileprefix: /docs/smallrye-health/latest/ +:imagesdir: /docs/smallrye-health/latest +:docs-project: smallrye-health +:docs-version: latest +:docs-ref: 4.3.0 diff --git a/content/docs/smallrye-health/latest/_nav.yml b/content/docs/smallrye-health/latest/_nav.yml new file mode 100644 index 000000000..2b75ac52c --- /dev/null +++ b/content/docs/smallrye-health/latest/_nav.yml @@ -0,0 +1,18 @@ +title: SmallRye Health +nav: +- title: Overview + page: index +- title: Features on top of the specification + items: + - title: Asynchronous health checks + page: async-checks + - title: Health Groups + page: health-groups + - title: Health Registry + page: health-registry + - title: Wellness + page: wellness + - title: Static access + page: static-access + - title: Custom Config properties + page: custom-config-properties diff --git a/content/docs/smallrye-health/latest/async-checks.adoc b/content/docs/smallrye-health/latest/async-checks.adoc new file mode 100644 index 000000000..76a05e243 --- /dev/null +++ b/content/docs/smallrye-health/latest/async-checks.adoc @@ -0,0 +1,44 @@ += Asynchronous health checks +include::_attributes.adoc[] + +== Defining asynchronous checks + +SmallRye Health provides a way to define asynchronous checks that can be consumed +in the reactive applications in a non-blocking manner. These checks are defined in +a very similar way to the usual health checks only they implement a different interface: + +[source,java] +---- +@Liveness <1> +@ApplicationScoped <2> +public class LivenessAsync implements AsyncHealthCheck { <3> + + @Override + public Uni call() { <4> + return Uni.createFrom().item(HealthCheckResponse.up("liveness-async")) + .onItem().delayIt().by(Duration.ofMillis(10)); + } +} +---- +<1> The MicroProfile Health CDI qualifier is still required +<2> The check still needs to be a CDI bean +<3> The asynchronous checks must implement the `AsyncHealthCheck` interface +<4> The return type of the health check `call()` method is `Uni` + +The `Uni` is one of the main types included in the https://smallrye.io/smallrye-mutiny/[Mutiny project] +on top of which asynchronous checks are built. Mutiny provides several ways to transform the +most common reactive types to `Uni`, so if you are used to work with different reacitve +library it should be an issue. + +== Consuming health checks asynchronously + +The `SmallRyeHealthReporter` class was extended to provide variants of get health methods +that end with `*Async()` (e.g. `getLivenessAsync()`) that return an `Uni` so it can +also be included in any reactive pipeline. Example uses of this API is demonstrated for instance in +the `AsyncHealthTest`. + +== References + +* https://github.com/smallrye/smallrye-health/blob/main/api/src/main/java/io/smallrye/health/api/AsyncHealthCheck.java[`AsyncHealthCheck`] +* https://github.com/smallrye/smallrye-health/blob/main/implementation/src/main/java/io/smallrye/health/SmallRyeHealthReporter.java[`SmallRyeHealthReporter`] +* https://github.com/smallrye/smallrye-health/blob/main/testsuite/experimental/src/test/java/io/smallrye/health/test/AsyncHealthTest.java[`AsyncHealthTest`] diff --git a/content/docs/smallrye-health/latest/custom-config-properties.adoc b/content/docs/smallrye-health/latest/custom-config-properties.adoc new file mode 100644 index 000000000..4df522378 --- /dev/null +++ b/content/docs/smallrye-health/latest/custom-config-properties.adoc @@ -0,0 +1,39 @@ += Custom configuration values +include::_attributes.adoc[] + +SmallRye Health provides several custom configuration values that are exposing additional features which are currently not a part of the MicroProfile Health specification. The following sections provide an overview of what users can configure to adjust the executions of health check invocations. + +== Disabling Health Checks with configuration + +In SmallRye Health, you can dynamically choose which `HealthCheck` implementation will be included in the final produces responses with configuration values. To disable any included `HealthCheck` implementation dynamically you can use the following configuration property: + +[source,properties] +---- +io.smallrye.health.check..enabled=false +---- + +where `health-check-classname` is a fully-qualified class name of the `HealthCheck` implementation that should not be included in the responses. This value can be specified dynamically on the application startup with MicroProfile Config values (see https://github.com/eclipse/microprofile-config[MicroProfile Config specification]). + +For instance, for `HealthCheck` implementation included in class `org.acme.TestHC` the property would look like `-Dio.smallrye.health.check.org.acme.TestHC.enabled=false` or `IO_SMALLRYE_HEALTH_CHECK_ORG_ACME_TESTHC_ENABLED=false`. + +== Adding additional JSON properties + +SmallRye Health provides a feature that allows users to specify additional properties with fixed values that should be included in the top level properties of the outputed JSON. To specify additonal JSON properties, you can use the following configuration values: + +[source,properties] +---- +io.smallrye.health.additional.property.testProperty1=testValue1 +io.smallrye.health.additional.property.testProperty2=testValue2 +---- + +which will output a JSON like this: + +[source,json] +---- +{ + "checks": [], + "status": "UP", + "testProperty1": "testValue1", + "testProperty2": "testValue2" +} +---- \ No newline at end of file diff --git a/content/docs/smallrye-health/latest/health-groups.adoc b/content/docs/smallrye-health/latest/health-groups.adoc new file mode 100644 index 000000000..a94eb6295 --- /dev/null +++ b/content/docs/smallrye-health/latest/health-groups.adoc @@ -0,0 +1,72 @@ += Health Groups +include::_attributes.adoc[] + +A health group is simply a user defined group of health checks. Namely, liveness and readiness +(and experimental wellness) are predefined health groups. With this feature users are allowed to +define any number of custom health groups and the individual checks that will belong to them. + +== Including a check in a health group + +To include a health check in a custom health group you can use the `HealthGroup` annotations: + +[source,java] +---- +@HealthGroup("group1") <1> +@HealthGroup("group2") <2> +@ApplicationScoped +public class SuccessfulCustom implements HealthCheck { + @Override + public HealthCheckResponse call() { + return HealthCheckResponse.up("successful-check"); + } +} +---- +<1> The check will belong to the health group _"group1"_ +<2> The check will also belong to the health group _"group2"_ + +A health check can be included in any number of health groups including the liveness and readiness: + +[source,java] +---- +@HealthGroup("custom1") +@HealthGroup("different-group") +@Liveness +@ApplicationScoped +public class GroupsCheck implements HealthCheck { ... +---- + +== Accessing the health groups + +The custom health groups are exposed under the `/health/group` resource tree: + +|=== +| Path | Description +| `/health/group` | All health checks in all groups +| `/health/group/\{group-name}` | All health checks in the group called \{group-name} +|=== + +Please note that is totally valid to define custom health group as a liveness and/or readiness +probe if required. Both liveness and readiness are shorcuts for predefined health groups. + +In the previous example, the `GroupsCheck` will be invoked when accessing: + +* `/health/group` +* `/health/group/custom1` +* `/health/group/different-group` +* `/health/live` + +However, it won't be invoked for instance when accessing: + +* `/health/group/foobar` +* `/health/ready` + +== Default health group + +The configuration property `io.smallrye.health.defaultHealthGroup=\{default-group-name}` can be used to specify the health +group name of a new, default health group that is implicitly added to the all +health checks that don't already belong into any health group. This might be useful to easily categorize the custom health groups. + +== References + +* https://github.com/smallrye/smallrye-health/blob/main/api/src/main/java/io/smallrye/health/api/HealthGroup.java[`HealthGroup`] + diff --git a/content/docs/smallrye-health/latest/health-registry.adoc b/content/docs/smallrye-health/latest/health-registry.adoc new file mode 100644 index 000000000..a5990eee5 --- /dev/null +++ b/content/docs/smallrye-health/latest/health-registry.adoc @@ -0,0 +1,65 @@ += Health Registry +include::_attributes.adoc[] + +A health registry provides a way to register the health checks programatically. It also allows +to remove the programatically registered health checks when they are no longer needed. This presents +an option to dynamically register and unregister health checks based on the user defined criteria. + +== Health Registry usage + +The `HealthRegistry` can be injected as a CDI bean with `@Liveness`, `@Readiness`, `@Startup`, or `@Wellness` qualifiers: + +[source,java] +---- +@Inject +@Liveness +HealthRegistry livenessHealthRegistry; + +@Inject +@Readiness +HealthRegistry readinessHealthRegistry; + +@Inject +@Startup +HealthRegistry startupHealthRegistry; + +@Inject +@Wellness +HealthRegistry wellnessHealthRegistry; +---- + +NOTE: Health group registry have not been requested as of yet but if some applications would find +it useful please raise a new issue in the https://github.com/smallrye/smallrye-health/issues[SmallRye Health issue tracker]. + +You can then register your health check by simple calling the `register` methods: + +[source,java] +---- +public void register() { + livenessHealthRegistry.register(new TestLiveness()); <1> +} + +public void registerReady() { + readinessHealthRegistry.register("dynamic-readiness", new TestReadiness()); <2> +} +---- +<1> If no user defined name is provided during registration the health check is registered as +`TestLiveness.class.getName()` +<2> The user provided name `dynamic-readiness` will be used as the check identificator + +To unregister the registered health check you can call the `remove` methods: + +[source,java] +---- +public void remove() { + livenessHealthRegistry.remove(TestLiveness.class.getName()); +} + +public void removeReady() { + readinessHealthRegistry.remove("dynamic-readiness"); +} +---- + +== Resources + +* https://github.com/smallrye/smallrye-health/blob/main/api/src/main/java/io/smallrye/health/api/HealthRegistry.java[`HealthRegistry`] diff --git a/content/docs/smallrye-health/latest/index.adoc b/content/docs/smallrye-health/latest/index.adoc new file mode 100644 index 000000000..980ac059f --- /dev/null +++ b/content/docs/smallrye-health/latest/index.adoc @@ -0,0 +1,18 @@ += SmallRye Health Documentation +include::_attributes.adoc[] + +SmallRye Health 3.0 is an implementation of the MicroProfile Health 3.0 specification. + +Runtimes that currently use it as their health monitoring implementation include: + +* https://quarkus.io/[Quarkus] +* https://wildfly.org/[WildFly] +* https://thorntail.io/[Thorntail] + + +[[more-resources]] +== More Resources + +* https://github.com/smallrye/smallrye-health/[SmallRye Health GitHub project repository] +* https://github.com/eclipse/microprofile-health/[Eclipse MicroProfile Health repository] +* https://download.eclipse.org/microprofile/microprofile-health-3.0/microprofile-health-spec-3.0.html[MicroProfile 3.0 specification text] diff --git a/content/docs/smallrye-health/latest/static-access.adoc b/content/docs/smallrye-health/latest/static-access.adoc new file mode 100644 index 000000000..847e0fff2 --- /dev/null +++ b/content/docs/smallrye-health/latest/static-access.adoc @@ -0,0 +1,59 @@ += Static access to the SmallRye Health +include::_attributes.adoc[] + +In some contexts it might have been necessary to access SmallRye health capabilities in the non-CDI environments where CDI injection access typical for MicroProfile application is not available. For this reason, SmallRye Health provides a programmatic non-CDI static access to the `SmallRyeHealthReporter` and all different type of `HealthRegistry` so users can register (and unregister) health checks dynamically and invoke the reporter to get the same JSON objects as from usual `/health` calls. + +== Accessing health registry and reporting health + +The class `HealthRegistries` provides several static methods to access different types of health registry that can be used to register different types of health checks (see xref:health-registry.adoc#health-registry[Health registry section]). The simplest way to report health is then: + +[source,java] +---- +public static void main(String[] args) { + HealthRegistry livenessRegistry = HealthRegistries.getRegistry(HealthType.LIVENESS); + + livenessRegistry.register("test-hc", () -> HealthCheckResponse.up("test")); + + SmallRyeHealthReporter reporter = new SmallRyeHealthReporter(); + reporter.reportHealth(System.out, reporter.getHealth()); +} +---- + +== Different types of health registries + +SmallRye Health provides a different health registry for each of the base health types: + +[source,java] +---- +HealthRegistry livenessRegistry = HealthRegistries.getRegistry(HealthType.LIVENESS); +HealthRegistry readinessRegistry = HealthRegistries.getRegistry(HealthType.READINESS); +HealthRegistry startupRegistry = HealthRegistries.getRegistry(HealthType.STARTUP); +HealthRegistry wellnessRegistry = HealthRegistries.getRegistry(HealthType.WELLNESS); +---- + +and also a static access to health groups (experimental, see xref:health-groups.adoc#health-groups[health groups section]): + +[source,java] +---- +HealthRegistry testGroupRegistry = HealthRegistries.getHealthGroupRegistry("test-group"); +Collection healthGroupRegistries = HealthRegistries.getHealthGroupRegistries(); +---- + +== Overriding configuration values + +In CDI environment, SmallRye Health uses MicroProfile Config for its configuration. However, in non-CDI environments, MicroProfile Config doesn't work so all configuration values used in SmallRye Health are overridable by methods in the classes that use particular config values: + +`SmallRyeHealthReporter` + +* #setContextPropagated(boolean) +* #setEmptyChecksOutcome(String) +* #setTimeoutSeconds(int) +* #setAdditionalProperties(Map) + +`AsyncHealthCheckFactory` + +* #setUncheckedExceptionDataStyle(String) + +== Resources + +* https://github.com/smallrye/smallrye-health/blob/main/implementation/src/main/java/io/smallrye/health/registry/HealthRegistries.java[`HealthRegistries`] \ No newline at end of file diff --git a/content/docs/smallrye-health/latest/wellness.adoc b/content/docs/smallrye-health/latest/wellness.adoc new file mode 100644 index 000000000..c21fd7e78 --- /dev/null +++ b/content/docs/smallrye-health/latest/wellness.adoc @@ -0,0 +1,34 @@ += Wellness +include::_attributes.adoc[] + +The wellness is another health groups besides the liveness and readiness that can be standardized +as a group of health checks that don't belong to either liveness (application restart) or +readiness (traffic discards) but have a value for any post processing automatic or manual +health monitoring. + +== The @Wellness annotation + +You can use the `@Wellness` annotation in a similar manner as `@Readiness` or `@Liveness`: + +[source,java] +---- +@Wellness <1> +@ApplicationScoped +public class WellnessCheck implements HealthCheck { + @Override + public HealthCheckResponse call() { + return HealthCheckResponse.up("wellness-check"); + } +} +---- +<1> The `@Wellness` qualifier + +== Accessing the wellness checks + +The wellness checks are exposed under the `/health/well` endpoint. They are also available +at grouped `/health` endpoint together with all other health checks (liveness, readiness, and +health groups). + +== References + +* https://github.com/smallrye/smallrye-health/blob/main/api/src/main/java/io/smallrye/health/api/Wellness.java[`Wellness`] diff --git a/content/docs/smallrye-health/main/_attributes.adoc b/content/docs/smallrye-health/main/_attributes.adoc new file mode 100644 index 000000000..b377e9450 --- /dev/null +++ b/content/docs/smallrye-health/main/_attributes.adoc @@ -0,0 +1,18 @@ +:project-version: 4.3.0 +// Attributes shared by every page. smallrye.io overrides the ones it needs when it publishes. +// Pages in subdirectories set :version-root: (../ per level) before including this file. +ifndef::version-root[:version-root:] +:idprefix: +:idseparator: - +:sectanchors: +:partialsdir: {version-root}_partials +:examples-dir: {version-root}_examples +:generated-dir: {version-root}_generated + +// Added by smallrye.io (tools/docs/Docs.java sync) +:javadoc-version: 4.3.0 +:relfileprefix: /docs/smallrye-health/main/ +:imagesdir: /docs/smallrye-health/main +:docs-project: smallrye-health +:docs-version: main +:docs-ref: main diff --git a/content/docs/smallrye-health/main/_nav.yml b/content/docs/smallrye-health/main/_nav.yml new file mode 100644 index 000000000..2b75ac52c --- /dev/null +++ b/content/docs/smallrye-health/main/_nav.yml @@ -0,0 +1,18 @@ +title: SmallRye Health +nav: +- title: Overview + page: index +- title: Features on top of the specification + items: + - title: Asynchronous health checks + page: async-checks + - title: Health Groups + page: health-groups + - title: Health Registry + page: health-registry + - title: Wellness + page: wellness + - title: Static access + page: static-access + - title: Custom Config properties + page: custom-config-properties diff --git a/content/docs/smallrye-health/main/async-checks.adoc b/content/docs/smallrye-health/main/async-checks.adoc new file mode 100644 index 000000000..76a05e243 --- /dev/null +++ b/content/docs/smallrye-health/main/async-checks.adoc @@ -0,0 +1,44 @@ += Asynchronous health checks +include::_attributes.adoc[] + +== Defining asynchronous checks + +SmallRye Health provides a way to define asynchronous checks that can be consumed +in the reactive applications in a non-blocking manner. These checks are defined in +a very similar way to the usual health checks only they implement a different interface: + +[source,java] +---- +@Liveness <1> +@ApplicationScoped <2> +public class LivenessAsync implements AsyncHealthCheck { <3> + + @Override + public Uni call() { <4> + return Uni.createFrom().item(HealthCheckResponse.up("liveness-async")) + .onItem().delayIt().by(Duration.ofMillis(10)); + } +} +---- +<1> The MicroProfile Health CDI qualifier is still required +<2> The check still needs to be a CDI bean +<3> The asynchronous checks must implement the `AsyncHealthCheck` interface +<4> The return type of the health check `call()` method is `Uni` + +The `Uni` is one of the main types included in the https://smallrye.io/smallrye-mutiny/[Mutiny project] +on top of which asynchronous checks are built. Mutiny provides several ways to transform the +most common reactive types to `Uni`, so if you are used to work with different reacitve +library it should be an issue. + +== Consuming health checks asynchronously + +The `SmallRyeHealthReporter` class was extended to provide variants of get health methods +that end with `*Async()` (e.g. `getLivenessAsync()`) that return an `Uni` so it can +also be included in any reactive pipeline. Example uses of this API is demonstrated for instance in +the `AsyncHealthTest`. + +== References + +* https://github.com/smallrye/smallrye-health/blob/main/api/src/main/java/io/smallrye/health/api/AsyncHealthCheck.java[`AsyncHealthCheck`] +* https://github.com/smallrye/smallrye-health/blob/main/implementation/src/main/java/io/smallrye/health/SmallRyeHealthReporter.java[`SmallRyeHealthReporter`] +* https://github.com/smallrye/smallrye-health/blob/main/testsuite/experimental/src/test/java/io/smallrye/health/test/AsyncHealthTest.java[`AsyncHealthTest`] diff --git a/content/docs/smallrye-health/main/custom-config-properties.adoc b/content/docs/smallrye-health/main/custom-config-properties.adoc new file mode 100644 index 000000000..4df522378 --- /dev/null +++ b/content/docs/smallrye-health/main/custom-config-properties.adoc @@ -0,0 +1,39 @@ += Custom configuration values +include::_attributes.adoc[] + +SmallRye Health provides several custom configuration values that are exposing additional features which are currently not a part of the MicroProfile Health specification. The following sections provide an overview of what users can configure to adjust the executions of health check invocations. + +== Disabling Health Checks with configuration + +In SmallRye Health, you can dynamically choose which `HealthCheck` implementation will be included in the final produces responses with configuration values. To disable any included `HealthCheck` implementation dynamically you can use the following configuration property: + +[source,properties] +---- +io.smallrye.health.check..enabled=false +---- + +where `health-check-classname` is a fully-qualified class name of the `HealthCheck` implementation that should not be included in the responses. This value can be specified dynamically on the application startup with MicroProfile Config values (see https://github.com/eclipse/microprofile-config[MicroProfile Config specification]). + +For instance, for `HealthCheck` implementation included in class `org.acme.TestHC` the property would look like `-Dio.smallrye.health.check.org.acme.TestHC.enabled=false` or `IO_SMALLRYE_HEALTH_CHECK_ORG_ACME_TESTHC_ENABLED=false`. + +== Adding additional JSON properties + +SmallRye Health provides a feature that allows users to specify additional properties with fixed values that should be included in the top level properties of the outputed JSON. To specify additonal JSON properties, you can use the following configuration values: + +[source,properties] +---- +io.smallrye.health.additional.property.testProperty1=testValue1 +io.smallrye.health.additional.property.testProperty2=testValue2 +---- + +which will output a JSON like this: + +[source,json] +---- +{ + "checks": [], + "status": "UP", + "testProperty1": "testValue1", + "testProperty2": "testValue2" +} +---- \ No newline at end of file diff --git a/content/docs/smallrye-health/main/health-groups.adoc b/content/docs/smallrye-health/main/health-groups.adoc new file mode 100644 index 000000000..a94eb6295 --- /dev/null +++ b/content/docs/smallrye-health/main/health-groups.adoc @@ -0,0 +1,72 @@ += Health Groups +include::_attributes.adoc[] + +A health group is simply a user defined group of health checks. Namely, liveness and readiness +(and experimental wellness) are predefined health groups. With this feature users are allowed to +define any number of custom health groups and the individual checks that will belong to them. + +== Including a check in a health group + +To include a health check in a custom health group you can use the `HealthGroup` annotations: + +[source,java] +---- +@HealthGroup("group1") <1> +@HealthGroup("group2") <2> +@ApplicationScoped +public class SuccessfulCustom implements HealthCheck { + @Override + public HealthCheckResponse call() { + return HealthCheckResponse.up("successful-check"); + } +} +---- +<1> The check will belong to the health group _"group1"_ +<2> The check will also belong to the health group _"group2"_ + +A health check can be included in any number of health groups including the liveness and readiness: + +[source,java] +---- +@HealthGroup("custom1") +@HealthGroup("different-group") +@Liveness +@ApplicationScoped +public class GroupsCheck implements HealthCheck { ... +---- + +== Accessing the health groups + +The custom health groups are exposed under the `/health/group` resource tree: + +|=== +| Path | Description +| `/health/group` | All health checks in all groups +| `/health/group/\{group-name}` | All health checks in the group called \{group-name} +|=== + +Please note that is totally valid to define custom health group as a liveness and/or readiness +probe if required. Both liveness and readiness are shorcuts for predefined health groups. + +In the previous example, the `GroupsCheck` will be invoked when accessing: + +* `/health/group` +* `/health/group/custom1` +* `/health/group/different-group` +* `/health/live` + +However, it won't be invoked for instance when accessing: + +* `/health/group/foobar` +* `/health/ready` + +== Default health group + +The configuration property `io.smallrye.health.defaultHealthGroup=\{default-group-name}` can be used to specify the health +group name of a new, default health group that is implicitly added to the all +health checks that don't already belong into any health group. This might be useful to easily categorize the custom health groups. + +== References + +* https://github.com/smallrye/smallrye-health/blob/main/api/src/main/java/io/smallrye/health/api/HealthGroup.java[`HealthGroup`] + diff --git a/content/docs/smallrye-health/main/health-registry.adoc b/content/docs/smallrye-health/main/health-registry.adoc new file mode 100644 index 000000000..a5990eee5 --- /dev/null +++ b/content/docs/smallrye-health/main/health-registry.adoc @@ -0,0 +1,65 @@ += Health Registry +include::_attributes.adoc[] + +A health registry provides a way to register the health checks programatically. It also allows +to remove the programatically registered health checks when they are no longer needed. This presents +an option to dynamically register and unregister health checks based on the user defined criteria. + +== Health Registry usage + +The `HealthRegistry` can be injected as a CDI bean with `@Liveness`, `@Readiness`, `@Startup`, or `@Wellness` qualifiers: + +[source,java] +---- +@Inject +@Liveness +HealthRegistry livenessHealthRegistry; + +@Inject +@Readiness +HealthRegistry readinessHealthRegistry; + +@Inject +@Startup +HealthRegistry startupHealthRegistry; + +@Inject +@Wellness +HealthRegistry wellnessHealthRegistry; +---- + +NOTE: Health group registry have not been requested as of yet but if some applications would find +it useful please raise a new issue in the https://github.com/smallrye/smallrye-health/issues[SmallRye Health issue tracker]. + +You can then register your health check by simple calling the `register` methods: + +[source,java] +---- +public void register() { + livenessHealthRegistry.register(new TestLiveness()); <1> +} + +public void registerReady() { + readinessHealthRegistry.register("dynamic-readiness", new TestReadiness()); <2> +} +---- +<1> If no user defined name is provided during registration the health check is registered as +`TestLiveness.class.getName()` +<2> The user provided name `dynamic-readiness` will be used as the check identificator + +To unregister the registered health check you can call the `remove` methods: + +[source,java] +---- +public void remove() { + livenessHealthRegistry.remove(TestLiveness.class.getName()); +} + +public void removeReady() { + readinessHealthRegistry.remove("dynamic-readiness"); +} +---- + +== Resources + +* https://github.com/smallrye/smallrye-health/blob/main/api/src/main/java/io/smallrye/health/api/HealthRegistry.java[`HealthRegistry`] diff --git a/content/docs/smallrye-health/main/index.adoc b/content/docs/smallrye-health/main/index.adoc new file mode 100644 index 000000000..980ac059f --- /dev/null +++ b/content/docs/smallrye-health/main/index.adoc @@ -0,0 +1,18 @@ += SmallRye Health Documentation +include::_attributes.adoc[] + +SmallRye Health 3.0 is an implementation of the MicroProfile Health 3.0 specification. + +Runtimes that currently use it as their health monitoring implementation include: + +* https://quarkus.io/[Quarkus] +* https://wildfly.org/[WildFly] +* https://thorntail.io/[Thorntail] + + +[[more-resources]] +== More Resources + +* https://github.com/smallrye/smallrye-health/[SmallRye Health GitHub project repository] +* https://github.com/eclipse/microprofile-health/[Eclipse MicroProfile Health repository] +* https://download.eclipse.org/microprofile/microprofile-health-3.0/microprofile-health-spec-3.0.html[MicroProfile 3.0 specification text] diff --git a/content/docs/smallrye-health/main/static-access.adoc b/content/docs/smallrye-health/main/static-access.adoc new file mode 100644 index 000000000..847e0fff2 --- /dev/null +++ b/content/docs/smallrye-health/main/static-access.adoc @@ -0,0 +1,59 @@ += Static access to the SmallRye Health +include::_attributes.adoc[] + +In some contexts it might have been necessary to access SmallRye health capabilities in the non-CDI environments where CDI injection access typical for MicroProfile application is not available. For this reason, SmallRye Health provides a programmatic non-CDI static access to the `SmallRyeHealthReporter` and all different type of `HealthRegistry` so users can register (and unregister) health checks dynamically and invoke the reporter to get the same JSON objects as from usual `/health` calls. + +== Accessing health registry and reporting health + +The class `HealthRegistries` provides several static methods to access different types of health registry that can be used to register different types of health checks (see xref:health-registry.adoc#health-registry[Health registry section]). The simplest way to report health is then: + +[source,java] +---- +public static void main(String[] args) { + HealthRegistry livenessRegistry = HealthRegistries.getRegistry(HealthType.LIVENESS); + + livenessRegistry.register("test-hc", () -> HealthCheckResponse.up("test")); + + SmallRyeHealthReporter reporter = new SmallRyeHealthReporter(); + reporter.reportHealth(System.out, reporter.getHealth()); +} +---- + +== Different types of health registries + +SmallRye Health provides a different health registry for each of the base health types: + +[source,java] +---- +HealthRegistry livenessRegistry = HealthRegistries.getRegistry(HealthType.LIVENESS); +HealthRegistry readinessRegistry = HealthRegistries.getRegistry(HealthType.READINESS); +HealthRegistry startupRegistry = HealthRegistries.getRegistry(HealthType.STARTUP); +HealthRegistry wellnessRegistry = HealthRegistries.getRegistry(HealthType.WELLNESS); +---- + +and also a static access to health groups (experimental, see xref:health-groups.adoc#health-groups[health groups section]): + +[source,java] +---- +HealthRegistry testGroupRegistry = HealthRegistries.getHealthGroupRegistry("test-group"); +Collection healthGroupRegistries = HealthRegistries.getHealthGroupRegistries(); +---- + +== Overriding configuration values + +In CDI environment, SmallRye Health uses MicroProfile Config for its configuration. However, in non-CDI environments, MicroProfile Config doesn't work so all configuration values used in SmallRye Health are overridable by methods in the classes that use particular config values: + +`SmallRyeHealthReporter` + +* #setContextPropagated(boolean) +* #setEmptyChecksOutcome(String) +* #setTimeoutSeconds(int) +* #setAdditionalProperties(Map) + +`AsyncHealthCheckFactory` + +* #setUncheckedExceptionDataStyle(String) + +== Resources + +* https://github.com/smallrye/smallrye-health/blob/main/implementation/src/main/java/io/smallrye/health/registry/HealthRegistries.java[`HealthRegistries`] \ No newline at end of file diff --git a/content/docs/smallrye-health/main/wellness.adoc b/content/docs/smallrye-health/main/wellness.adoc new file mode 100644 index 000000000..c21fd7e78 --- /dev/null +++ b/content/docs/smallrye-health/main/wellness.adoc @@ -0,0 +1,34 @@ += Wellness +include::_attributes.adoc[] + +The wellness is another health groups besides the liveness and readiness that can be standardized +as a group of health checks that don't belong to either liveness (application restart) or +readiness (traffic discards) but have a value for any post processing automatic or manual +health monitoring. + +== The @Wellness annotation + +You can use the `@Wellness` annotation in a similar manner as `@Readiness` or `@Liveness`: + +[source,java] +---- +@Wellness <1> +@ApplicationScoped +public class WellnessCheck implements HealthCheck { + @Override + public HealthCheckResponse call() { + return HealthCheckResponse.up("wellness-check"); + } +} +---- +<1> The `@Wellness` qualifier + +== Accessing the wellness checks + +The wellness checks are exposed under the `/health/well` endpoint. They are also available +at grouped `/health` endpoint together with all other health checks (liveness, readiness, and +health groups). + +== References + +* https://github.com/smallrye/smallrye-health/blob/main/api/src/main/java/io/smallrye/health/api/Wellness.java[`Wellness`] diff --git a/content/docs/smallrye-jwt/latest/_attributes.adoc b/content/docs/smallrye-jwt/latest/_attributes.adoc new file mode 100644 index 000000000..cca9b00cc --- /dev/null +++ b/content/docs/smallrye-jwt/latest/_attributes.adoc @@ -0,0 +1,18 @@ +:project-version: 4.6.4 +// Attributes shared by every page. smallrye.io overrides the ones it needs when it publishes. +// Pages in subdirectories set :version-root: (../ per level) before including this file. +ifndef::version-root[:version-root:] +:idprefix: +:idseparator: - +:sectanchors: +:partialsdir: {version-root}_partials +:examples-dir: {version-root}_examples +:generated-dir: {version-root}_generated + +// Added by smallrye.io (tools/docs/Docs.java sync) +:javadoc-version: 4.6.4 +:relfileprefix: /docs/smallrye-jwt/latest/ +:imagesdir: /docs/smallrye-jwt/latest +:docs-project: smallrye-jwt +:docs-version: latest +:docs-ref: 4.6.4 diff --git a/content/docs/smallrye-jwt/latest/_nav.yml b/content/docs/smallrye-jwt/latest/_nav.yml new file mode 100644 index 000000000..eb3b51375 --- /dev/null +++ b/content/docs/smallrye-jwt/latest/_nav.yml @@ -0,0 +1,8 @@ +title: SmallRye JWT +nav: +- title: Overview + page: index +- title: Configuration + page: configuration +- title: Generate JWT Tokens + page: generate-jwt diff --git a/content/docs/smallrye-jwt/latest/_partials/attributes.adoc b/content/docs/smallrye-jwt/latest/_partials/attributes.adoc new file mode 100644 index 000000000..503c73041 --- /dev/null +++ b/content/docs/smallrye-jwt/latest/_partials/attributes.adoc @@ -0,0 +1 @@ +:version: 2.1.2-SNAPSHOT diff --git a/content/docs/smallrye-jwt/latest/configuration.adoc b/content/docs/smallrye-jwt/latest/configuration.adoc new file mode 100644 index 000000000..ae4c280cd --- /dev/null +++ b/content/docs/smallrye-jwt/latest/configuration.adoc @@ -0,0 +1,189 @@ +include::_attributes.adoc[] +include::{partialsdir}/attributes.adoc[] + +[[configuration]] + += Configuration + +== MicroProfile JWT properties + +[cols=">. +|mp.jwt.verify.publickey.location|none|Config property allows for an external or internal location of Public Key to be specified. +|mp.jwt.verify.publickey.algorithm|`RS256`|Signature algorithm. Set it to `ES256` to support the Elliptic Curve signature algorithm. +|mp.jwt.decrypt.key.location|none|Config property allows for an external or internal location of Private Decryption Key to be specified. +|mp.jwt.decrypt.key.algorithm|RSA-OAEP, RSA-OAEP-256|Decryption algorithm, both `RSA-OAEP` and `RSA-OAEP-256` will be supported by default. Set it to `RSA-OAEP-256` to support `RSA-OAEP` with `SHA-256` only. +|mp.jwt.verify.issuer|none|Expected value of the JWT `iss` (issuer) claim. +|mp.jwt.verify.audiences|`none`|Comma separated list of the audiences that a token `aud` claim may contain. +|mp.jwt.verify.clock.skew|`60`|Clock skew in seconds used during the token expiration and age verification. An expired token is accepted if the current time is within the number of seconds specified by this property after the token expiration time. The default value is 60 seconds. +|mp.jwt.verify.token.age|`none`|Number of seconds that must not elapse since the `iat` (issued at) time. +|mp.jwt.token.header|`Authorization`|Set this property if another header such as `Cookie` is used to pass the token. +|mp.jwt.token.cookie|`Bearer`|Name of the cookie containing a token. This property will be effective only if `mp.jwt.token.header` is set to `Cookie`. +|=== + +== Supported Public Key Formats + +Public Keys may be formatted in any of the following formats, specified in order of +precedence: + +- Public Key Cryptography Standards #8 (PKCS#8) PEM +- JSON Web Key (JWK) or JSON Web Key Set (JWKS) +- Base64 URL encoded JSON Web Key (JWK) or JSON Web Key Set (JWKS) + +== SmallRye JWT properties + +SmallRye JWT supports many properties which can be used to customize the token processing: + +[cols=" + io.smallrye + smallrye-jwt-build + ${smallrye.jwt.version} + +---- + +== Create JwtClaimsBuilder and set the claims + +The first step is to initialize a `JwtClaimsBuilder` using one of the options below and add some claims to it: + +[source,java] +---- +import java.util.Collections; +import io.smallrye.jwt.build.Jwt; +import io.smallrye.jwt.build.JwtClaimsBuilder; +... +// Create an empty builder and add some claims +JwtClaimsBuilder builder1 = Jwt.claims(); +builder1.claim("customClaim", "custom-value").issuer("https://issuer.org"); +// Or start typing the claims immediately: +// JwtClaimsBuilder builder1 = Jwt.upn("Alice"); + +// Builder created from the existing claims +JwtClaimsBuilder builder2 = Jwt.claims("/tokenClaims.json"); + +// Builder created from a map of claims +JwtClaimsBuilder builder3 = Jwt.claims(Collections.singletonMap("customClaim", "custom-value")); + +// Builder created from JsonObject +JsonObject userName = Json.createObjectBuilder().add("username", "Alice").build(); +JsonObject userAddress = Json.createObjectBuilder().add("city", "someCity").add("street", "someStreet").build(); +JsonObject json = Json.createObjectBuilder(userName).add("address", userAddress).build(); +JwtClaimsBuilder builder4 = Jwt.claims(json); + +// Builder created from JsonWebToken +@Inject JsonWebToken token; +JwtClaimsBuilder builder5 = Jwt.claims(token); +---- + +The API is fluent so the builder initialization can be done as part of the fluent API sequence. The builder will also +set `iat` (issued at) claim to the current time, `exp` (expires at) claim to a sum of the `iat` claim and `smallrye.jwt.new-token.lifespan` +property values and `jti` (unique token identifier) claim if they have not already been set, so one can skip setting them when possible. + +The next step is to decide how to secure the claims. + +== Sign the claims + +The claims can be signed immediately or after the `JSON Web Signature` headers have been set: + +[source,java] +---- +import io.smallrye.jwt.build.Jwt; +... + +// Sign the claims using the private key loaded from the location set with a 'smallrye.jwt.sign.key.location' property. +// No 'jws()' transition is necessary. +String jwt1 = Jwt.claims("/tokenClaims.json").sign(); + +// Set the headers and sign the claims with an RSA private key loaded in the code (the implementation of this method is omitted). Note a 'jws()' transition to a 'JwtSignatureBuilder'. +String jwt2 = Jwt.claims("/tokenClaims.json").jws().keyId("kid1").header("custom-header", "custom-value").sign(getPrivateKey()); +---- + +Note the `alg` (algorithm) header is set to `RS256` by default. + +== Encrypt the claims + +The claims can be encrypted immediately or after the `JSON Web Encryption` headers have been set the same way as they +can be signed. The only minor difference is that encrypting the claims always requires a `jwe() JwtEncryptionBuilder` +transition: + +[source,java] +---- +import io.smallrye.jwt.build.Jwt; +... + +// Encrypt the claims using the public key loaded from the location set with a 'smallrye.jwt.encrypt.key.location' property. +String jwt1 = Jwt.claims("/tokenClaims.json").jwe().encrypt(); + +// Set the headers and encrypt the claims with an RSA public key loaded in the code (the implementation of this method is omitted). +String jwt2 = Jwt.claims("/tokenClaims.json").jwe().header("custom-header", "custom-value").encrypt(getPublicKey()); +---- + +Note the `alg` (key management algorithm) header is set to `RSA-OAEP` and the `enc` (content encryption header) is set to `A256GCM` by default. + +== Sign the claims and encrypt the nested JWT token + +The claims can be signed and then the nested JWT token encrypted by combining the sign and encrypt steps. + +[source,java] +---- +import io.smallrye.jwt.build.Jwt; +... + +// Sign the claims and encrypt the nested token using the private and public keys loaded from the locations set with the 'smallrye.jwt.sign.key.location' and 'smallrye.jwt.encrypt.key.location' properties respectively. +String jwt = Jwt.claims("/tokenClaims.json").innerSign().encrypt(); +---- + +=== Fast JWT Generation + +If `smallrye.jwt.sign.key.location` or/and `smallrye.jwt.encrypt.key.location` properties are set then one can secure the existing claims (resources, maps, JsonObjects) with a single call: +``` +// More compact than Jwt.claims("/claims.json").sign(); +Jwt.sign("/claims.json"); + +// More compact than Jwt.claims("/claims.json").jwe().encrypt(); +Jwt.encrypt("/claims.json"); + +// More compact than Jwt.claims("/claims.json").innerSign().encrypt(); +Jwt.signAndEncrypt("/claims.json"); +``` +As mentioned above, `iat`, `exp`, `jti` and `iss` claims will be added if needed. + +== Configuration + +Smallrye JWT supports the following properties which can be used to customize the way claims are signed and encrypted: + +[cols=">. +|mp.jwt.verify.publickey.location|none|Config property allows for an external or internal location of Public Key to be specified. +|mp.jwt.verify.publickey.algorithm|`RS256`|Signature algorithm. Set it to `ES256` to support the Elliptic Curve signature algorithm. +|mp.jwt.decrypt.key.location|none|Config property allows for an external or internal location of Private Decryption Key to be specified. +|mp.jwt.decrypt.key.algorithm|RSA-OAEP, RSA-OAEP-256|Decryption algorithm, both `RSA-OAEP` and `RSA-OAEP-256` will be supported by default. Set it to `RSA-OAEP-256` to support `RSA-OAEP` with `SHA-256` only. +|mp.jwt.verify.issuer|none|Expected value of the JWT `iss` (issuer) claim. +|mp.jwt.verify.audiences|`none`|Comma separated list of the audiences that a token `aud` claim may contain. +|mp.jwt.verify.clock.skew|`60`|Clock skew in seconds used during the token expiration and age verification. An expired token is accepted if the current time is within the number of seconds specified by this property after the token expiration time. The default value is 60 seconds. +|mp.jwt.verify.token.age|`none`|Number of seconds that must not elapse since the `iat` (issued at) time. +|mp.jwt.token.header|`Authorization`|Set this property if another header such as `Cookie` is used to pass the token. +|mp.jwt.token.cookie|`Bearer`|Name of the cookie containing a token. This property will be effective only if `mp.jwt.token.header` is set to `Cookie`. +|=== + +== Supported Public Key Formats + +Public Keys may be formatted in any of the following formats, specified in order of +precedence: + +- Public Key Cryptography Standards #8 (PKCS#8) PEM +- JSON Web Key (JWK) or JSON Web Key Set (JWKS) +- Base64 URL encoded JSON Web Key (JWK) or JSON Web Key Set (JWKS) + +== SmallRye JWT properties + +SmallRye JWT supports many properties which can be used to customize the token processing: + +[cols=" + io.smallrye + smallrye-jwt-build + ${smallrye.jwt.version} + +---- + +== Create JwtClaimsBuilder and set the claims + +The first step is to initialize a `JwtClaimsBuilder` using one of the options below and add some claims to it: + +[source,java] +---- +import java.util.Collections; +import io.smallrye.jwt.build.Jwt; +import io.smallrye.jwt.build.JwtClaimsBuilder; +... +// Create an empty builder and add some claims +JwtClaimsBuilder builder1 = Jwt.claims(); +builder1.claim("customClaim", "custom-value").issuer("https://issuer.org"); +// Or start typing the claims immediately: +// JwtClaimsBuilder builder1 = Jwt.upn("Alice"); + +// Builder created from the existing claims +JwtClaimsBuilder builder2 = Jwt.claims("/tokenClaims.json"); + +// Builder created from a map of claims +JwtClaimsBuilder builder3 = Jwt.claims(Collections.singletonMap("customClaim", "custom-value")); + +// Builder created from JsonObject +JsonObject userName = Json.createObjectBuilder().add("username", "Alice").build(); +JsonObject userAddress = Json.createObjectBuilder().add("city", "someCity").add("street", "someStreet").build(); +JsonObject json = Json.createObjectBuilder(userName).add("address", userAddress).build(); +JwtClaimsBuilder builder4 = Jwt.claims(json); + +// Builder created from JsonWebToken +@Inject JsonWebToken token; +JwtClaimsBuilder builder5 = Jwt.claims(token); +---- + +The API is fluent so the builder initialization can be done as part of the fluent API sequence. The builder will also +set `iat` (issued at) claim to the current time, `exp` (expires at) claim to a sum of the `iat` claim and `smallrye.jwt.new-token.lifespan` +property values and `jti` (unique token identifier) claim if they have not already been set, so one can skip setting them when possible. + +The next step is to decide how to secure the claims. + +== Sign the claims + +The claims can be signed immediately or after the `JSON Web Signature` headers have been set: + +[source,java] +---- +import io.smallrye.jwt.build.Jwt; +... + +// Sign the claims using the private key loaded from the location set with a 'smallrye.jwt.sign.key.location' property. +// No 'jws()' transition is necessary. +String jwt1 = Jwt.claims("/tokenClaims.json").sign(); + +// Set the headers and sign the claims with an RSA private key loaded in the code (the implementation of this method is omitted). Note a 'jws()' transition to a 'JwtSignatureBuilder'. +String jwt2 = Jwt.claims("/tokenClaims.json").jws().keyId("kid1").header("custom-header", "custom-value").sign(getPrivateKey()); +---- + +Note the `alg` (algorithm) header is set to `RS256` by default. + +== Encrypt the claims + +The claims can be encrypted immediately or after the `JSON Web Encryption` headers have been set the same way as they +can be signed. The only minor difference is that encrypting the claims always requires a `jwe() JwtEncryptionBuilder` +transition: + +[source,java] +---- +import io.smallrye.jwt.build.Jwt; +... + +// Encrypt the claims using the public key loaded from the location set with a 'smallrye.jwt.encrypt.key.location' property. +String jwt1 = Jwt.claims("/tokenClaims.json").jwe().encrypt(); + +// Set the headers and encrypt the claims with an RSA public key loaded in the code (the implementation of this method is omitted). +String jwt2 = Jwt.claims("/tokenClaims.json").jwe().header("custom-header", "custom-value").encrypt(getPublicKey()); +---- + +Note the `alg` (key management algorithm) header is set to `RSA-OAEP` and the `enc` (content encryption header) is set to `A256GCM` by default. + +== Sign the claims and encrypt the nested JWT token + +The claims can be signed and then the nested JWT token encrypted by combining the sign and encrypt steps. + +[source,java] +---- +import io.smallrye.jwt.build.Jwt; +... + +// Sign the claims and encrypt the nested token using the private and public keys loaded from the locations set with the 'smallrye.jwt.sign.key.location' and 'smallrye.jwt.encrypt.key.location' properties respectively. +String jwt = Jwt.claims("/tokenClaims.json").innerSign().encrypt(); +---- + +=== Fast JWT Generation + +If `smallrye.jwt.sign.key.location` or/and `smallrye.jwt.encrypt.key.location` properties are set then one can secure the existing claims (resources, maps, JsonObjects) with a single call: +``` +// More compact than Jwt.claims("/claims.json").sign(); +Jwt.sign("/claims.json"); + +// More compact than Jwt.claims("/claims.json").jwe().encrypt(); +Jwt.encrypt("/claims.json"); + +// More compact than Jwt.claims("/claims.json").innerSign().encrypt(); +Jwt.signAndEncrypt("/claims.json"); +``` +As mentioned above, `iat`, `exp`, `jti` and `iss` claims will be added if needed. + +== Configuration + +Smallrye JWT supports the following properties which can be used to customize the way claims are signed and encrypted: + +[cols=")` in plain Vert.x. + */ + @Override + public Uni asyncStart() { + + /* + * Vert.x stream (ticks every 2 seconds) to Mutiny stream (Multi), + * then increment a counter. + */ + vertx.periodicStream(2000L) + .toMulti() + .subscribe().with(tick -> counter++); + + /* + * HTTP endpoint, where `listen` returns a `Uni`. + * Notifies of the start procedure completion by replacing and + * returning the`Uni` by `Uni`. + */ + return vertx.createHttpServer() + .requestHandler(req -> req.response().endAndForget("@" + counter)) + .listen(8080) + .onItem() + .invoke(() -> System.out.println("See http://127.0.0.1:8080")) + .onFailure() + .invoke(Throwable::printStackTrace) + .replaceWithVoid(); + } + } + + /* + * Main method, deploys a verticle and awaits for the completion with + * an `*AndAwait()` method. + */ + public static void main(String... args) { + var vertx = Vertx.vertx(); + System.out.println("Deployment Starting"); + vertx.deployVerticleAndAwait(MyVerticle::new, new DeploymentOptions()); + System.out.println("Deployment completed"); + } +} diff --git a/content/docs/smallrye-mutiny-vertx-bindings/2/_nav.yml b/content/docs/smallrye-mutiny-vertx-bindings/2/_nav.yml new file mode 100644 index 000000000..b3aae5716 --- /dev/null +++ b/content/docs/smallrye-mutiny-vertx-bindings/2/_nav.yml @@ -0,0 +1,6 @@ +title: Mutiny Vert.x bindings +nav: +- title: Overview + page: index +- title: API Translation + page: api-translation diff --git a/content/docs/smallrye-mutiny-vertx-bindings/2/api-translation.adoc b/content/docs/smallrye-mutiny-vertx-bindings/2/api-translation.adoc new file mode 100644 index 000000000..5c0c1dbf2 --- /dev/null +++ b/content/docs/smallrye-mutiny-vertx-bindings/2/api-translation.adoc @@ -0,0 +1,114 @@ += API translation +include::_attributes.adoc[] + +The Vert.x Mutiny bindings are generated from the existing Vert.x APIs. + +== Vert.x and asynchronous operation methods + +Vert.x is a toolkit that simplifies asynchronous I/O programming. +The Vert.x APIs rely on simple conventions to denote methods doing asynchronous operations: + +[arabic] +. methods that accept a _callback_ as a last parameter, and +. methods that return a Vert.x `Future`. + +Vert.x has historically relied on callbacks, as in: + +[source,java] +---- +public void someOperation(Foo a, Bar b, Baz c, Handler> handler) { + // (...) +} +---- + +Here `someOperation` is a method where `a`, `b` and `c` are _parameters_, and `handler` is a _callback_: + +* the callback is notified when the asynchronous operation done in `someOperation` completes (_continuation_), and +* callbacks are always passed as a last parameter of type `Handler++<++AsyncResult++<++T++>>++`, and +* `AsyncResult` encapsulates an asynchronous result of type `T`, or a failure. + +Since Vert.x 4 asynchronous operations can also be defined by returning a `Future++<++T++>++`, as in: + +[source,java] +---- +public Future someOperation(Foo a, Bar b, Baz c) { + // (...) +} +---- + +Code generators (e.g., Mutiny, RxJava, Kotlin coroutines, etc) can spot these methods and derive their own https://en.wikipedia.org/wiki/Shim_(computing)[shim APIs]. + +For instance with Mutiny `someOperation` becomes: + +[source,java] +---- +public Uni someOperation(Foo a, Bar b, Baz c) { + // (...) +} +---- + +== What does the Mutiny code generator do? + +.Mutiny code generator +image::images/codegen.png[Mutiny code generator] + +The code generator is applied to selected modules from the Vert.x stack. + +The translation rules are the following. + +[width="100%",cols="30%,70%",options="header",] +|=== +|Rule |Translation +|`io.vertx` package |`io.vertx.mutiny` package +|Asynchronous method |A method returning a `Uni++<++T++>++` +|`ReadStreams++<++T++>++` |Consumed as a `Multi++<++T++>++` +|`WriteStreams++<++T++>++` |Consumed as a _Reactive Streams_ `Subscriber++<++T++>++` +|=== + +Because Mutiny is a _Reactive Streams_ compliant implementation, the generated shims also adapt the Vert.x back-pressure protocol to that of _Reactive Streams_. + +[NOTE] +.Method erasure +==== + +The Mutiny code generator _erases_ methods. +Given an asynchronous method `foo(Handler++<++AsyncResult++<++T++>>++)`, then it is not present in the generated shim as it is replaced with `Uni++<++T++>++ foo()`. + +==== + +== Helper methods + +Given an asynchronous operation method, the Mutiny code generator also provides 2 further variants: + +* `xAndAwait()` to block the caller thread until the outcome is received, or throw a `RuntimeException` when a failure arises, and +* `xAndForget()` to trigger the operation then discard the outcome (failures will be logged). + +Given the `someOperation` method above, the generator provides these 3 methods: + +[source,java] +---- +// The canonical Mutiny method +public Uni someOperation(Foo a, Bar b, Baz c) { + // (...) +} + +// Blocks, may throw a RuntimeException +public T someOperationAndAwait(Foo a, Bar b, Baz c) { + // (...) +} + +// Does not block, the result or failure is discarded +public void someOperationAndForget(Foo a, Bar b, Baz c) { + // (...), +} +---- + +== Generate Mutiny variants from your own APIs + +Vert.x uses an open code generator, so you can generate Mutiny APIs for your own asynchronous interfaces. + +The code generator is available from the `io.smallrye.reactive:vertx-mutiny-generator` artifact. +It works with the Vert.x `io.vertx:vertx-codegen` annotation processor. + +You can generate bindings by having the artifacts on the classpath for annotation processing at compilation time, or you may use other ways in your builds. +The code for this project shows how to generate the files using Maven plugins. diff --git a/content/docs/smallrye-mutiny-vertx-bindings/2/images/codegen.png b/content/docs/smallrye-mutiny-vertx-bindings/2/images/codegen.png new file mode 100644 index 000000000..71a043c6a Binary files /dev/null and b/content/docs/smallrye-mutiny-vertx-bindings/2/images/codegen.png differ diff --git a/content/docs/smallrye-mutiny-vertx-bindings/2/images/redhat_reversed.svg b/content/docs/smallrye-mutiny-vertx-bindings/2/images/redhat_reversed.svg new file mode 100644 index 000000000..37fe83c48 --- /dev/null +++ b/content/docs/smallrye-mutiny-vertx-bindings/2/images/redhat_reversed.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/content/docs/smallrye-mutiny-vertx-bindings/2/index.adoc b/content/docs/smallrye-mutiny-vertx-bindings/2/index.adoc new file mode 100644 index 000000000..8d90b5c7b --- /dev/null +++ b/content/docs/smallrye-mutiny-vertx-bindings/2/index.adoc @@ -0,0 +1,65 @@ += Overview +include::_attributes.adoc[] + +https://vertx.io/[Eclipse Vert.x] is the leading toolkit for writing reactive applications on the JVM. + +While the Vert.x core APIs expose asynchronous programming through _callbacks_ and _promise / future_, code generators offer bindings to other asynchronous programming models, including: https://github.com/vert-x3/vertx-lang-kotlin/tree/master/vertx-lang-kotlin-coroutines[Kotlin coroutines], and https://github.com/vert-x3/vertx-rx[RxJava 1, 2 and 3]. + +This project offers https://vertx.io/[Vert.x] binding for https://smallrye.io/smallrye-mutiny/[Mutiny, an intuitive event-driven reactive programming library for Java]. + +== Getting the bindings + +The bindings can be accessed from the following Maven coordinates: + +* Group: `io.smallrye.reactive` +* Artifact: `smallrye-mutiny-vertx-++<++MODULE++>++` where `MODULE` refers to a Vert.x module, such as `core`, `pg-client`, `web-client`, etc. + +[NOTE] +.The Mutiny bindings are modular +==== + +If you are familiar with other Vert.x bindings such as those for RxJava then you need to be aware that the Mutiny bindings are offered on a per-module basis. +For instance the RxJava 3 bindings are exposed through the `io.vertx:vertx-rx-java3` dependency, and that `vertx-rx-java3` has optional dependencies _on the whole Vert.x stack_. +We think that it is cleaner to offer bindings on a per-module basis, so your project does not have optional dependencies on modules of the Vert.x stack that you don't consume. + +==== + +The full list of supported modules from the Vert.x stack is available at https://github.com/smallrye/smallrye-mutiny-vertx-bindings/tree/main/vertx-mutiny-clients + +== A short example + +The following self-contained https://www.jbang.dev[JBang] script shows some of the features of the Vert.x Mutiny bindings (see the highlights): + +[source,java] +---- +include::{examples-dir}/docs/snippets/hello.java[] +---- + +This script can be run with `./hello.java` or `jbang run hello.java`, and exposes a HTTP server on port 8080: + +.... +$ ./hello.java +[jbang] Building jar... +Deployment Starting +See http://127.0.0.1:8080 +Deployment completed +.... + +The HTTP server responds to any HTTP request with the current value of a counter that is incremented every 2 seconds: + +.... +$ http :8080 +HTTP/1.1 200 OK +content-length: 2 + +@1 + +$ http :8080 +HTTP/1.1 200 OK +content-length: 2 + +@2 +.... + +The deployed verticle uses the Mutiny API, where the `start(Promise++<++Void++>++)` method is replaced by `asyncStart()` method that returns a `Uni++<++Void++>++`. +The code also shows how to convert Vert.x streams into Mutiny `Multi` streams, and how to await for the verticle deployment to complete. diff --git a/content/docs/smallrye-mutiny-vertx-bindings/3/_attributes.adoc b/content/docs/smallrye-mutiny-vertx-bindings/3/_attributes.adoc new file mode 100644 index 000000000..67f676461 --- /dev/null +++ b/content/docs/smallrye-mutiny-vertx-bindings/3/_attributes.adoc @@ -0,0 +1,20 @@ +:project-version: 3.23.1 +// Attributes shared by every page. smallrye.io overrides the ones it needs when it publishes. +// Pages in subdirectories set :version-root: (../ per level) before including this file. +ifndef::version-root[:version-root:] +:idprefix: +:idseparator: - +:sectanchors: +:examples-dir: {version-root}_examples +:partialsdir: {version-root}_partials +:project-name: Mutiny Vert.x bindings +:javadoc-version: latest +:generated-dir: {version-root}_generated + +// Added by smallrye.io (tools/docs/Docs.java sync) +:javadoc-version: 3.23.1 +:relfileprefix: /docs/smallrye-mutiny-vertx-bindings/3/ +:imagesdir: /docs/smallrye-mutiny-vertx-bindings/3 +:docs-project: smallrye-mutiny-vertx-bindings +:docs-version: 3 +:docs-ref: 3.23.1 diff --git a/content/docs/smallrye-mutiny-vertx-bindings/3/_examples/docs/snippets/hello.java b/content/docs/smallrye-mutiny-vertx-bindings/3/_examples/docs/snippets/hello.java new file mode 100644 index 000000000..0e9193688 --- /dev/null +++ b/content/docs/smallrye-mutiny-vertx-bindings/3/_examples/docs/snippets/hello.java @@ -0,0 +1,56 @@ +///usr/bin/env jbang "$0" "$@" ; exit $? +//DEPS io.smallrye.reactive:smallrye-mutiny-vertx-core:2.6.0 + +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.vertx.core.AbstractVerticle; +import io.vertx.core.DeploymentOptions; +import io.vertx.mutiny.core.Vertx; + +public class hello { + + static class MyVerticle extends AbstractVerticle { + + private long counter = 0L; + + /* + * Asynchronous start completion notification through a Uni. + * This is the Mutiny variant of `start(Promise)` in plain Vert.x. + */ + @Override + public Uni asyncStart() { + + /* + * Vert.x stream (ticks every 2 seconds) to Mutiny stream (Multi), + * then increment a counter. + */ + vertx.periodicStream(2000L) + .toMulti() + .subscribe().with(tick -> counter++); + + /* + * HTTP endpoint, where `listen` returns a `Uni`. + * Notifies of the start procedure completion by replacing and + * returning the`Uni` by `Uni`. + */ + return vertx.createHttpServer() + .requestHandler(req -> req.response().endAndForget("@" + counter)) + .listen(8080) + .onItem() + .invoke(() -> System.out.println("See http://127.0.0.1:8080")) + .onFailure() + .invoke(Throwable::printStackTrace) + .replaceWithVoid(); + } + } + + /* + * Main method, deploys a verticle and awaits for the completion with + * an `*AndAwait()` method. + */ + public static void main(String... args) { + var vertx = Vertx.vertx(); + System.out.println("Deployment Starting"); + vertx.deployVerticleAndAwait(MyVerticle::new, new DeploymentOptions()); + System.out.println("Deployment completed"); + } +} diff --git a/content/docs/smallrye-mutiny-vertx-bindings/3/_nav.yml b/content/docs/smallrye-mutiny-vertx-bindings/3/_nav.yml new file mode 100644 index 000000000..b3aae5716 --- /dev/null +++ b/content/docs/smallrye-mutiny-vertx-bindings/3/_nav.yml @@ -0,0 +1,6 @@ +title: Mutiny Vert.x bindings +nav: +- title: Overview + page: index +- title: API Translation + page: api-translation diff --git a/content/docs/smallrye-mutiny-vertx-bindings/3/api-translation.adoc b/content/docs/smallrye-mutiny-vertx-bindings/3/api-translation.adoc new file mode 100644 index 000000000..5c0c1dbf2 --- /dev/null +++ b/content/docs/smallrye-mutiny-vertx-bindings/3/api-translation.adoc @@ -0,0 +1,114 @@ += API translation +include::_attributes.adoc[] + +The Vert.x Mutiny bindings are generated from the existing Vert.x APIs. + +== Vert.x and asynchronous operation methods + +Vert.x is a toolkit that simplifies asynchronous I/O programming. +The Vert.x APIs rely on simple conventions to denote methods doing asynchronous operations: + +[arabic] +. methods that accept a _callback_ as a last parameter, and +. methods that return a Vert.x `Future`. + +Vert.x has historically relied on callbacks, as in: + +[source,java] +---- +public void someOperation(Foo a, Bar b, Baz c, Handler> handler) { + // (...) +} +---- + +Here `someOperation` is a method where `a`, `b` and `c` are _parameters_, and `handler` is a _callback_: + +* the callback is notified when the asynchronous operation done in `someOperation` completes (_continuation_), and +* callbacks are always passed as a last parameter of type `Handler++<++AsyncResult++<++T++>>++`, and +* `AsyncResult` encapsulates an asynchronous result of type `T`, or a failure. + +Since Vert.x 4 asynchronous operations can also be defined by returning a `Future++<++T++>++`, as in: + +[source,java] +---- +public Future someOperation(Foo a, Bar b, Baz c) { + // (...) +} +---- + +Code generators (e.g., Mutiny, RxJava, Kotlin coroutines, etc) can spot these methods and derive their own https://en.wikipedia.org/wiki/Shim_(computing)[shim APIs]. + +For instance with Mutiny `someOperation` becomes: + +[source,java] +---- +public Uni someOperation(Foo a, Bar b, Baz c) { + // (...) +} +---- + +== What does the Mutiny code generator do? + +.Mutiny code generator +image::images/codegen.png[Mutiny code generator] + +The code generator is applied to selected modules from the Vert.x stack. + +The translation rules are the following. + +[width="100%",cols="30%,70%",options="header",] +|=== +|Rule |Translation +|`io.vertx` package |`io.vertx.mutiny` package +|Asynchronous method |A method returning a `Uni++<++T++>++` +|`ReadStreams++<++T++>++` |Consumed as a `Multi++<++T++>++` +|`WriteStreams++<++T++>++` |Consumed as a _Reactive Streams_ `Subscriber++<++T++>++` +|=== + +Because Mutiny is a _Reactive Streams_ compliant implementation, the generated shims also adapt the Vert.x back-pressure protocol to that of _Reactive Streams_. + +[NOTE] +.Method erasure +==== + +The Mutiny code generator _erases_ methods. +Given an asynchronous method `foo(Handler++<++AsyncResult++<++T++>>++)`, then it is not present in the generated shim as it is replaced with `Uni++<++T++>++ foo()`. + +==== + +== Helper methods + +Given an asynchronous operation method, the Mutiny code generator also provides 2 further variants: + +* `xAndAwait()` to block the caller thread until the outcome is received, or throw a `RuntimeException` when a failure arises, and +* `xAndForget()` to trigger the operation then discard the outcome (failures will be logged). + +Given the `someOperation` method above, the generator provides these 3 methods: + +[source,java] +---- +// The canonical Mutiny method +public Uni someOperation(Foo a, Bar b, Baz c) { + // (...) +} + +// Blocks, may throw a RuntimeException +public T someOperationAndAwait(Foo a, Bar b, Baz c) { + // (...) +} + +// Does not block, the result or failure is discarded +public void someOperationAndForget(Foo a, Bar b, Baz c) { + // (...), +} +---- + +== Generate Mutiny variants from your own APIs + +Vert.x uses an open code generator, so you can generate Mutiny APIs for your own asynchronous interfaces. + +The code generator is available from the `io.smallrye.reactive:vertx-mutiny-generator` artifact. +It works with the Vert.x `io.vertx:vertx-codegen` annotation processor. + +You can generate bindings by having the artifacts on the classpath for annotation processing at compilation time, or you may use other ways in your builds. +The code for this project shows how to generate the files using Maven plugins. diff --git a/content/docs/smallrye-mutiny-vertx-bindings/3/images/codegen.png b/content/docs/smallrye-mutiny-vertx-bindings/3/images/codegen.png new file mode 100644 index 000000000..71a043c6a Binary files /dev/null and b/content/docs/smallrye-mutiny-vertx-bindings/3/images/codegen.png differ diff --git a/content/docs/smallrye-mutiny-vertx-bindings/3/images/redhat_reversed.svg b/content/docs/smallrye-mutiny-vertx-bindings/3/images/redhat_reversed.svg new file mode 100644 index 000000000..37fe83c48 --- /dev/null +++ b/content/docs/smallrye-mutiny-vertx-bindings/3/images/redhat_reversed.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/content/docs/smallrye-mutiny-vertx-bindings/3/index.adoc b/content/docs/smallrye-mutiny-vertx-bindings/3/index.adoc new file mode 100644 index 000000000..8d90b5c7b --- /dev/null +++ b/content/docs/smallrye-mutiny-vertx-bindings/3/index.adoc @@ -0,0 +1,65 @@ += Overview +include::_attributes.adoc[] + +https://vertx.io/[Eclipse Vert.x] is the leading toolkit for writing reactive applications on the JVM. + +While the Vert.x core APIs expose asynchronous programming through _callbacks_ and _promise / future_, code generators offer bindings to other asynchronous programming models, including: https://github.com/vert-x3/vertx-lang-kotlin/tree/master/vertx-lang-kotlin-coroutines[Kotlin coroutines], and https://github.com/vert-x3/vertx-rx[RxJava 1, 2 and 3]. + +This project offers https://vertx.io/[Vert.x] binding for https://smallrye.io/smallrye-mutiny/[Mutiny, an intuitive event-driven reactive programming library for Java]. + +== Getting the bindings + +The bindings can be accessed from the following Maven coordinates: + +* Group: `io.smallrye.reactive` +* Artifact: `smallrye-mutiny-vertx-++<++MODULE++>++` where `MODULE` refers to a Vert.x module, such as `core`, `pg-client`, `web-client`, etc. + +[NOTE] +.The Mutiny bindings are modular +==== + +If you are familiar with other Vert.x bindings such as those for RxJava then you need to be aware that the Mutiny bindings are offered on a per-module basis. +For instance the RxJava 3 bindings are exposed through the `io.vertx:vertx-rx-java3` dependency, and that `vertx-rx-java3` has optional dependencies _on the whole Vert.x stack_. +We think that it is cleaner to offer bindings on a per-module basis, so your project does not have optional dependencies on modules of the Vert.x stack that you don't consume. + +==== + +The full list of supported modules from the Vert.x stack is available at https://github.com/smallrye/smallrye-mutiny-vertx-bindings/tree/main/vertx-mutiny-clients + +== A short example + +The following self-contained https://www.jbang.dev[JBang] script shows some of the features of the Vert.x Mutiny bindings (see the highlights): + +[source,java] +---- +include::{examples-dir}/docs/snippets/hello.java[] +---- + +This script can be run with `./hello.java` or `jbang run hello.java`, and exposes a HTTP server on port 8080: + +.... +$ ./hello.java +[jbang] Building jar... +Deployment Starting +See http://127.0.0.1:8080 +Deployment completed +.... + +The HTTP server responds to any HTTP request with the current value of a counter that is incremented every 2 seconds: + +.... +$ http :8080 +HTTP/1.1 200 OK +content-length: 2 + +@1 + +$ http :8080 +HTTP/1.1 200 OK +content-length: 2 + +@2 +.... + +The deployed verticle uses the Mutiny API, where the `start(Promise++<++Void++>++)` method is replaced by `asyncStart()` method that returns a `Uni++<++Void++>++`. +The code also shows how to convert Vert.x streams into Mutiny `Multi` streams, and how to await for the verticle deployment to complete. diff --git a/content/docs/smallrye-mutiny-vertx-bindings/3/using-vertx-expectations.adoc b/content/docs/smallrye-mutiny-vertx-bindings/3/using-vertx-expectations.adoc new file mode 100644 index 000000000..44e817a02 --- /dev/null +++ b/content/docs/smallrye-mutiny-vertx-bindings/3/using-vertx-expectations.adoc @@ -0,0 +1,54 @@ += Using Vert.x expectations +include::_attributes.adoc[] + +Vert.x `Future` support https://javadoc.io/static/io.vertx/vertx-core/4.5.10/io/vertx/core/Expectation.html[expectations] as predicates on the resolved values. + +A good example of pre-defined expectations are those from `HttpResponseExpectation`: + +[source,java] +---- +Future future = client + .request(HttpMethod.GET, "some-uri") + .compose(request -> request + .send() + .expecting(HttpResponseExpectation.SC_OK.and(HttpResponseExpectation.JSON)) + .compose(response -> response + .body() + .map(buffer -> buffer.toJsonObject()))); +---- + +In this example the HTTP response is expected to be with status code 200 (`SC++_++OK`) and have the `application/json` content-type (`JSON`). + +== Where are expectations gone in a Vert.x Mutiny bindings API? + +While expectations are very useful, they apply to `Future` in most of the Vert.x APIs such as the core HTTP client. + +Since the Mutiny bindings generator turns `Future`-returning methods into `Uni`-returning methods, you need to leverage another route to use them. + +== Turning expectations into operators + +The `io.smallrye.mutiny.vertx.core.Expectations` class that comes with the `smallrye-mutiny-vertx-core` artifact bring 2 helper methods. + +The `expecting` methods build functions that can be used with the `Uni::plug` operator, as in: + +[source,java] +---- +Expectation tenToTwenty = (value -> value >= 10 && value <= 20); + +return Uni.createFrom().item(15) + .plug(expectation(tenToTwenty)); +---- + +Some expectations such as those in `HttpResponseExpectation` apply to types from the core Vert.x APIs. +Since the Mutiny bindings generate shims (e.g., `io.vertx.mutiny.core.http.HttpResponseHead`), you need to extract the delegate type for the expectations to work on the correct type (e.g., `io.vertx.core.http.HttpResponseHead`): + +[source,java] +---- +return vertx.createHttpClient() + .request(HttpMethod.GET, port, "localhost", "/") + .chain(HttpClientRequest::send) + .plug(expectation(HttpClientResponse::getDelegate, status(200).and(contentType("text/plain")))) + .onItem().transformToUni(HttpClientResponse::body) +---- + +The extractor function here is `HttpClientResponse::getDelegate`, so that the `status(200).and(contentType("text/plain"))` expectation applies to `io.vertx.core.http.HttpResponseHead` and not the `io.vertx.mutiny.core.http.HttpResponseHead` generated shim. diff --git a/content/docs/smallrye-mutiny-vertx-bindings/latest/_attributes.adoc b/content/docs/smallrye-mutiny-vertx-bindings/latest/_attributes.adoc new file mode 100644 index 000000000..20813e2bd --- /dev/null +++ b/content/docs/smallrye-mutiny-vertx-bindings/latest/_attributes.adoc @@ -0,0 +1,20 @@ +:project-version: 4.0.2 +// Attributes shared by every page. smallrye.io overrides the ones it needs when it publishes. +// Pages in subdirectories set :version-root: (../ per level) before including this file. +ifndef::version-root[:version-root:] +:idprefix: +:idseparator: - +:sectanchors: +:examples-dir: {version-root}_examples +:partialsdir: {version-root}_partials +:project-name: Mutiny Vert.x bindings +:javadoc-version: latest +:generated-dir: {version-root}_generated + +// Added by smallrye.io (tools/docs/Docs.java sync) +:javadoc-version: 4.0.2 +:relfileprefix: /docs/smallrye-mutiny-vertx-bindings/latest/ +:imagesdir: /docs/smallrye-mutiny-vertx-bindings/latest +:docs-project: smallrye-mutiny-vertx-bindings +:docs-version: latest +:docs-ref: 4.0.2 diff --git a/content/docs/smallrye-mutiny-vertx-bindings/latest/_nav.yml b/content/docs/smallrye-mutiny-vertx-bindings/latest/_nav.yml new file mode 100644 index 000000000..168665b5a --- /dev/null +++ b/content/docs/smallrye-mutiny-vertx-bindings/latest/_nav.yml @@ -0,0 +1,20 @@ +title: Mutiny Vert.x bindings +nav: +- title: Overview + page: index +- title: Getting Started + page: getting-started +- title: Uni and Multi + page: uni-and-multi +- title: Type Mapping + page: type-mapping +- title: Error Handling + page: error-handling +- title: Blocking and Testing + page: blocking-and-testing +- title: Using the Generator + page: using-the-generator +- title: Available Modules + page: available-modules +- title: Upgrading from version 3 to 4 + page: migration-guide-3x-4x diff --git a/content/docs/smallrye-mutiny-vertx-bindings/latest/available-modules.adoc b/content/docs/smallrye-mutiny-vertx-bindings/latest/available-modules.adoc new file mode 100644 index 000000000..1b55df84f --- /dev/null +++ b/content/docs/smallrye-mutiny-vertx-bindings/latest/available-modules.adoc @@ -0,0 +1,50 @@ += Available Modules +include::_attributes.adoc[] + +This project provides Mutiny bindings for a wide range of Vert.x modules: web, databases, messaging, authentication, templating, and more. + +== Naming Convention + +All modules share the same group ID and follow a predictable artifact ID pattern: + +* *Group ID*: `io.smallrye.reactive` +* *Artifact ID*: `smallrye-mutiny-vertx-++{++name}` + +The `++{++name}` suffix mirrors the Vert.x module name. +For example, the Vert.x `vertx-web-client` module becomes `smallrye-mutiny-vertx-web-client`. + +== Version Management + +Import the BOM to manage versions centrally. +See xref:getting-started.adoc#adding-the-bom[Getting Started — Adding the BOM] for the dependency management snippet. + +== Finding the Right Module + +To find the Mutiny binding for a Vert.x module, take the Vert.x artifact ID and prefix it with `smallrye-mutiny-`. +For example, the Vert.x `vertx-pg-client` module becomes `smallrye-mutiny-vertx-pg-client`: + +[source,xml] +---- + + io.smallrye.reactive + smallrye-mutiny-vertx-pg-client + +---- + +This pattern applies consistently across all modules. + +== Imports + +When using a Mutiny binding, import from the `io.vertx.mutiny.++*++` packages rather than `io.vertx.core.++*++` or `io.vertx.ext.++*++`. +The Mutiny classes are generated shims that wrap the original Vert.x types. + +[source,java] +---- +// Correct: Mutiny shim +import io.vertx.mutiny.ext.web.client.WebClient; + +// Wrong: original Vert.x type +import io.vertx.ext.web.client.WebClient; +---- + +Configuration and options classes (e.g., `WebClientOptions`) are data objects and are used directly from the Vert.x packages since they do not need shims. diff --git a/content/docs/smallrye-mutiny-vertx-bindings/latest/blocking-and-testing.adoc b/content/docs/smallrye-mutiny-vertx-bindings/latest/blocking-and-testing.adoc new file mode 100644 index 000000000..5e2e8b435 --- /dev/null +++ b/content/docs/smallrye-mutiny-vertx-bindings/latest/blocking-and-testing.adoc @@ -0,0 +1,325 @@ += Blocking and Testing +include::_attributes.adoc[] + +The Mutiny Vert.x bindings provide blocking APIs that convert asynchronous `Uni` and `Multi` results into synchronous calls. These APIs are essential for testing and are equally valuable for imperative-style programming on virtual threads, worker threads, and CLI tools. + +*Never block on the Vert.x event loop.* Blocking the event loop freezes the entire application. Use these APIs only in contexts where blocking is safe: + +* *Virtual threads* (Java 21{plus}): first-class imperative programming with Vert.x clients +* *Worker threads*: offloaded via `executeBlocking` +* *Tests*: JUnit test methods run outside the event loop +* *CLI tools and scripts*: short-lived programs without an event loop + +== Virtual Threads: Imperative Vert.x + +With Java 21{plus} virtual threads, the blocking APIs let you write Vert.x client code in a straightforward imperative style. Virtual threads can block without wasting platform threads, making `andAwait()` and `.await().atMost()` practical outside of tests. + +For example, a web client call written imperatively: + +[.tabs] +==== + +Mutiny Bindings:: ++ +-- + +[source,java] +---- +WebClient client = WebClient.create(vertx, new WebClientOptions() + .setDefaultPort(8080) + .setDefaultHost("localhost")); + +HttpResponse response = client.get("/api/users") + .as(BodyCodec.jsonObject()) + .sendAndAwait(); + +JsonObject body = response.body(); +---- + +-- + +Vanilla Vert.x:: ++ +-- + +[source,java] +---- +WebClient client = WebClient.create(vertx, new WebClientOptions() + .setDefaultPort(8080) + .setDefaultHost("localhost")); + +HttpResponse response = client.get("/api/users") + .as(BodyCodec.jsonObject()) + .send() + .toCompletionStage().toCompletableFuture().join(); + +JsonObject body = response.body(); +---- + +-- + +==== + +No callbacks, no reactive chains: just sequential code that reads top to bottom. Each `andAwait()` call suspends the virtual thread until the result is available, while the underlying Vert.x event loop continues processing other work. + +== andAwait() + +Methods ending in `andAwait()` block the calling thread indefinitely until the operation completes. Every `Uni`-returning method on a Mutiny Vert.x class has a corresponding `andAwait()` variant. + +[.tabs] +==== + +Mutiny Bindings:: ++ +-- + +[source,java] +---- +EventBus bus = vertx.eventBus(); +bus.consumer("address", message -> message.reply("world")) + .completionAndAwait(); + +Message reply = bus.requestAndAwait("address", "hello"); +---- + +-- + +Vanilla Vert.x:: ++ +-- + +[source,java] +---- +EventBus bus = vertx.eventBus(); +bus.consumer("address", message -> message.reply("world")) + .completion() + .toCompletionStage().toCompletableFuture().join(); + +Message reply = bus.request("address", "hello") + .toCompletionStage().toCompletableFuture().join(); +---- + +-- + +==== + +Use `andAwait()` when you are confident the operation will complete and do not need a timeout: for instance, in tests with controlled infrastructure or on virtual threads where indefinite suspension is acceptable. + +== .await().atMost(Duration) + +For operations that may hang or where you want a safety net, use `.await().atMost(Duration)` on the `Uni` directly. This blocks until the result arrives or the timeout expires, throwing a `TimeoutException` if the deadline is exceeded. + +[.tabs] +==== + +Mutiny Bindings:: ++ +-- + +[source,java] +---- +Buffer payload = vertx.createHttpClient() + .request(HttpMethod.GET, port, "localhost", "/") + .onItem().transformToUni(HttpClientRequest::send) + .onItem().transformToUni(HttpClientResponse::body) + .await().atMost(Duration.ofSeconds(5)); +---- + +-- + +Vanilla Vert.x:: ++ +-- + +[source,java] +---- +Buffer payload = vertx.createHttpClient() + .request(HttpMethod.GET, port, "localhost", "/") + .compose(HttpClientRequest::send) + .compose(HttpClientResponse::body) + .toCompletionStage().toCompletableFuture() + .get(5, TimeUnit.SECONDS); +---- + +-- + +==== + +This is the preferred approach when calling external services that might not respond. + +== Blocking Iteration on Multi + +For `Multi` streams, two methods convert the asynchronous stream into a blocking iterator or Java stream: + +* `toBlockingIterable()`: returns an `Iterable` that blocks on each element +* `toBlockingStream()`: returns a `java.util.stream.Stream` that blocks on each element + +[.tabs] +==== + +Mutiny Bindings:: ++ +-- + +[source,java] +---- +AsyncFile asyncFile = vertx.fileSystem() + .openAndAwait(path, new OpenOptions()); + +asyncFile.toBlockingStream() + .forEach(buffer::appendBuffer); +---- + +-- + +Vanilla Vert.x:: ++ +-- + +[source,java] +---- +AsyncFile asyncFile = vertx.fileSystem() + .open(path, new OpenOptions()) + .toCompletionStage().toCompletableFuture().join(); + +asyncFile.handler(chunk -> buffer.appendBuffer(chunk)); +---- + +-- + +==== + +These are useful for consuming a reactive stream in imperative code, such as reading a file or draining a message consumer. + +== JUnit 5 Testing Pattern + +A standard pattern for testing Mutiny Vert.x code with JUnit 5: + +[.tabs] +==== + +Mutiny Bindings:: ++ +-- + +[source,java] +---- +class WebClientTest { + + Vertx vertx; + + @BeforeEach + void setUp() { + vertx = Vertx.vertx(); + } + + @AfterEach + void tearDown() { + vertx.closeAndAwait(); + } + + @Test + void getJsonObject() { + // ... set up a test HTTP server ... + + WebClient client = WebClient.create(vertx, new WebClientOptions() + .setDefaultPort(server.actualPort()) + .setDefaultHost("localhost")); + + HttpResponse response = client.get("/yo") + .as(BodyCodec.jsonObject()) + .sendAndAwait(); + + assertThat(response.statusCode()).isEqualTo(200); + assertThat(response.body().getString("msg")).isEqualTo("hello"); + } +} +---- + +-- + +Vanilla Vert.x:: ++ +-- + +[source,java] +---- +@ExtendWith(VertxExtension.class) +class WebClientTest { + + @Test + void getJsonObject(Vertx vertx, VertxTestContext testContext) { + // ... set up a test HTTP server ... + + WebClient client = WebClient.create(vertx, new WebClientOptions() + .setDefaultPort(server.actualPort()) + .setDefaultHost("localhost")); + + client.get("/yo") + .as(BodyCodec.jsonObject()) + .send() + .onComplete(testContext.succeeding(response -> { + testContext.verify(() -> { + assertThat(response.statusCode()).isEqualTo(200); + assertThat(response.body().getString("msg")).isEqualTo("hello"); + }); + testContext.completeNow(); + })); + } +} +---- + +-- + +==== + +Key points: + +* Create a fresh `Vertx` instance in `@BeforeEach` to isolate tests. +* Shut it down with `closeAndAwait()` in `@AfterEach` to release resources. +* Use `andAwait()` or `.await().atMost()` in the test body: JUnit test threads are not event loop threads, so blocking is safe. + +With the Mutiny bindings, tests read sequentially with no callbacks. With vanilla Vert.x, you need `VertxExtension`, `VertxTestContext`, and nested `succeeding` / `verify` callbacks to handle the asynchronous result. + +== executeBlocking for CPU-Intensive Work + +When you need to run CPU-intensive or legacy blocking code from within a Vert.x context, use `vertx.executeBlocking()`. This offloads work to a worker thread pool, returning a result asynchronously. + +[.tabs] +==== + +Mutiny Bindings:: ++ +-- + +[source,java] +---- +Uni uni = vertx.executeBlocking(() -> { + // Runs on a worker thread — blocking calls are safe here + return expensiveComputation(); +}); + +String result = uni.await().indefinitely(); +---- + +-- + +Vanilla Vert.x:: ++ +-- + +[source,java] +---- +Future future = vertx.executeBlocking(() -> { + // Runs on a worker thread — blocking calls are safe here + return expensiveComputation(); +}); + +String result = future.toCompletionStage().toCompletableFuture().join(); +---- + +-- + +==== + +Inside the `executeBlocking` callable, you are on a worker thread, so blocking calls are safe. The Mutiny variant returns a `Uni` that can be composed into a larger reactive pipeline or awaited directly. diff --git a/content/docs/smallrye-mutiny-vertx-bindings/latest/error-handling.adoc b/content/docs/smallrye-mutiny-vertx-bindings/latest/error-handling.adoc new file mode 100644 index 000000000..fa5da0685 --- /dev/null +++ b/content/docs/smallrye-mutiny-vertx-bindings/latest/error-handling.adoc @@ -0,0 +1,435 @@ += Error Handling +include::_attributes.adoc[] + +One advantage of using Mutiny with Vert.x is that errors propagate through the reactive pipeline automatically. +With the callback or `Future`-based API you must manually check for failures at each step. +With Mutiny, failures flow downstream until something handles them, so you focus on the success path and deal with errors once: + +[.tabs] +==== + +Mutiny Bindings:: ++ +-- + +[source,java] +---- +// Errors propagate automatically +client.request(HttpMethod.GET, "/data") + .onItem().transformToUni(HttpClientRequest::send) + .onItem().transformToUni(HttpClientResponse::body) + .subscribe().with( + body -> System.out.println("Got: " + body), + failure -> System.err.println("Failed: " + failure.getMessage()) + ); +---- + +-- + +Vanilla Vert.x:: ++ +-- + +[source,java] +---- +// Manual failure checks at each step +client.request(HttpMethod.GET, "/data") + .compose(request -> request.send()) + .compose(response -> response.body()) + .onSuccess(body -> System.out.println("Got: " + body)) + .onFailure(err -> System.err.println("Failed: " + err.getMessage())); +---- + +-- + +==== + +If any step in the pipeline fails, the remaining `onItem()` operators are skipped and the failure handler receives the error. With vanilla Vert.x, `compose()` chains behave similarly — a failure in any stage short-circuits to the `onFailure` handler — but the Mutiny API makes the distinction between item and failure handling more explicit through its operator vocabulary. + +== Basic failure handling + +The simplest way to handle errors is to provide both a success and failure callback to `subscribe().with()`: + +[.tabs] +==== + +Mutiny Bindings:: ++ +-- + +[source,java] +---- +uni.subscribe().with( + item -> System.out.println("Received: " + item), + failure -> System.err.println("Error: " + failure.getMessage()) +); +---- + +-- + +Vanilla Vert.x:: ++ +-- + +[source,java] +---- +future + .onSuccess(item -> System.out.println("Received: " + item)) + .onFailure(err -> System.err.println("Error: " + err.getMessage())); +---- + +-- + +==== + +If you omit the failure callback, unhandled failures are reported to Mutiny's global failure handler. + +== Recovery + +You can recover from failures inline rather than letting them propagate to the subscriber. + +=== Recover with a fallback item + +[.tabs] +==== + +Mutiny Bindings:: ++ +-- + +[source,java] +---- +Uni result = uni + .onFailure().recoverWithItem("default value"); +---- + +-- + +Vanilla Vert.x:: ++ +-- + +[source,java] +---- +Future result = future + .recover(err -> Future.succeededFuture("default value")); +---- + +-- + +==== + +=== Recover with another Uni + +When recovery itself requires an asynchronous operation, use `recoverWithUni`: + +[.tabs] +==== + +Mutiny Bindings:: ++ +-- + +[source,java] +---- +Uni result = uni + .onFailure().recoverWithUni(() -> fetchFromFallbackService()); +---- + +-- + +Vanilla Vert.x:: ++ +-- + +[source,java] +---- +Future result = future + .recover(err -> fetchFromFallbackService()); +---- + +-- + +==== + +=== Recover from specific exceptions + +Both recovery methods accept a predicate or exception class to recover selectively: + +[.tabs] +==== + +Mutiny Bindings:: ++ +-- + +[source,java] +---- +Uni result = uni + .onFailure(TimeoutException.class).recoverWithItem("timed out") + .onFailure(IOException.class).recoverWithUni(() -> retryElsewhere()); +---- + +-- + +Vanilla Vert.x:: ++ +-- + +[source,java] +---- +Future result = future + .recover(err -> { + if (err instanceof TimeoutException) { + return Future.succeededFuture("timed out"); + } + if (err instanceof IOException) { + return retryElsewhere(); + } + return Future.failedFuture(err); + }); +---- + +-- + +==== + +With Mutiny, selective recovery is declarative — you chain `.onFailure(ExceptionType.class)` for each case. With vanilla Vert.x, you use a single `recover()` block and check types with `instanceof`. + +== Retries + +Mutiny provides built-in retry support: + +[.tabs] +==== + +Mutiny Bindings:: ++ +-- + +[source,java] +---- +Uni result = uni + .onFailure().retry().atMost(3); +---- + +-- + +Vanilla Vert.x:: ++ +-- + +[source,java] +---- +Future fetchWithRetry(int retries) { + return doFetch() + .recover(err -> { + if (retries > 0) { + return fetchWithRetry(retries - 1); + } + return Future.failedFuture(err); + }); +} + +Future result = fetchWithRetry(3); +---- + +-- + +==== + +This retries the upstream operation up to 3 times on any failure. +With vanilla Vert.x, retries require a recursive method that calls itself from within a `recover()` block. + +For production use, add exponential backoff to avoid overwhelming a failing service: + +[.tabs] +==== + +Mutiny Bindings:: ++ +-- + +[source,java] +---- +Uni result = uni + .onFailure().retry() + .withBackOff(Duration.ofMillis(100), Duration.ofSeconds(5)) + .atMost(5); +---- + +-- + +Vanilla Vert.x:: ++ +-- + +[source,java] +---- +Future fetchWithBackoff(int retries, long delayMs) { + return doFetch() + .recover(err -> { + if (retries > 0) { + Promise promise = Promise.promise(); + long nextDelay = Math.min(delayMs * 2, 5000); + vertx.setTimer(delayMs, id -> + fetchWithBackoff(retries - 1, nextDelay) + .onComplete(promise)); + return promise.future(); + } + return Future.failedFuture(err); + }); +} + +Future result = fetchWithBackoff(5, 100); +---- + +-- + +==== + +This retries up to 5 times with an initial delay of 100ms that increases exponentially up to a maximum of 5 seconds between attempts. +With vanilla Vert.x, exponential backoff requires managing a `Promise`, a timer via `vertx.setTimer()`, and manual delay calculation — all of which Mutiny handles as a single operator chain. + +== Failure transformation + +You can transform a failure into a different exception type, which is useful for wrapping low-level errors into domain-specific ones: + +[.tabs] +==== + +Mutiny Bindings:: ++ +-- + +[source,java] +---- +Uni result = uni + .onFailure().transform(err -> new MyServiceException("Operation failed", err)); +---- + +-- + +Vanilla Vert.x:: ++ +-- + +[source,java] +---- +Future result = future + .recover(err -> Future.failedFuture( + new MyServiceException("Operation failed", err))); +---- + +-- + +==== + +== Vert.x expectations with Mutiny + +Vert.x provides an `Expectation` interface that works as a predicate on resolved `Future` values. +Pre-defined expectations such as `HttpResponseExpectation.status(200)` are convenient for validating HTTP responses. + +Since the Mutiny bindings convert `Future`-returning methods into `Uni`-returning methods, you cannot use expectations directly. +Instead, the `io.smallrye.mutiny.vertx.core.Expectations` helper class adapts them for use with `Uni.plug()`. + +=== Simple expectation + +[.tabs] +==== + +Mutiny Bindings:: ++ +-- + +[source,java] +---- +import static io.smallrye.mutiny.vertx.core.Expectations.expectation; + +Expectation tenToTwenty = (value -> value >= 10 && value <= 20); + +Uni.createFrom().item(15) + .plug(expectation(tenToTwenty)); +---- + +-- + +Vanilla Vert.x:: ++ +-- + +[source,java] +---- +Expectation tenToTwenty = (value -> value >= 10 && value <= 20); + +Future.succeededFuture(15) + .expecting(tenToTwenty); +---- + +-- + +==== + +If the value does not satisfy the expectation, the `Uni` fails (or the `Future` fails in vanilla Vert.x). +For example, passing `42` instead of `15` would produce a failure with the message: `Unexpected result: 42`. + +=== HTTP response expectations + +Vert.x ships pre-defined expectations for HTTP responses, such as `status()` and `contentType()`. +These expectations are typed against core Vert.x types (e.g., `io.vertx.core.http.HttpResponseHead`), not against the Mutiny shim types (e.g., `io.vertx.mutiny.core.http.HttpResponseHead`). + +To bridge this gap, the `expectation` helper accepts an extractor function as its first argument. +Passing `HttpClientResponse::getDelegate` unwraps the Mutiny shim so the expectation receives the underlying Vert.x type: + +[.tabs] +==== + +Mutiny Bindings:: ++ +-- + +[source,java] +---- +import static io.smallrye.mutiny.vertx.core.Expectations.expectation; +import static io.vertx.core.http.HttpResponseExpectation.contentType; +import static io.vertx.core.http.HttpResponseExpectation.status; + +vertx.createHttpClient() + .request(HttpMethod.GET, port, "localhost", "/") + .onItem().transformToUni(HttpClientRequest::send) + .plug(expectation(HttpClientResponse::getDelegate, status(200).and(contentType("text/plain")))) + .onItem().transformToUni(HttpClientResponse::body) + .await().atMost(Duration.ofSeconds(5)); +---- + +-- + +Vanilla Vert.x:: ++ +-- + +[source,java] +---- +import static io.vertx.core.http.HttpResponseExpectation.contentType; +import static io.vertx.core.http.HttpResponseExpectation.status; + +vertx.createHttpClient() + .request(HttpMethod.GET, port, "localhost", "/") + .compose(HttpClientRequest::send) + .expecting(status(200).and(contentType("text/plain"))) + .compose(HttpClientResponse::body) + .toCompletionStage().toCompletableFuture() + .get(5, TimeUnit.SECONDS); +---- + +-- + +==== + +In this example: + +* `HttpClientResponse::getDelegate` extracts the core Vert.x response from the Mutiny shim. With vanilla Vert.x, no unwrapping is needed since you work directly with the core types. +* `status(200).and(contentType("text/plain"))` combines two expectations: the response must have status 200 and content type `text/plain`. +* If either expectation fails, the `Uni` pipeline (or the `Future` chain) fails with a descriptive error. + +You can combine any number of expectations using `.and()`. diff --git a/content/docs/smallrye-mutiny-vertx-bindings/latest/getting-started.adoc b/content/docs/smallrye-mutiny-vertx-bindings/latest/getting-started.adoc new file mode 100644 index 000000000..178be01e5 --- /dev/null +++ b/content/docs/smallrye-mutiny-vertx-bindings/latest/getting-started.adoc @@ -0,0 +1,218 @@ += Getting Started +include::_attributes.adoc[] + +This guide walks you through adding SmallRye Mutiny Vert.x Bindings to your project and writing a first reactive application. + +== Prerequisites + +* *Java 17* or later +* *Maven 3.9* or later + +== Adding the BOM + +The simplest way to manage dependency versions is to import the BOM (Bill of Materials) in your `pom.xml`: + +[source,xml] +---- + + + + io.smallrye.reactive + vertx-mutiny-clients-bom + ${mutiny-vertx.version} + pom + import + + + +---- + +Replace `$++{++mutiny-vertx.version}` with the version you want to use, or define it as a Maven property. + +== Adding module dependencies + +With the BOM imported you can add individual modules without specifying a version. +At a minimum you need the core module: + +[source,xml] +---- + + io.smallrye.reactive + smallrye-mutiny-vertx-core + +---- + +If you also want to use the reactive HTTP client (WebClient), add: + +[source,xml] +---- + + io.smallrye.reactive + smallrye-mutiny-vertx-web-client + +---- + +== A complete example + +The following program starts an HTTP server that replies with a greeting and then uses WebClient to call it: + +[.tabs] +==== + +Mutiny Bindings:: ++ +-- + +[source,java] +---- +import io.vertx.mutiny.core.Vertx; +import io.vertx.mutiny.core.http.HttpServer; +import io.vertx.mutiny.ext.web.client.WebClient; +import io.vertx.ext.web.client.WebClientOptions; +import io.vertx.mutiny.ext.web.client.HttpResponse; +import io.vertx.mutiny.ext.web.codec.BodyCodec; + +public class GettingStarted { + + public static void main(String[] args) { + Vertx vertx = Vertx.vertx(); + + HttpServer server = vertx.createHttpServer() + .requestHandler(req -> req.response() + .setStatusCode(200) + .putHeader("content-type", "text/plain") + .endAndForget("Hello from Mutiny!")) + .listenAndAwait(8080); + + System.out.println("Server listening on port " + server.actualPort()); + + WebClient client = WebClient.create(vertx, new WebClientOptions() + .setDefaultPort(8080) + .setDefaultHost("localhost")); + + HttpResponse response = client.get("/") + .as(BodyCodec.string()) + .sendAndAwait(); + + System.out.println("Response: " + response.body()); + + vertx.closeAndAwait(); + } +} +---- + +-- + +Vanilla Vert.x:: ++ +-- + +[source,java] +---- +import io.vertx.core.Vertx; +import io.vertx.ext.web.client.WebClient; +import io.vertx.ext.web.client.WebClientOptions; +import io.vertx.ext.web.codec.BodyCodec; + +public class GettingStarted { + + public static void main(String[] args) { + Vertx vertx = Vertx.vertx(); + + vertx.createHttpServer() + .requestHandler(req -> req.response() + .setStatusCode(200) + .putHeader("content-type", "text/plain") + .end("Hello from Vert.x!")) + .listen(8080) + .compose(server -> { + System.out.println("Server listening on port " + server.actualPort()); + + WebClient client = WebClient.create(vertx, new WebClientOptions() + .setDefaultPort(8080) + .setDefaultHost("localhost")); + + return client.get("/") + .as(BodyCodec.string()) + .send(); + }) + .onSuccess(response -> + System.out.println("Response: " + response.body())) + .onComplete(v -> vertx.close()); + } +} +---- + +-- + +==== + +=== What is happening here + +[arabic] +. `Vertx.vertx()` creates a Mutiny-based Vert.x instance. +. `listenAndAwait(8080)` starts the server and blocks the calling thread until the server is ready. Under the hood it subscribes to the `Uni++<++HttpServer++>++` returned by `listen()` and awaits its result. +. `sendAndAwait()` sends the HTTP request and blocks until the response arrives: again a convenience wrapper around a `Uni`. +. `closeAndAwait()` shuts down the Vert.x instance. + +=== Using explicit reactive operators + +The `++*++AndAwait()` helpers are convenient for scripts and tests, but in production code you will typically compose operations with Mutiny operators. +Here is the same WebClient call expressed reactively: + +[.tabs] +==== + +Mutiny Bindings:: ++ +-- + +[source,java] +---- +client.get("/") + .as(BodyCodec.string()) + .send() + .onItem().transform(HttpResponse::body) + .subscribe().with( + body -> System.out.println("Response: " + body), + failure -> System.err.println("Request failed: " + failure.getMessage()) + ); +---- + +-- + +Vanilla Vert.x:: ++ +-- + +[source,java] +---- +client.get("/") + .as(BodyCodec.string()) + .send() + .map(HttpResponse::body) + .onSuccess(body -> System.out.println("Response: " + body)) + .onFailure(err -> System.err.println("Request failed: " + err.getMessage())); +---- + +-- + +==== + +Note the use of `onItem().transform()` rather than shorthand aliases: this project favours explicit operator names so that the pipeline reads clearly. + +[TIP] +.Use `io.vertx.mutiny.*` imports +==== + +Always import classes from the `io.vertx.mutiny.++*++` packages, *not* from `io.vertx.core.++*++`. +The Mutiny classes are generated shims that wrap the Vert.x core classes and expose `Uni` / `Multi` return types instead of `Future`. + +==== + +== Next steps + +* xref:uni-and-multi.adoc[Uni and Multi]: learn how `Future` and `ReadStream` map to Mutiny types. +* xref:type-mapping.adoc[Type Mapping]: understand how Vert.x types are converted in the generated bindings. +* xref:error-handling.adoc[Error Handling]: handle failures and use Vert.x expectations with Mutiny. +* xref:available-modules.adoc[Available Modules]: see the full list of generated client modules. diff --git a/content/docs/smallrye-mutiny-vertx-bindings/latest/images/diagrams/mermaid-ccfeac935876.svg b/content/docs/smallrye-mutiny-vertx-bindings/latest/images/diagrams/mermaid-ccfeac935876.svg new file mode 100644 index 000000000..62bc07f57 --- /dev/null +++ b/content/docs/smallrye-mutiny-vertx-bindings/latest/images/diagrams/mermaid-ccfeac935876.svg @@ -0,0 +1 @@ +I/O operationUniSubscriberI/O operationUniSubscriberalt[success][failure]subscribe()start operationresultonItem(result)erroronFailure(error) \ No newline at end of file diff --git a/content/docs/smallrye-mutiny-vertx-bindings/latest/index.adoc b/content/docs/smallrye-mutiny-vertx-bindings/latest/index.adoc new file mode 100644 index 000000000..b989797ed --- /dev/null +++ b/content/docs/smallrye-mutiny-vertx-bindings/latest/index.adoc @@ -0,0 +1,100 @@ += Overview +include::_attributes.adoc[] + +SmallRye Mutiny Vert.x Bindings provides Vert.x client APIs using the https://smallrye.io/smallrye-mutiny/[Mutiny] reactive programming model. +Instead of working with Vert.x `Future` and callback-based APIs, you use `Uni` (for single results) and `Multi` (for streams of items) with a rich, explicit set of operators. +The bindings are generated automatically from the Vert.x source code, so they stay in sync with every Vert.x release. + +== Why Mutiny? + +* *Lazy evaluation*: a `Uni` does nothing until you subscribe, giving you full control over when side effects happen. +* *Explicit operator vocabulary*: methods like `onItem().transform()` and `onFailure().retry()` make the intent of each pipeline stage obvious. +* *Back-pressured streams*: `Multi` implements Reactive Streams, so producers cannot overwhelm consumers. +* *Blocking helpers for tests and virtual threads*: call `andAwait()` to block until a result is available, which is useful in unit tests and when running on virtual threads. + +== Quick Example + +The following example fetches JSON data from an HTTP endpoint. Compare the Mutiny bindings with the equivalent vanilla Vert.x code: + +[.tabs] +==== + +Mutiny Bindings:: ++ +-- + +[source,java] +---- +Uni fetchData(WebClient client) { + return client.get("/api/data") + .as(BodyCodec.jsonObject()) + .send() + .onItem().transform(HttpResponse::body) + .onFailure().retry().atMost(3) + .onFailure().invoke(err -> logger.error("Request failed", err)); +} +---- + +-- + +Vanilla Vert.x:: ++ +-- + +[source,java] +---- +Future fetchData(WebClient client, int retries) { + return client.get("/api/data") + .as(BodyCodec.jsonObject()) + .send() + .map(HttpResponse::body) + .recover(err -> { + logger.error("Request failed", err); + if (retries > 0) { + return fetchData(client, retries - 1); + } + return Future.failedFuture(err); + }); +} +---- + +-- + +==== + +With the Mutiny bindings, every step in the pipeline is visible: + +[arabic] +. `send()` returns a `Uni++<++HttpResponse++<++JsonObject++>>++`: nothing happens yet. +. `onItem().transform()` maps the response to its JSON body when the item arrives. +. `onFailure().retry().atMost(3)` retries the entire operation up to 3 times on failure. +. `onFailure().invoke()` logs errors that persist after retries, without swallowing them. + +Notice that retry logic requires no extra code with Mutiny — it is a built-in operator. With vanilla Vert.x, you need to manage a retry counter and use recursive `recover()` calls, as shown in the second tab. + +You can also block for the result when that is appropriate (tests, virtual threads): + +[source,java] +---- +JsonObject body = fetchData(client).await().indefinitely(); +---- + +[TIP] +.Explicit operators +==== + +Mutiny favours explicit names over short aliases. +Prefer `onItem().transform()` over `map`, `onItem().transformToUni()` over `flatMap`, and `onItem().invoke()` over a bare `invoke`. +The longer forms make pipelines easier to read and review. + +==== + +== Next Steps + +* xref:getting-started.adoc[Getting Started]: add the dependency and write your first Mutiny Vert.x program. +* xref:uni-and-multi.adoc[Uni and Multi]: learn the two core reactive types in depth. +* xref:type-mapping.adoc[Type Mapping]: understand how Vert.x types are converted to Mutiny types. +* xref:error-handling.adoc[Error Handling]: strategies for handling failures in reactive pipelines. +* xref:available-modules.adoc[Available Modules]: the full list of generated client modules. +* xref:migration-guide-3x-4x.adoc[Upgrading from version 3 to 4]: migrate to the new Mutiny bindings generator. +* xref:using-the-generator.adoc[Using the binding generator for your own APIs]: learn to use the generator for your own APIs. diff --git a/content/docs/smallrye-mutiny-vertx-bindings/latest/migration-guide-3x-4x.adoc b/content/docs/smallrye-mutiny-vertx-bindings/latest/migration-guide-3x-4x.adoc new file mode 100644 index 000000000..67bbe0eef --- /dev/null +++ b/content/docs/smallrye-mutiny-vertx-bindings/latest/migration-guide-3x-4x.adoc @@ -0,0 +1,110 @@ += Upgrading from version 3 to 4 +include::_attributes.adoc[] + +== Overview + +With the release of Vert.x 5, important API changes were made, sometimes breaking the compability with Vert.x 4.x projects. It was an opportunity to release a new version of the generator. This guide will walk through the changes between the Mutiny Vert.x bindings for Vert.x 4 and Vert.x 5. + +== A new Mutiny bindings generator + +In its 3.x version, the Vert.x-mutiny bindings generator was an annotation processor. It was executed on a given module by using the `maven-processor-plugin`. +As an annotation processor, it can only be run during maven compilation. Generating bindings thus requires compiling or recompiling the sources. +The new approach still uses annotations to detect which classes and interfaces must be generated, but annotation processing is now part of a full CLI possessing its own `Main` class and its own lifecycle independent from that of the sources. +This makes it easier to test and faster to run. + +== API changes + +Numerous API changes are due to the transition to Vert.x 5. +The Vert.x 4 to 5 migration guide is available at https://vertx.io/docs/guides/vertx-5-migration-guide/. + +We provide here a brief summary of the breaking changes, the exhaustive list can be found link:compatibility-report-3-to-4.html[in a compatibility report detailing every API change]. + +=== Generics are better specified + +In version 3 of the bindings generator, several generics were left unspecified. We changed this behavior so that every generics used is either correctly specified or uses a wildcard. +See the link:compatibility-report-3-to-4.html[full compatibility report] for more information. + +=== No more Callbacks + +Complete removal of callback methods such as: +`java void request(RequestOptions request, Handler++<++AsyncResult++<++HttpClientRequest++>>++ callback);` + +They have all been replaced with a Future such as: + +[source,java] +---- +Future request(RequestOptions request); +---- + +Consequently, the bindings generator only supports the `Future` style and will generate the following: + +[source,java] +---- +Uni request(RequestOptions request); +---- + +=== Deprecated modules are no more + +[width="100%",cols="22%,78%",options="header",] +|=== +|Sunsetting in 4.x |Replacement in 5.x +|Vert.x Sync |Vert.x virtual threads +|Service Factories |None +|Maven Service Factory |None +|HTTP Service Factory |None +|Vert.x Web OpenAPI |https://vertx.io/docs/vertx-web-openapi-router/java/[Vert.x Web OpenAPI Router] +|Vert.x CLI |None +|=== + +=== Changes with regards to `@DataObject` and `@VertxGen` + +Numerous classes are now annotated `@DataObject` instead of `@VertxGen`. `@DataObject` annotation existed in Vert.x 4.x and was used mainly for `++*++Options.java` classes. No wrappers are thus required, the core Vert.x interface is used directly. They will break existing projects as imports are now invalid. + +Below are some classes that are not converted to Mutiny shims anymore: + +* `io.vertx.core.buffer.Buffer` +* `io.vertx.core.net.Address` +* `io.vertx.core.net.SocketAddress` +* `io.vertx.core.parsetools.JsonEvent` +* `io.vertx.core.MultiMap` +* `io.vertx.core.datagram.DatagramPacket` + +=== SQL clients have been reworked + +In Vert.x 4, `Pool` subtypes were used for every kind of databases: `PgPool, MySQLPool, MSSQLPool`, etc. They have all been removed and replaced with client builder subtypes: + +[source,java] +---- +// 4.x +PgPool client = PgPool.pool(vertx, connectOptions, poolOptions); + +//5.0 +Pool client = PgBuilder.pool() + .with(poolOptions) + .connectingTo(connectOptions) + .using(vertx) + .build(); +---- + +For more information, see https://vertx.io/docs/guides/vertx-5-migration-guide/#_vert_x_sql_client[Vert.x 5 migration guide]. + +== Dropped or replaced modules + +[cols=",",options="header",] +|=== +|Module |Replacement +|service-discovery-++*++ |_dropped_ +|web-openapi |(openapi, web-openapi-router) +|web-auth-shiro |_dropped_ +|web-auth-webauthn |web-auth-webauthn4j +|auth-jdbc |_dropped_ +|jdbc-client |_dropped_ +|web-api-contract |_dropped_ +|web-templ-jade |_dropped_ +|stomp |_dropped_ +|shell |_dropped_ +|=== + +== Using the generator with your own asynchronous APIs + +xref:using-the-generator.adoc[Learn how to use the bindings generator with your own asynchronous APIs]. diff --git a/content/docs/smallrye-mutiny-vertx-bindings/latest/type-mapping.adoc b/content/docs/smallrye-mutiny-vertx-bindings/latest/type-mapping.adoc new file mode 100644 index 000000000..08fc123d1 --- /dev/null +++ b/content/docs/smallrye-mutiny-vertx-bindings/latest/type-mapping.adoc @@ -0,0 +1,100 @@ += Type Mapping +include::_attributes.adoc[] + +This page documents how Vert.x types are converted into Mutiny types by the code generator. +Understanding these mappings is essential for working with the generated bindings. + +== Overview + +The code generator parses Vert.x source code and produces Mutiny shim classes that wrap the original Vert.x APIs. +The core idea is to replace callback- and `Future`-based patterns with `Uni` and `Multi` from Mutiny. +Types that are already plain data (`JsonObject`, primitives, enums, data objects) pass through unchanged. + +== Type mapping table + +[width="100%",cols="34%,33%,33%",options="header",] +|=== +|Vert.x Type |Mutiny Type |Notes +|`Future++<++T++>++` |`Uni++<++T++>++` |Primary async conversion +|`ReadStream++<++T++>++` |`Multi++<++T++>++` |Via `toMulti()` on the shim +|`WriteStream++<++T++>++` |`Flow.Subscriber++<++T++>++` |Via `toSubscriber()` on the shim +|`Handler++<++AsyncResult++<++T++>>++` |_(removed)_ |Replaced by subscription model +|`Handler++<++T++>++` |`Consumer++<++T++>++` |Standard functional interface +|`Handler++<++Void++>++` |`Runnable` |No argument needed +|`Handler++<++Promise++<++T++>>++` |`Uni++<++T++>++` |Promise-based handlers become Unis +|`Consumer++<++Promise++<++T++>>++` |`Uni++<++T++>++` |Same treatment as Handler of Promise +|`@VertxGen` types |Shim in `io.vertx.mutiny.++*++` |Each interface gets a wrapper class +|`JsonObject`, `JsonArray` |Pass-through |Used as-is, no conversion +|Primitives, boxed types |Pass-through |`int`, `Integer`, `boolean`, etc. +|Data objects |Pass-through |Vert.x data objects are used directly +|Enums |Pass-through |Enum types are used directly +|`String` |Pass-through |Used as-is +|`Throwable` |Pass-through |Used as-is +|`List++<++T++>++`, `Set++<++T++>++` |Same collection, elements converted |If `T` is a `@VertxGen` type, each element is wrapped +|`Map++<++K, V++>++` |Same map, values converted |If `V` is a `@VertxGen` type, each value is wrapped +|=== + +== Shim classes + +Every Vert.x interface annotated with `@VertxGen` gets a corresponding *shim class* generated in the `io.vertx.mutiny.++*++` package. +The shim wraps the original Vert.x object (called the *delegate*) and exposes converted method signatures. + +Key properties of shim classes: + +* They implement `MutinyDelegate`, which provides the `getDelegate()` method. +* They live in `io.vertx.mutiny.++*++`, mirroring the original package structure (e.g. `io.vertx.core.eventbus.EventBus` becomes `io.vertx.mutiny.core.eventbus.EventBus`). +* They have a `newInstance()` static factory method used internally for wrapping delegates. +* Type parameters on the original interface are preserved in the shim. + +[source,java] +---- +// Original Vert.x interface +io.vertx.core.eventbus.EventBus + +// Generated Mutiny shim +io.vertx.mutiny.core.eventbus.EventBus + +// Accessing the underlying delegate +io.vertx.core.eventbus.EventBus delegate = mutinyEventBus.getDelegate(); +---- + +== Generated method variants + +For every Vert.x method that returns a `Future++<++T++>++`, the generator produces three method variants: the `Uni`-returning reactive method, a blocking `AndAwait` method, and a fire-and-forget `AndForget` method. + +See xref:uni-and-multi.adoc#the-three-method-variants[Uni and Multi — The three method variants] for the full table, return types, and guidance on when to use each variant. + +== ReadStream conversion + +When a `@VertxGen` interface extends `ReadStream++<++T++>++`, the generated shim gains three additional methods: + +[width="100%",cols="34%,33%,33%",options="header",] +|=== +|Method |Return type |Description +|`toMulti()` |`Multi++<++T++>++` |Converts the read stream into a Mutiny `Multi` +|`toBlockingIterable()` |`Iterable++<++T++>++` |Returns a blocking iterable backed by the `Multi` +|`toBlockingStream()` |`Stream++<++T++>++` |Returns a blocking Java `Stream` backed by the `Multi` +|=== + +The `toMulti()` method is lazy and cached: calling it multiple times returns the same `Multi` instance. +If `T` is itself a `@VertxGen` type, each emitted item is automatically wrapped in its shim class. + +== WriteStream conversion + +When a `@VertxGen` interface extends `WriteStream++<++T++>++`, the generated shim gains a `toSubscriber()` method: + +[width="100%",cols="34%,33%,33%",options="header",] +|=== +|Method |Return type |Description +|`toSubscriber()` |`Flow.Subscriber++<++T++>++` |Converts the write stream into a reactive `Flow.Subscriber` +|=== + +This lets you pipe a `Multi` into a Vert.x `WriteStream` using standard reactive streams back-pressure. + +== Collections of @VertxGen types + +When a method returns `Future++<++List++<++X++>>++`, `Future++<++Set++<++X++>>++`, or `Future++<++Map++<++K, V++>>++` where `X` or `V` is a `@VertxGen` type, the generator automatically wraps each element in its shim class. + +For example, a method returning `Future++<++List++<++io.vertx.core.eventbus.Message++<++T++>>>++` becomes `Uni++<++List++<++io.vertx.mutiny.core.eventbus.Message++<++T++>>>++`, and each `Message` in the list is wrapped in the Mutiny shim. + +The same applies to `Set` (using `Collectors.toSet()`) and `Map` values (using `Collectors.toMap()`). diff --git a/content/docs/smallrye-mutiny-vertx-bindings/latest/uni-and-multi.adoc b/content/docs/smallrye-mutiny-vertx-bindings/latest/uni-and-multi.adoc new file mode 100644 index 000000000..eb09ca6b3 --- /dev/null +++ b/content/docs/smallrye-mutiny-vertx-bindings/latest/uni-and-multi.adoc @@ -0,0 +1,348 @@ += Uni and Multi +include::_attributes.adoc[] + +The Mutiny Vert.x bindings replace Vert.x's `Future` and `ReadStream` types with Mutiny's two reactive types: `Uni` and `Multi`. +Understanding these types is essential to working with the bindings. + +== What is Uni? + +`Uni++<++T++>++` represents a *single asynchronous result*: it either emits one item of type `T` or a failure. +It is the Mutiny equivalent of Vert.x's `Future++<++T++>++`. + +A `Uni` is *lazy*: nothing happens until you subscribe. +Creating a `Uni` merely declares an asynchronous pipeline; the underlying I/O or computation only starts when a subscriber asks for it. + +image::images/diagrams/mermaid-ccfeac935876.svg[mermaid diagram] + +== What is Multi? + +`Multi++<++T++>++` represents an *asynchronous stream* of items: it can emit zero, one, or many items of type `T`, optionally followed by a completion or failure signal. +It is the Mutiny equivalent of Vert.x's `ReadStream++<++T++>++`. + +`Multi` supports *back-pressure*: a subscriber controls how many items it is ready to receive, preventing the producer from overwhelming it. + +== The three method variants + +For every Vert.x method that returns a `Future++<++T++>++`, the code generator produces three variants: + +[width="100%",cols="34%,33%,33%",options="header",] +|=== +|Variant |Signature |Behavior +|Reactive |`Uni++<++T++>++ method()` |Lazy. Returns a `Uni` that starts the operation on subscription. +|Blocking |`T methodAndAwait()` |Subscribes and blocks the calling thread until the result arrives. +|Fire-and-forget |`EnclosingType methodAndForget()` |Subscribes immediately, discards the result, and returns `this` for fluent chaining. +|=== + +For example, given a Vert.x method `Future++<++HttpServer++>++ listen(int port)` on the `HttpServer` class, the generator produces: + +[source,java] +---- +Uni listen(int port); // reactive +HttpServer listenAndAwait(int port); // blocking helper +HttpServer listenAndForget(int port); // fire-and-forget (returns this) +---- + +Use `methodAndAwait()` in tests and setup code where blocking is acceptable. +Use `methodAndForget()` for one-way operations where you do not need the result. +Prefer `Uni`-returning methods in production reactive pipelines. + +== Side-by-side comparison: event bus + +[.tabs] +==== + +Mutiny Bindings:: ++ +-- + +[source,java] +---- +EventBus bus = vertx.eventBus(); + +bus.consumer("address", message -> message.reply("world")) + .completionAndAwait(); + +bus.request("address", "hello") + .onItem().invoke(reply -> System.out.println(reply.body())) + .onFailure().invoke(Throwable::printStackTrace) + .await().atMost(Duration.ofSeconds(10)); +---- + +-- + +Vanilla Vert.x:: ++ +-- + +[source,java] +---- +EventBus bus = vertx.eventBus(); + +bus.consumer("address", message -> message.reply("world")); + +bus.request("address", "hello") + .onSuccess(reply -> System.out.println(reply.body())) + .onFailure(err -> err.printStackTrace()); +---- + +-- + +==== + +With the Mutiny bindings, `request` returns a `Uni++<++Message++<++Object++>>++`. +You compose over it with explicit operators, then subscribe: either by calling `.await()` or `.subscribe().with(...)`. + +== Multi from an event bus consumer + +A `MessageConsumer` can be turned into a `Multi` of messages. +Each message published or sent to the address becomes an item in the stream: + +[.tabs] +==== + +Mutiny Bindings:: ++ +-- + +[source,java] +---- +EventBus bus = vertx.eventBus(); + +bus.consumer("address").toMulti() + .onItem().transform(Message::body) + .subscribe().with(body -> System.out.println("Received: " + body)); + +bus.send("address", 1); +bus.send("address", 2); +bus.send("address", 3); +---- + +-- + +Vanilla Vert.x:: ++ +-- + +[source,java] +---- +EventBus bus = vertx.eventBus(); + +bus.consumer("address") + .handler(message -> System.out.println("Received: " + message.body())); + +bus.send("address", 1); +bus.send("address", 2); +bus.send("address", 3); +---- + +-- + +==== + +The `toMulti()` method bridges the Vert.x `ReadStream` into a Mutiny `Multi`. + +== Chaining Uni operations + +Mutiny encourages explicit operator names that describe what each step does. +Here is an HTTP client pipeline that chains several asynchronous steps: + +[.tabs] +==== + +Mutiny Bindings:: ++ +-- + +[source,java] +---- +Buffer payload = vertx.createHttpClient() + .request(HttpMethod.GET, port, "localhost", "/") + .onItem().transformToUni(HttpClientRequest::send) + .onItem().transformToUni(HttpClientResponse::body) + .await().atMost(Duration.ofSeconds(5)); +---- + +-- + +Vanilla Vert.x:: ++ +-- + +[source,java] +---- +Buffer payload = vertx.createHttpClient() + .request(HttpMethod.GET, port, "localhost", "/") + .compose(HttpClientRequest::send) + .compose(HttpClientResponse::body) + .toCompletionStage().toCompletableFuture() + .get(5, TimeUnit.SECONDS); +---- + +-- + +==== + +Each operator has a clear role: + +* `.onItem().transformToUni(HttpClientRequest::send)`: when the request object arrives, send it and return the response as a new `Uni`. +* `.onItem().transformToUni(HttpClientResponse::body)`: read the response body as another asynchronous step. +* `.await().atMost(Duration.ofSeconds(5))`: block and wait for the final result with a timeout. + +The key operators for chaining are: + +[width="100%",cols="50%,50%",options="header",] +|=== +|Operator |Purpose +|`.onItem().transform(fn)` |Synchronously transform the item (like `map`). +|`.onItem().transformToUni(fn)` |Asynchronously transform the item into another `Uni` (like `flatMap`). +|`.onItem().invoke(fn)` |Perform a side effect without changing the item. +|`.onItem().call(fn)` |Perform an asynchronous side effect (returns a `Uni++<++Void++>++`), then continue with the original item. +|`.onFailure().invoke(fn)` |Perform a side effect when a failure occurs. +|`.onFailure().recoverWithItem(fn)` |Replace a failure with a fallback item. +|=== + +== Subscription model + +A `Uni` does nothing until subscribed. +This is different from Vert.x's `Future`, which represents an already-started operation. + +This laziness gives you an important advantage: you can build up a pipeline, pass it around, and decide _when_ to trigger it. +Multiple subscriptions re-execute the pipeline each time, which is useful for retry logic and repeated operations. + +[.tabs] +==== + +Mutiny Bindings:: ++ +-- + +[source,java] +---- +// Build the pipeline once — nothing happens yet +Uni> pipeline = bus.request("address", "hello"); + +// Subscribe later, possibly multiple times +pipeline.subscribe().with( + reply -> System.out.println(reply.body()), + failure -> failure.printStackTrace() +); +---- + +-- + +Vanilla Vert.x:: ++ +-- + +[source,java] +---- +// The operation starts immediately — the Future is already in progress +Future> future = bus.request("address", "hello"); + +// Attach callbacks after the fact +future + .onSuccess(reply -> System.out.println(reply.body())) + .onFailure(Throwable::printStackTrace); +---- + +-- + +==== + +== ReadStream to Multi: WebSocket example + +Any Vert.x `ReadStream` is exposed as a `Multi` through the `toMulti()` method. +WebSocket is a practical example: incoming frames arrive as a stream of messages. + +*Server side*: handle each incoming WebSocket connection and read messages as a `Multi`: + +[.tabs] +==== + +Mutiny Bindings:: ++ +-- + +[source,java] +---- +vertx.createHttpServer(new HttpServerOptions().setIdleTimeout(2)) + .webSocketHandler(ws -> { + ws.toMulti() + .subscribe().with(msg -> { + System.out.println("Server received: " + msg); + ws.writeTextMessageAndForget("pong"); + }); + }) + .listenAndAwait(0, "localhost"); +---- + +-- + +Vanilla Vert.x:: ++ +-- + +[source,java] +---- +vertx.createHttpServer(new HttpServerOptions().setIdleTimeout(2)) + .webSocketHandler(ws -> { + ws.handler(msg -> { + System.out.println("Server received: " + msg); + ws.writeTextMessage("pong"); + }); + }) + .listen(0, "localhost") + .onSuccess(server -> System.out.println("Listening on " + server.actualPort())); +---- + +-- + +==== + +*Client side*: connect, send a message, then read responses as a `Multi`: + +[.tabs] +==== + +Mutiny Bindings:: ++ +-- + +[source,java] +---- +WebSocketClient client = vertx.createWebSocketClient(); + +client.connect(port, "localhost", "/") + .onItem().call(ws -> ws.writeTextMessage("ping")) + .onItem().transformToMulti(WebSocket::toMulti) + .subscribe().with(msg -> System.out.println("Client received: " + msg)); +---- + +-- + +Vanilla Vert.x:: ++ +-- + +[source,java] +---- +WebSocketClient client = vertx.createWebSocketClient(); + +client.connect(port, "localhost", "/") + .onSuccess(ws -> { + ws.handler(msg -> System.out.println("Client received: " + msg)); + ws.writeTextMessage("ping"); + }); +---- + +-- + +==== + +The client pipeline demonstrates a common pattern: + +[arabic] +. `connect(...)` returns a `Uni++<++WebSocket++>++`. +. `.onItem().call(...)` sends a message as an asynchronous side effect, then continues with the same `WebSocket`. +. `.onItem().transformToMulti(...)` switches from a single `Uni++<++WebSocket++>++` to a `Multi` of incoming messages. +. `.subscribe().with(...)` starts the whole pipeline. diff --git a/content/docs/smallrye-mutiny-vertx-bindings/latest/using-the-generator.adoc b/content/docs/smallrye-mutiny-vertx-bindings/latest/using-the-generator.adoc new file mode 100644 index 000000000..0538ffe59 --- /dev/null +++ b/content/docs/smallrye-mutiny-vertx-bindings/latest/using-the-generator.adoc @@ -0,0 +1,202 @@ += Using the Generator +include::_attributes.adoc[] + +The Mutiny code generator takes Vert.x source files and produces Mutiny shim classes that wrap the original Vert.x APIs with `Uni` and `Multi` return types. You point it at a directory of Vert.x sources, and it outputs ready-to-compile Java files in the `io.vertx.mutiny.++*++` package. + +== CLI Options + +[width="100%",cols="34%,33%,33%",options="header",] +|=== +|Option |Required |Description +|`--source ++<++path++>++` |Yes |Vert.x Java source directory +|`--output ++<++path++>++` |No |Output directory (dry run if omitted) +|`--additional-source ++<++path++>++` |No, repeatable |Additional sources for symbol resolution +|`--module-name ++<++name++>++` |No |Module name for the generated bindings +|=== + +== CLI Usage + +[source,bash] +---- +java -cp generator.jar io.smallrye.mutiny.vertx.apigenerator.Main \ + --source=path/to/vertx-sources \ + --output=path/to/output \ + --module-name=vertx-web-client +---- + +If `--output` is omitted the generator performs a dry run, parsing and resolving types without writing any files. + +== Maven Integration + +Most users run the generator as part of a Maven build. The pattern uses three plugins executed during the `generate-sources` phase. + +=== 1. Unpack Vert.x Sources + +The `maven-dependency-plugin` extracts Vert.x source JARs into `target/sources/java` so the generator can parse them. + +[source,xml] +---- + + maven-dependency-plugin + + io.vertx + vertx-web-client + sources + jar + + + + unpack-java + generate-sources + unpack-dependencies + + io/vertx/**/*.java + **/impl/**/*.java + ${project.build.directory}/sources/java + + + + + unpack-vertx-core + generate-sources + unpack-dependencies + + io/vertx/**/*.java + **/impl/**/*.java + vertx-core,vertx-auth-common + ${project.basedir}/target/additional-sources/vertx-core + + + + +---- + +[TIP] +.Additional sources +==== + +You might need to generate Mutiny bindings for modules `moduleA` and `moduleB`, where `moduleB` uses classes or interfaces from `moduleA`. +In this instance, you should add `moduleA` as additional sources to ensure that you are using the generated mutiny API for `moduleA` in `moduleB`, and not the original one. +This is easily achievable by setting the `maven-dependency-plugin` for `moduleB` like below. + +[source,xml] +---- + + maven-dependency-plugin + + org.acme + moduleB + sources + jar + + + + unpack-java + generate-sources + unpack-dependencies + + io/vertx/**/*.java + **/impl/**/*.java + ${project.build.directory}/sources/java + + + + + unpack-vertx-core + generate-sources + unpack-dependencies + + io/vertx/**/*.java + **/impl/**/*.java + vertx-core,moduleA + ${project.basedir}/target/additional-sources/vertx-core + + + + +---- + +==== + +=== 2. Run the Generator + +The `exec-maven-plugin` invokes the generator with the unpacked sources as input. + +[source,xml] +---- + + org.codehaus.mojo + exec-maven-plugin + + + generate + generate-sources + java + + io.smallrye.mutiny.vertx.apigenerator.Main + + --module-name=vertx-web-client + --source=${project.basedir}/target/sources + --output=${project.basedir}/target/generated-sources/codegen + --additional-source=${project.basedir}/target/additional-sources/vertx-core + + true + + + + + + io.smallrye.reactive + vertx-mutiny-code-generator + ${project.version} + + + +---- + +=== 3. Add Generated Sources to the Build + +The `build-helper-maven-plugin` adds the generated output directory as a source root so Maven compiles it alongside handwritten code. + +[source,xml] +---- + + org.codehaus.mojo + build-helper-maven-plugin + + + add-source + generate-sources + add-source + + + target/generated-sources/codegen + + + + + +---- + +== Cross-Module Type Resolution + +When a Vert.x module references types defined in another module (for example, `vertx-web-client` uses types from `vertx-core`), the generator needs access to those additional sources to resolve types correctly. Use `--additional-source` to provide them. This flag is repeatable: pass it once for each extra source tree: + +[source,bash] +---- +java -cp generator.jar io.smallrye.mutiny.vertx.apigenerator.Main \ + --source=target/sources \ + --output=target/generated-sources/codegen \ + --additional-source=target/additional-sources/vertx-core \ + --additional-source=target/additional-sources/vertx-auth-common +---- + +In the Maven setup, this corresponds to unpacking the additional module sources into separate directories (as shown in the optional execution above) and passing them via `++<++arg++>++` elements. + +== When You Need the Generator + +Most application developers consume the pre-built Mutiny bindings as Maven dependencies and never run the generator directly. You need it if you are: + +* *A project maintainer* rebuilding the bindings after a Vert.x upgrade. +* *A custom `@VertxGen` API author* who wants Mutiny bindings for your own Vert.x-style interfaces. +* *A Vert.x extension author* producing a new client module that follows the Vert.x codegen conventions. diff --git a/content/docs/smallrye-mutiny-vertx-bindings/main/_attributes.adoc b/content/docs/smallrye-mutiny-vertx-bindings/main/_attributes.adoc new file mode 100644 index 000000000..8e24634c1 --- /dev/null +++ b/content/docs/smallrye-mutiny-vertx-bindings/main/_attributes.adoc @@ -0,0 +1,20 @@ +:project-version: 4.0.2 +// Attributes shared by every page. smallrye.io overrides the ones it needs when it publishes. +// Pages in subdirectories set :version-root: (../ per level) before including this file. +ifndef::version-root[:version-root:] +:idprefix: +:idseparator: - +:sectanchors: +:examples-dir: {version-root}_examples +:partialsdir: {version-root}_partials +:project-name: Mutiny Vert.x bindings +:javadoc-version: latest +:generated-dir: {version-root}_generated + +// Added by smallrye.io (tools/docs/Docs.java sync) +:javadoc-version: 4.0.2 +:relfileprefix: /docs/smallrye-mutiny-vertx-bindings/main/ +:imagesdir: /docs/smallrye-mutiny-vertx-bindings/main +:docs-project: smallrye-mutiny-vertx-bindings +:docs-version: main +:docs-ref: main diff --git a/content/docs/smallrye-mutiny-vertx-bindings/main/_nav.yml b/content/docs/smallrye-mutiny-vertx-bindings/main/_nav.yml new file mode 100644 index 000000000..168665b5a --- /dev/null +++ b/content/docs/smallrye-mutiny-vertx-bindings/main/_nav.yml @@ -0,0 +1,20 @@ +title: Mutiny Vert.x bindings +nav: +- title: Overview + page: index +- title: Getting Started + page: getting-started +- title: Uni and Multi + page: uni-and-multi +- title: Type Mapping + page: type-mapping +- title: Error Handling + page: error-handling +- title: Blocking and Testing + page: blocking-and-testing +- title: Using the Generator + page: using-the-generator +- title: Available Modules + page: available-modules +- title: Upgrading from version 3 to 4 + page: migration-guide-3x-4x diff --git a/content/docs/smallrye-mutiny-vertx-bindings/main/available-modules.adoc b/content/docs/smallrye-mutiny-vertx-bindings/main/available-modules.adoc new file mode 100644 index 000000000..1b55df84f --- /dev/null +++ b/content/docs/smallrye-mutiny-vertx-bindings/main/available-modules.adoc @@ -0,0 +1,50 @@ += Available Modules +include::_attributes.adoc[] + +This project provides Mutiny bindings for a wide range of Vert.x modules: web, databases, messaging, authentication, templating, and more. + +== Naming Convention + +All modules share the same group ID and follow a predictable artifact ID pattern: + +* *Group ID*: `io.smallrye.reactive` +* *Artifact ID*: `smallrye-mutiny-vertx-++{++name}` + +The `++{++name}` suffix mirrors the Vert.x module name. +For example, the Vert.x `vertx-web-client` module becomes `smallrye-mutiny-vertx-web-client`. + +== Version Management + +Import the BOM to manage versions centrally. +See xref:getting-started.adoc#adding-the-bom[Getting Started — Adding the BOM] for the dependency management snippet. + +== Finding the Right Module + +To find the Mutiny binding for a Vert.x module, take the Vert.x artifact ID and prefix it with `smallrye-mutiny-`. +For example, the Vert.x `vertx-pg-client` module becomes `smallrye-mutiny-vertx-pg-client`: + +[source,xml] +---- + + io.smallrye.reactive + smallrye-mutiny-vertx-pg-client + +---- + +This pattern applies consistently across all modules. + +== Imports + +When using a Mutiny binding, import from the `io.vertx.mutiny.++*++` packages rather than `io.vertx.core.++*++` or `io.vertx.ext.++*++`. +The Mutiny classes are generated shims that wrap the original Vert.x types. + +[source,java] +---- +// Correct: Mutiny shim +import io.vertx.mutiny.ext.web.client.WebClient; + +// Wrong: original Vert.x type +import io.vertx.ext.web.client.WebClient; +---- + +Configuration and options classes (e.g., `WebClientOptions`) are data objects and are used directly from the Vert.x packages since they do not need shims. diff --git a/content/docs/smallrye-mutiny-vertx-bindings/main/blocking-and-testing.adoc b/content/docs/smallrye-mutiny-vertx-bindings/main/blocking-and-testing.adoc new file mode 100644 index 000000000..5e2e8b435 --- /dev/null +++ b/content/docs/smallrye-mutiny-vertx-bindings/main/blocking-and-testing.adoc @@ -0,0 +1,325 @@ += Blocking and Testing +include::_attributes.adoc[] + +The Mutiny Vert.x bindings provide blocking APIs that convert asynchronous `Uni` and `Multi` results into synchronous calls. These APIs are essential for testing and are equally valuable for imperative-style programming on virtual threads, worker threads, and CLI tools. + +*Never block on the Vert.x event loop.* Blocking the event loop freezes the entire application. Use these APIs only in contexts where blocking is safe: + +* *Virtual threads* (Java 21{plus}): first-class imperative programming with Vert.x clients +* *Worker threads*: offloaded via `executeBlocking` +* *Tests*: JUnit test methods run outside the event loop +* *CLI tools and scripts*: short-lived programs without an event loop + +== Virtual Threads: Imperative Vert.x + +With Java 21{plus} virtual threads, the blocking APIs let you write Vert.x client code in a straightforward imperative style. Virtual threads can block without wasting platform threads, making `andAwait()` and `.await().atMost()` practical outside of tests. + +For example, a web client call written imperatively: + +[.tabs] +==== + +Mutiny Bindings:: ++ +-- + +[source,java] +---- +WebClient client = WebClient.create(vertx, new WebClientOptions() + .setDefaultPort(8080) + .setDefaultHost("localhost")); + +HttpResponse response = client.get("/api/users") + .as(BodyCodec.jsonObject()) + .sendAndAwait(); + +JsonObject body = response.body(); +---- + +-- + +Vanilla Vert.x:: ++ +-- + +[source,java] +---- +WebClient client = WebClient.create(vertx, new WebClientOptions() + .setDefaultPort(8080) + .setDefaultHost("localhost")); + +HttpResponse response = client.get("/api/users") + .as(BodyCodec.jsonObject()) + .send() + .toCompletionStage().toCompletableFuture().join(); + +JsonObject body = response.body(); +---- + +-- + +==== + +No callbacks, no reactive chains: just sequential code that reads top to bottom. Each `andAwait()` call suspends the virtual thread until the result is available, while the underlying Vert.x event loop continues processing other work. + +== andAwait() + +Methods ending in `andAwait()` block the calling thread indefinitely until the operation completes. Every `Uni`-returning method on a Mutiny Vert.x class has a corresponding `andAwait()` variant. + +[.tabs] +==== + +Mutiny Bindings:: ++ +-- + +[source,java] +---- +EventBus bus = vertx.eventBus(); +bus.consumer("address", message -> message.reply("world")) + .completionAndAwait(); + +Message reply = bus.requestAndAwait("address", "hello"); +---- + +-- + +Vanilla Vert.x:: ++ +-- + +[source,java] +---- +EventBus bus = vertx.eventBus(); +bus.consumer("address", message -> message.reply("world")) + .completion() + .toCompletionStage().toCompletableFuture().join(); + +Message reply = bus.request("address", "hello") + .toCompletionStage().toCompletableFuture().join(); +---- + +-- + +==== + +Use `andAwait()` when you are confident the operation will complete and do not need a timeout: for instance, in tests with controlled infrastructure or on virtual threads where indefinite suspension is acceptable. + +== .await().atMost(Duration) + +For operations that may hang or where you want a safety net, use `.await().atMost(Duration)` on the `Uni` directly. This blocks until the result arrives or the timeout expires, throwing a `TimeoutException` if the deadline is exceeded. + +[.tabs] +==== + +Mutiny Bindings:: ++ +-- + +[source,java] +---- +Buffer payload = vertx.createHttpClient() + .request(HttpMethod.GET, port, "localhost", "/") + .onItem().transformToUni(HttpClientRequest::send) + .onItem().transformToUni(HttpClientResponse::body) + .await().atMost(Duration.ofSeconds(5)); +---- + +-- + +Vanilla Vert.x:: ++ +-- + +[source,java] +---- +Buffer payload = vertx.createHttpClient() + .request(HttpMethod.GET, port, "localhost", "/") + .compose(HttpClientRequest::send) + .compose(HttpClientResponse::body) + .toCompletionStage().toCompletableFuture() + .get(5, TimeUnit.SECONDS); +---- + +-- + +==== + +This is the preferred approach when calling external services that might not respond. + +== Blocking Iteration on Multi + +For `Multi` streams, two methods convert the asynchronous stream into a blocking iterator or Java stream: + +* `toBlockingIterable()`: returns an `Iterable` that blocks on each element +* `toBlockingStream()`: returns a `java.util.stream.Stream` that blocks on each element + +[.tabs] +==== + +Mutiny Bindings:: ++ +-- + +[source,java] +---- +AsyncFile asyncFile = vertx.fileSystem() + .openAndAwait(path, new OpenOptions()); + +asyncFile.toBlockingStream() + .forEach(buffer::appendBuffer); +---- + +-- + +Vanilla Vert.x:: ++ +-- + +[source,java] +---- +AsyncFile asyncFile = vertx.fileSystem() + .open(path, new OpenOptions()) + .toCompletionStage().toCompletableFuture().join(); + +asyncFile.handler(chunk -> buffer.appendBuffer(chunk)); +---- + +-- + +==== + +These are useful for consuming a reactive stream in imperative code, such as reading a file or draining a message consumer. + +== JUnit 5 Testing Pattern + +A standard pattern for testing Mutiny Vert.x code with JUnit 5: + +[.tabs] +==== + +Mutiny Bindings:: ++ +-- + +[source,java] +---- +class WebClientTest { + + Vertx vertx; + + @BeforeEach + void setUp() { + vertx = Vertx.vertx(); + } + + @AfterEach + void tearDown() { + vertx.closeAndAwait(); + } + + @Test + void getJsonObject() { + // ... set up a test HTTP server ... + + WebClient client = WebClient.create(vertx, new WebClientOptions() + .setDefaultPort(server.actualPort()) + .setDefaultHost("localhost")); + + HttpResponse response = client.get("/yo") + .as(BodyCodec.jsonObject()) + .sendAndAwait(); + + assertThat(response.statusCode()).isEqualTo(200); + assertThat(response.body().getString("msg")).isEqualTo("hello"); + } +} +---- + +-- + +Vanilla Vert.x:: ++ +-- + +[source,java] +---- +@ExtendWith(VertxExtension.class) +class WebClientTest { + + @Test + void getJsonObject(Vertx vertx, VertxTestContext testContext) { + // ... set up a test HTTP server ... + + WebClient client = WebClient.create(vertx, new WebClientOptions() + .setDefaultPort(server.actualPort()) + .setDefaultHost("localhost")); + + client.get("/yo") + .as(BodyCodec.jsonObject()) + .send() + .onComplete(testContext.succeeding(response -> { + testContext.verify(() -> { + assertThat(response.statusCode()).isEqualTo(200); + assertThat(response.body().getString("msg")).isEqualTo("hello"); + }); + testContext.completeNow(); + })); + } +} +---- + +-- + +==== + +Key points: + +* Create a fresh `Vertx` instance in `@BeforeEach` to isolate tests. +* Shut it down with `closeAndAwait()` in `@AfterEach` to release resources. +* Use `andAwait()` or `.await().atMost()` in the test body: JUnit test threads are not event loop threads, so blocking is safe. + +With the Mutiny bindings, tests read sequentially with no callbacks. With vanilla Vert.x, you need `VertxExtension`, `VertxTestContext`, and nested `succeeding` / `verify` callbacks to handle the asynchronous result. + +== executeBlocking for CPU-Intensive Work + +When you need to run CPU-intensive or legacy blocking code from within a Vert.x context, use `vertx.executeBlocking()`. This offloads work to a worker thread pool, returning a result asynchronously. + +[.tabs] +==== + +Mutiny Bindings:: ++ +-- + +[source,java] +---- +Uni uni = vertx.executeBlocking(() -> { + // Runs on a worker thread — blocking calls are safe here + return expensiveComputation(); +}); + +String result = uni.await().indefinitely(); +---- + +-- + +Vanilla Vert.x:: ++ +-- + +[source,java] +---- +Future future = vertx.executeBlocking(() -> { + // Runs on a worker thread — blocking calls are safe here + return expensiveComputation(); +}); + +String result = future.toCompletionStage().toCompletableFuture().join(); +---- + +-- + +==== + +Inside the `executeBlocking` callable, you are on a worker thread, so blocking calls are safe. The Mutiny variant returns a `Uni` that can be composed into a larger reactive pipeline or awaited directly. diff --git a/content/docs/smallrye-mutiny-vertx-bindings/main/error-handling.adoc b/content/docs/smallrye-mutiny-vertx-bindings/main/error-handling.adoc new file mode 100644 index 000000000..fa5da0685 --- /dev/null +++ b/content/docs/smallrye-mutiny-vertx-bindings/main/error-handling.adoc @@ -0,0 +1,435 @@ += Error Handling +include::_attributes.adoc[] + +One advantage of using Mutiny with Vert.x is that errors propagate through the reactive pipeline automatically. +With the callback or `Future`-based API you must manually check for failures at each step. +With Mutiny, failures flow downstream until something handles them, so you focus on the success path and deal with errors once: + +[.tabs] +==== + +Mutiny Bindings:: ++ +-- + +[source,java] +---- +// Errors propagate automatically +client.request(HttpMethod.GET, "/data") + .onItem().transformToUni(HttpClientRequest::send) + .onItem().transformToUni(HttpClientResponse::body) + .subscribe().with( + body -> System.out.println("Got: " + body), + failure -> System.err.println("Failed: " + failure.getMessage()) + ); +---- + +-- + +Vanilla Vert.x:: ++ +-- + +[source,java] +---- +// Manual failure checks at each step +client.request(HttpMethod.GET, "/data") + .compose(request -> request.send()) + .compose(response -> response.body()) + .onSuccess(body -> System.out.println("Got: " + body)) + .onFailure(err -> System.err.println("Failed: " + err.getMessage())); +---- + +-- + +==== + +If any step in the pipeline fails, the remaining `onItem()` operators are skipped and the failure handler receives the error. With vanilla Vert.x, `compose()` chains behave similarly — a failure in any stage short-circuits to the `onFailure` handler — but the Mutiny API makes the distinction between item and failure handling more explicit through its operator vocabulary. + +== Basic failure handling + +The simplest way to handle errors is to provide both a success and failure callback to `subscribe().with()`: + +[.tabs] +==== + +Mutiny Bindings:: ++ +-- + +[source,java] +---- +uni.subscribe().with( + item -> System.out.println("Received: " + item), + failure -> System.err.println("Error: " + failure.getMessage()) +); +---- + +-- + +Vanilla Vert.x:: ++ +-- + +[source,java] +---- +future + .onSuccess(item -> System.out.println("Received: " + item)) + .onFailure(err -> System.err.println("Error: " + err.getMessage())); +---- + +-- + +==== + +If you omit the failure callback, unhandled failures are reported to Mutiny's global failure handler. + +== Recovery + +You can recover from failures inline rather than letting them propagate to the subscriber. + +=== Recover with a fallback item + +[.tabs] +==== + +Mutiny Bindings:: ++ +-- + +[source,java] +---- +Uni result = uni + .onFailure().recoverWithItem("default value"); +---- + +-- + +Vanilla Vert.x:: ++ +-- + +[source,java] +---- +Future result = future + .recover(err -> Future.succeededFuture("default value")); +---- + +-- + +==== + +=== Recover with another Uni + +When recovery itself requires an asynchronous operation, use `recoverWithUni`: + +[.tabs] +==== + +Mutiny Bindings:: ++ +-- + +[source,java] +---- +Uni result = uni + .onFailure().recoverWithUni(() -> fetchFromFallbackService()); +---- + +-- + +Vanilla Vert.x:: ++ +-- + +[source,java] +---- +Future result = future + .recover(err -> fetchFromFallbackService()); +---- + +-- + +==== + +=== Recover from specific exceptions + +Both recovery methods accept a predicate or exception class to recover selectively: + +[.tabs] +==== + +Mutiny Bindings:: ++ +-- + +[source,java] +---- +Uni result = uni + .onFailure(TimeoutException.class).recoverWithItem("timed out") + .onFailure(IOException.class).recoverWithUni(() -> retryElsewhere()); +---- + +-- + +Vanilla Vert.x:: ++ +-- + +[source,java] +---- +Future result = future + .recover(err -> { + if (err instanceof TimeoutException) { + return Future.succeededFuture("timed out"); + } + if (err instanceof IOException) { + return retryElsewhere(); + } + return Future.failedFuture(err); + }); +---- + +-- + +==== + +With Mutiny, selective recovery is declarative — you chain `.onFailure(ExceptionType.class)` for each case. With vanilla Vert.x, you use a single `recover()` block and check types with `instanceof`. + +== Retries + +Mutiny provides built-in retry support: + +[.tabs] +==== + +Mutiny Bindings:: ++ +-- + +[source,java] +---- +Uni result = uni + .onFailure().retry().atMost(3); +---- + +-- + +Vanilla Vert.x:: ++ +-- + +[source,java] +---- +Future fetchWithRetry(int retries) { + return doFetch() + .recover(err -> { + if (retries > 0) { + return fetchWithRetry(retries - 1); + } + return Future.failedFuture(err); + }); +} + +Future result = fetchWithRetry(3); +---- + +-- + +==== + +This retries the upstream operation up to 3 times on any failure. +With vanilla Vert.x, retries require a recursive method that calls itself from within a `recover()` block. + +For production use, add exponential backoff to avoid overwhelming a failing service: + +[.tabs] +==== + +Mutiny Bindings:: ++ +-- + +[source,java] +---- +Uni result = uni + .onFailure().retry() + .withBackOff(Duration.ofMillis(100), Duration.ofSeconds(5)) + .atMost(5); +---- + +-- + +Vanilla Vert.x:: ++ +-- + +[source,java] +---- +Future fetchWithBackoff(int retries, long delayMs) { + return doFetch() + .recover(err -> { + if (retries > 0) { + Promise promise = Promise.promise(); + long nextDelay = Math.min(delayMs * 2, 5000); + vertx.setTimer(delayMs, id -> + fetchWithBackoff(retries - 1, nextDelay) + .onComplete(promise)); + return promise.future(); + } + return Future.failedFuture(err); + }); +} + +Future result = fetchWithBackoff(5, 100); +---- + +-- + +==== + +This retries up to 5 times with an initial delay of 100ms that increases exponentially up to a maximum of 5 seconds between attempts. +With vanilla Vert.x, exponential backoff requires managing a `Promise`, a timer via `vertx.setTimer()`, and manual delay calculation — all of which Mutiny handles as a single operator chain. + +== Failure transformation + +You can transform a failure into a different exception type, which is useful for wrapping low-level errors into domain-specific ones: + +[.tabs] +==== + +Mutiny Bindings:: ++ +-- + +[source,java] +---- +Uni result = uni + .onFailure().transform(err -> new MyServiceException("Operation failed", err)); +---- + +-- + +Vanilla Vert.x:: ++ +-- + +[source,java] +---- +Future result = future + .recover(err -> Future.failedFuture( + new MyServiceException("Operation failed", err))); +---- + +-- + +==== + +== Vert.x expectations with Mutiny + +Vert.x provides an `Expectation` interface that works as a predicate on resolved `Future` values. +Pre-defined expectations such as `HttpResponseExpectation.status(200)` are convenient for validating HTTP responses. + +Since the Mutiny bindings convert `Future`-returning methods into `Uni`-returning methods, you cannot use expectations directly. +Instead, the `io.smallrye.mutiny.vertx.core.Expectations` helper class adapts them for use with `Uni.plug()`. + +=== Simple expectation + +[.tabs] +==== + +Mutiny Bindings:: ++ +-- + +[source,java] +---- +import static io.smallrye.mutiny.vertx.core.Expectations.expectation; + +Expectation tenToTwenty = (value -> value >= 10 && value <= 20); + +Uni.createFrom().item(15) + .plug(expectation(tenToTwenty)); +---- + +-- + +Vanilla Vert.x:: ++ +-- + +[source,java] +---- +Expectation tenToTwenty = (value -> value >= 10 && value <= 20); + +Future.succeededFuture(15) + .expecting(tenToTwenty); +---- + +-- + +==== + +If the value does not satisfy the expectation, the `Uni` fails (or the `Future` fails in vanilla Vert.x). +For example, passing `42` instead of `15` would produce a failure with the message: `Unexpected result: 42`. + +=== HTTP response expectations + +Vert.x ships pre-defined expectations for HTTP responses, such as `status()` and `contentType()`. +These expectations are typed against core Vert.x types (e.g., `io.vertx.core.http.HttpResponseHead`), not against the Mutiny shim types (e.g., `io.vertx.mutiny.core.http.HttpResponseHead`). + +To bridge this gap, the `expectation` helper accepts an extractor function as its first argument. +Passing `HttpClientResponse::getDelegate` unwraps the Mutiny shim so the expectation receives the underlying Vert.x type: + +[.tabs] +==== + +Mutiny Bindings:: ++ +-- + +[source,java] +---- +import static io.smallrye.mutiny.vertx.core.Expectations.expectation; +import static io.vertx.core.http.HttpResponseExpectation.contentType; +import static io.vertx.core.http.HttpResponseExpectation.status; + +vertx.createHttpClient() + .request(HttpMethod.GET, port, "localhost", "/") + .onItem().transformToUni(HttpClientRequest::send) + .plug(expectation(HttpClientResponse::getDelegate, status(200).and(contentType("text/plain")))) + .onItem().transformToUni(HttpClientResponse::body) + .await().atMost(Duration.ofSeconds(5)); +---- + +-- + +Vanilla Vert.x:: ++ +-- + +[source,java] +---- +import static io.vertx.core.http.HttpResponseExpectation.contentType; +import static io.vertx.core.http.HttpResponseExpectation.status; + +vertx.createHttpClient() + .request(HttpMethod.GET, port, "localhost", "/") + .compose(HttpClientRequest::send) + .expecting(status(200).and(contentType("text/plain"))) + .compose(HttpClientResponse::body) + .toCompletionStage().toCompletableFuture() + .get(5, TimeUnit.SECONDS); +---- + +-- + +==== + +In this example: + +* `HttpClientResponse::getDelegate` extracts the core Vert.x response from the Mutiny shim. With vanilla Vert.x, no unwrapping is needed since you work directly with the core types. +* `status(200).and(contentType("text/plain"))` combines two expectations: the response must have status 200 and content type `text/plain`. +* If either expectation fails, the `Uni` pipeline (or the `Future` chain) fails with a descriptive error. + +You can combine any number of expectations using `.and()`. diff --git a/content/docs/smallrye-mutiny-vertx-bindings/main/getting-started.adoc b/content/docs/smallrye-mutiny-vertx-bindings/main/getting-started.adoc new file mode 100644 index 000000000..178be01e5 --- /dev/null +++ b/content/docs/smallrye-mutiny-vertx-bindings/main/getting-started.adoc @@ -0,0 +1,218 @@ += Getting Started +include::_attributes.adoc[] + +This guide walks you through adding SmallRye Mutiny Vert.x Bindings to your project and writing a first reactive application. + +== Prerequisites + +* *Java 17* or later +* *Maven 3.9* or later + +== Adding the BOM + +The simplest way to manage dependency versions is to import the BOM (Bill of Materials) in your `pom.xml`: + +[source,xml] +---- + + + + io.smallrye.reactive + vertx-mutiny-clients-bom + ${mutiny-vertx.version} + pom + import + + + +---- + +Replace `$++{++mutiny-vertx.version}` with the version you want to use, or define it as a Maven property. + +== Adding module dependencies + +With the BOM imported you can add individual modules without specifying a version. +At a minimum you need the core module: + +[source,xml] +---- + + io.smallrye.reactive + smallrye-mutiny-vertx-core + +---- + +If you also want to use the reactive HTTP client (WebClient), add: + +[source,xml] +---- + + io.smallrye.reactive + smallrye-mutiny-vertx-web-client + +---- + +== A complete example + +The following program starts an HTTP server that replies with a greeting and then uses WebClient to call it: + +[.tabs] +==== + +Mutiny Bindings:: ++ +-- + +[source,java] +---- +import io.vertx.mutiny.core.Vertx; +import io.vertx.mutiny.core.http.HttpServer; +import io.vertx.mutiny.ext.web.client.WebClient; +import io.vertx.ext.web.client.WebClientOptions; +import io.vertx.mutiny.ext.web.client.HttpResponse; +import io.vertx.mutiny.ext.web.codec.BodyCodec; + +public class GettingStarted { + + public static void main(String[] args) { + Vertx vertx = Vertx.vertx(); + + HttpServer server = vertx.createHttpServer() + .requestHandler(req -> req.response() + .setStatusCode(200) + .putHeader("content-type", "text/plain") + .endAndForget("Hello from Mutiny!")) + .listenAndAwait(8080); + + System.out.println("Server listening on port " + server.actualPort()); + + WebClient client = WebClient.create(vertx, new WebClientOptions() + .setDefaultPort(8080) + .setDefaultHost("localhost")); + + HttpResponse response = client.get("/") + .as(BodyCodec.string()) + .sendAndAwait(); + + System.out.println("Response: " + response.body()); + + vertx.closeAndAwait(); + } +} +---- + +-- + +Vanilla Vert.x:: ++ +-- + +[source,java] +---- +import io.vertx.core.Vertx; +import io.vertx.ext.web.client.WebClient; +import io.vertx.ext.web.client.WebClientOptions; +import io.vertx.ext.web.codec.BodyCodec; + +public class GettingStarted { + + public static void main(String[] args) { + Vertx vertx = Vertx.vertx(); + + vertx.createHttpServer() + .requestHandler(req -> req.response() + .setStatusCode(200) + .putHeader("content-type", "text/plain") + .end("Hello from Vert.x!")) + .listen(8080) + .compose(server -> { + System.out.println("Server listening on port " + server.actualPort()); + + WebClient client = WebClient.create(vertx, new WebClientOptions() + .setDefaultPort(8080) + .setDefaultHost("localhost")); + + return client.get("/") + .as(BodyCodec.string()) + .send(); + }) + .onSuccess(response -> + System.out.println("Response: " + response.body())) + .onComplete(v -> vertx.close()); + } +} +---- + +-- + +==== + +=== What is happening here + +[arabic] +. `Vertx.vertx()` creates a Mutiny-based Vert.x instance. +. `listenAndAwait(8080)` starts the server and blocks the calling thread until the server is ready. Under the hood it subscribes to the `Uni++<++HttpServer++>++` returned by `listen()` and awaits its result. +. `sendAndAwait()` sends the HTTP request and blocks until the response arrives: again a convenience wrapper around a `Uni`. +. `closeAndAwait()` shuts down the Vert.x instance. + +=== Using explicit reactive operators + +The `++*++AndAwait()` helpers are convenient for scripts and tests, but in production code you will typically compose operations with Mutiny operators. +Here is the same WebClient call expressed reactively: + +[.tabs] +==== + +Mutiny Bindings:: ++ +-- + +[source,java] +---- +client.get("/") + .as(BodyCodec.string()) + .send() + .onItem().transform(HttpResponse::body) + .subscribe().with( + body -> System.out.println("Response: " + body), + failure -> System.err.println("Request failed: " + failure.getMessage()) + ); +---- + +-- + +Vanilla Vert.x:: ++ +-- + +[source,java] +---- +client.get("/") + .as(BodyCodec.string()) + .send() + .map(HttpResponse::body) + .onSuccess(body -> System.out.println("Response: " + body)) + .onFailure(err -> System.err.println("Request failed: " + err.getMessage())); +---- + +-- + +==== + +Note the use of `onItem().transform()` rather than shorthand aliases: this project favours explicit operator names so that the pipeline reads clearly. + +[TIP] +.Use `io.vertx.mutiny.*` imports +==== + +Always import classes from the `io.vertx.mutiny.++*++` packages, *not* from `io.vertx.core.++*++`. +The Mutiny classes are generated shims that wrap the Vert.x core classes and expose `Uni` / `Multi` return types instead of `Future`. + +==== + +== Next steps + +* xref:uni-and-multi.adoc[Uni and Multi]: learn how `Future` and `ReadStream` map to Mutiny types. +* xref:type-mapping.adoc[Type Mapping]: understand how Vert.x types are converted in the generated bindings. +* xref:error-handling.adoc[Error Handling]: handle failures and use Vert.x expectations with Mutiny. +* xref:available-modules.adoc[Available Modules]: see the full list of generated client modules. diff --git a/content/docs/smallrye-mutiny-vertx-bindings/main/images/diagrams/mermaid-ccfeac935876.svg b/content/docs/smallrye-mutiny-vertx-bindings/main/images/diagrams/mermaid-ccfeac935876.svg new file mode 100644 index 000000000..62bc07f57 --- /dev/null +++ b/content/docs/smallrye-mutiny-vertx-bindings/main/images/diagrams/mermaid-ccfeac935876.svg @@ -0,0 +1 @@ +I/O operationUniSubscriberI/O operationUniSubscriberalt[success][failure]subscribe()start operationresultonItem(result)erroronFailure(error) \ No newline at end of file diff --git a/content/docs/smallrye-mutiny-vertx-bindings/main/index.adoc b/content/docs/smallrye-mutiny-vertx-bindings/main/index.adoc new file mode 100644 index 000000000..b989797ed --- /dev/null +++ b/content/docs/smallrye-mutiny-vertx-bindings/main/index.adoc @@ -0,0 +1,100 @@ += Overview +include::_attributes.adoc[] + +SmallRye Mutiny Vert.x Bindings provides Vert.x client APIs using the https://smallrye.io/smallrye-mutiny/[Mutiny] reactive programming model. +Instead of working with Vert.x `Future` and callback-based APIs, you use `Uni` (for single results) and `Multi` (for streams of items) with a rich, explicit set of operators. +The bindings are generated automatically from the Vert.x source code, so they stay in sync with every Vert.x release. + +== Why Mutiny? + +* *Lazy evaluation*: a `Uni` does nothing until you subscribe, giving you full control over when side effects happen. +* *Explicit operator vocabulary*: methods like `onItem().transform()` and `onFailure().retry()` make the intent of each pipeline stage obvious. +* *Back-pressured streams*: `Multi` implements Reactive Streams, so producers cannot overwhelm consumers. +* *Blocking helpers for tests and virtual threads*: call `andAwait()` to block until a result is available, which is useful in unit tests and when running on virtual threads. + +== Quick Example + +The following example fetches JSON data from an HTTP endpoint. Compare the Mutiny bindings with the equivalent vanilla Vert.x code: + +[.tabs] +==== + +Mutiny Bindings:: ++ +-- + +[source,java] +---- +Uni fetchData(WebClient client) { + return client.get("/api/data") + .as(BodyCodec.jsonObject()) + .send() + .onItem().transform(HttpResponse::body) + .onFailure().retry().atMost(3) + .onFailure().invoke(err -> logger.error("Request failed", err)); +} +---- + +-- + +Vanilla Vert.x:: ++ +-- + +[source,java] +---- +Future fetchData(WebClient client, int retries) { + return client.get("/api/data") + .as(BodyCodec.jsonObject()) + .send() + .map(HttpResponse::body) + .recover(err -> { + logger.error("Request failed", err); + if (retries > 0) { + return fetchData(client, retries - 1); + } + return Future.failedFuture(err); + }); +} +---- + +-- + +==== + +With the Mutiny bindings, every step in the pipeline is visible: + +[arabic] +. `send()` returns a `Uni++<++HttpResponse++<++JsonObject++>>++`: nothing happens yet. +. `onItem().transform()` maps the response to its JSON body when the item arrives. +. `onFailure().retry().atMost(3)` retries the entire operation up to 3 times on failure. +. `onFailure().invoke()` logs errors that persist after retries, without swallowing them. + +Notice that retry logic requires no extra code with Mutiny — it is a built-in operator. With vanilla Vert.x, you need to manage a retry counter and use recursive `recover()` calls, as shown in the second tab. + +You can also block for the result when that is appropriate (tests, virtual threads): + +[source,java] +---- +JsonObject body = fetchData(client).await().indefinitely(); +---- + +[TIP] +.Explicit operators +==== + +Mutiny favours explicit names over short aliases. +Prefer `onItem().transform()` over `map`, `onItem().transformToUni()` over `flatMap`, and `onItem().invoke()` over a bare `invoke`. +The longer forms make pipelines easier to read and review. + +==== + +== Next Steps + +* xref:getting-started.adoc[Getting Started]: add the dependency and write your first Mutiny Vert.x program. +* xref:uni-and-multi.adoc[Uni and Multi]: learn the two core reactive types in depth. +* xref:type-mapping.adoc[Type Mapping]: understand how Vert.x types are converted to Mutiny types. +* xref:error-handling.adoc[Error Handling]: strategies for handling failures in reactive pipelines. +* xref:available-modules.adoc[Available Modules]: the full list of generated client modules. +* xref:migration-guide-3x-4x.adoc[Upgrading from version 3 to 4]: migrate to the new Mutiny bindings generator. +* xref:using-the-generator.adoc[Using the binding generator for your own APIs]: learn to use the generator for your own APIs. diff --git a/content/docs/smallrye-mutiny-vertx-bindings/main/migration-guide-3x-4x.adoc b/content/docs/smallrye-mutiny-vertx-bindings/main/migration-guide-3x-4x.adoc new file mode 100644 index 000000000..67bbe0eef --- /dev/null +++ b/content/docs/smallrye-mutiny-vertx-bindings/main/migration-guide-3x-4x.adoc @@ -0,0 +1,110 @@ += Upgrading from version 3 to 4 +include::_attributes.adoc[] + +== Overview + +With the release of Vert.x 5, important API changes were made, sometimes breaking the compability with Vert.x 4.x projects. It was an opportunity to release a new version of the generator. This guide will walk through the changes between the Mutiny Vert.x bindings for Vert.x 4 and Vert.x 5. + +== A new Mutiny bindings generator + +In its 3.x version, the Vert.x-mutiny bindings generator was an annotation processor. It was executed on a given module by using the `maven-processor-plugin`. +As an annotation processor, it can only be run during maven compilation. Generating bindings thus requires compiling or recompiling the sources. +The new approach still uses annotations to detect which classes and interfaces must be generated, but annotation processing is now part of a full CLI possessing its own `Main` class and its own lifecycle independent from that of the sources. +This makes it easier to test and faster to run. + +== API changes + +Numerous API changes are due to the transition to Vert.x 5. +The Vert.x 4 to 5 migration guide is available at https://vertx.io/docs/guides/vertx-5-migration-guide/. + +We provide here a brief summary of the breaking changes, the exhaustive list can be found link:compatibility-report-3-to-4.html[in a compatibility report detailing every API change]. + +=== Generics are better specified + +In version 3 of the bindings generator, several generics were left unspecified. We changed this behavior so that every generics used is either correctly specified or uses a wildcard. +See the link:compatibility-report-3-to-4.html[full compatibility report] for more information. + +=== No more Callbacks + +Complete removal of callback methods such as: +`java void request(RequestOptions request, Handler++<++AsyncResult++<++HttpClientRequest++>>++ callback);` + +They have all been replaced with a Future such as: + +[source,java] +---- +Future request(RequestOptions request); +---- + +Consequently, the bindings generator only supports the `Future` style and will generate the following: + +[source,java] +---- +Uni request(RequestOptions request); +---- + +=== Deprecated modules are no more + +[width="100%",cols="22%,78%",options="header",] +|=== +|Sunsetting in 4.x |Replacement in 5.x +|Vert.x Sync |Vert.x virtual threads +|Service Factories |None +|Maven Service Factory |None +|HTTP Service Factory |None +|Vert.x Web OpenAPI |https://vertx.io/docs/vertx-web-openapi-router/java/[Vert.x Web OpenAPI Router] +|Vert.x CLI |None +|=== + +=== Changes with regards to `@DataObject` and `@VertxGen` + +Numerous classes are now annotated `@DataObject` instead of `@VertxGen`. `@DataObject` annotation existed in Vert.x 4.x and was used mainly for `++*++Options.java` classes. No wrappers are thus required, the core Vert.x interface is used directly. They will break existing projects as imports are now invalid. + +Below are some classes that are not converted to Mutiny shims anymore: + +* `io.vertx.core.buffer.Buffer` +* `io.vertx.core.net.Address` +* `io.vertx.core.net.SocketAddress` +* `io.vertx.core.parsetools.JsonEvent` +* `io.vertx.core.MultiMap` +* `io.vertx.core.datagram.DatagramPacket` + +=== SQL clients have been reworked + +In Vert.x 4, `Pool` subtypes were used for every kind of databases: `PgPool, MySQLPool, MSSQLPool`, etc. They have all been removed and replaced with client builder subtypes: + +[source,java] +---- +// 4.x +PgPool client = PgPool.pool(vertx, connectOptions, poolOptions); + +//5.0 +Pool client = PgBuilder.pool() + .with(poolOptions) + .connectingTo(connectOptions) + .using(vertx) + .build(); +---- + +For more information, see https://vertx.io/docs/guides/vertx-5-migration-guide/#_vert_x_sql_client[Vert.x 5 migration guide]. + +== Dropped or replaced modules + +[cols=",",options="header",] +|=== +|Module |Replacement +|service-discovery-++*++ |_dropped_ +|web-openapi |(openapi, web-openapi-router) +|web-auth-shiro |_dropped_ +|web-auth-webauthn |web-auth-webauthn4j +|auth-jdbc |_dropped_ +|jdbc-client |_dropped_ +|web-api-contract |_dropped_ +|web-templ-jade |_dropped_ +|stomp |_dropped_ +|shell |_dropped_ +|=== + +== Using the generator with your own asynchronous APIs + +xref:using-the-generator.adoc[Learn how to use the bindings generator with your own asynchronous APIs]. diff --git a/content/docs/smallrye-mutiny-vertx-bindings/main/type-mapping.adoc b/content/docs/smallrye-mutiny-vertx-bindings/main/type-mapping.adoc new file mode 100644 index 000000000..08fc123d1 --- /dev/null +++ b/content/docs/smallrye-mutiny-vertx-bindings/main/type-mapping.adoc @@ -0,0 +1,100 @@ += Type Mapping +include::_attributes.adoc[] + +This page documents how Vert.x types are converted into Mutiny types by the code generator. +Understanding these mappings is essential for working with the generated bindings. + +== Overview + +The code generator parses Vert.x source code and produces Mutiny shim classes that wrap the original Vert.x APIs. +The core idea is to replace callback- and `Future`-based patterns with `Uni` and `Multi` from Mutiny. +Types that are already plain data (`JsonObject`, primitives, enums, data objects) pass through unchanged. + +== Type mapping table + +[width="100%",cols="34%,33%,33%",options="header",] +|=== +|Vert.x Type |Mutiny Type |Notes +|`Future++<++T++>++` |`Uni++<++T++>++` |Primary async conversion +|`ReadStream++<++T++>++` |`Multi++<++T++>++` |Via `toMulti()` on the shim +|`WriteStream++<++T++>++` |`Flow.Subscriber++<++T++>++` |Via `toSubscriber()` on the shim +|`Handler++<++AsyncResult++<++T++>>++` |_(removed)_ |Replaced by subscription model +|`Handler++<++T++>++` |`Consumer++<++T++>++` |Standard functional interface +|`Handler++<++Void++>++` |`Runnable` |No argument needed +|`Handler++<++Promise++<++T++>>++` |`Uni++<++T++>++` |Promise-based handlers become Unis +|`Consumer++<++Promise++<++T++>>++` |`Uni++<++T++>++` |Same treatment as Handler of Promise +|`@VertxGen` types |Shim in `io.vertx.mutiny.++*++` |Each interface gets a wrapper class +|`JsonObject`, `JsonArray` |Pass-through |Used as-is, no conversion +|Primitives, boxed types |Pass-through |`int`, `Integer`, `boolean`, etc. +|Data objects |Pass-through |Vert.x data objects are used directly +|Enums |Pass-through |Enum types are used directly +|`String` |Pass-through |Used as-is +|`Throwable` |Pass-through |Used as-is +|`List++<++T++>++`, `Set++<++T++>++` |Same collection, elements converted |If `T` is a `@VertxGen` type, each element is wrapped +|`Map++<++K, V++>++` |Same map, values converted |If `V` is a `@VertxGen` type, each value is wrapped +|=== + +== Shim classes + +Every Vert.x interface annotated with `@VertxGen` gets a corresponding *shim class* generated in the `io.vertx.mutiny.++*++` package. +The shim wraps the original Vert.x object (called the *delegate*) and exposes converted method signatures. + +Key properties of shim classes: + +* They implement `MutinyDelegate`, which provides the `getDelegate()` method. +* They live in `io.vertx.mutiny.++*++`, mirroring the original package structure (e.g. `io.vertx.core.eventbus.EventBus` becomes `io.vertx.mutiny.core.eventbus.EventBus`). +* They have a `newInstance()` static factory method used internally for wrapping delegates. +* Type parameters on the original interface are preserved in the shim. + +[source,java] +---- +// Original Vert.x interface +io.vertx.core.eventbus.EventBus + +// Generated Mutiny shim +io.vertx.mutiny.core.eventbus.EventBus + +// Accessing the underlying delegate +io.vertx.core.eventbus.EventBus delegate = mutinyEventBus.getDelegate(); +---- + +== Generated method variants + +For every Vert.x method that returns a `Future++<++T++>++`, the generator produces three method variants: the `Uni`-returning reactive method, a blocking `AndAwait` method, and a fire-and-forget `AndForget` method. + +See xref:uni-and-multi.adoc#the-three-method-variants[Uni and Multi — The three method variants] for the full table, return types, and guidance on when to use each variant. + +== ReadStream conversion + +When a `@VertxGen` interface extends `ReadStream++<++T++>++`, the generated shim gains three additional methods: + +[width="100%",cols="34%,33%,33%",options="header",] +|=== +|Method |Return type |Description +|`toMulti()` |`Multi++<++T++>++` |Converts the read stream into a Mutiny `Multi` +|`toBlockingIterable()` |`Iterable++<++T++>++` |Returns a blocking iterable backed by the `Multi` +|`toBlockingStream()` |`Stream++<++T++>++` |Returns a blocking Java `Stream` backed by the `Multi` +|=== + +The `toMulti()` method is lazy and cached: calling it multiple times returns the same `Multi` instance. +If `T` is itself a `@VertxGen` type, each emitted item is automatically wrapped in its shim class. + +== WriteStream conversion + +When a `@VertxGen` interface extends `WriteStream++<++T++>++`, the generated shim gains a `toSubscriber()` method: + +[width="100%",cols="34%,33%,33%",options="header",] +|=== +|Method |Return type |Description +|`toSubscriber()` |`Flow.Subscriber++<++T++>++` |Converts the write stream into a reactive `Flow.Subscriber` +|=== + +This lets you pipe a `Multi` into a Vert.x `WriteStream` using standard reactive streams back-pressure. + +== Collections of @VertxGen types + +When a method returns `Future++<++List++<++X++>>++`, `Future++<++Set++<++X++>>++`, or `Future++<++Map++<++K, V++>>++` where `X` or `V` is a `@VertxGen` type, the generator automatically wraps each element in its shim class. + +For example, a method returning `Future++<++List++<++io.vertx.core.eventbus.Message++<++T++>>>++` becomes `Uni++<++List++<++io.vertx.mutiny.core.eventbus.Message++<++T++>>>++`, and each `Message` in the list is wrapped in the Mutiny shim. + +The same applies to `Set` (using `Collectors.toSet()`) and `Map` values (using `Collectors.toMap()`). diff --git a/content/docs/smallrye-mutiny-vertx-bindings/main/uni-and-multi.adoc b/content/docs/smallrye-mutiny-vertx-bindings/main/uni-and-multi.adoc new file mode 100644 index 000000000..eb09ca6b3 --- /dev/null +++ b/content/docs/smallrye-mutiny-vertx-bindings/main/uni-and-multi.adoc @@ -0,0 +1,348 @@ += Uni and Multi +include::_attributes.adoc[] + +The Mutiny Vert.x bindings replace Vert.x's `Future` and `ReadStream` types with Mutiny's two reactive types: `Uni` and `Multi`. +Understanding these types is essential to working with the bindings. + +== What is Uni? + +`Uni++<++T++>++` represents a *single asynchronous result*: it either emits one item of type `T` or a failure. +It is the Mutiny equivalent of Vert.x's `Future++<++T++>++`. + +A `Uni` is *lazy*: nothing happens until you subscribe. +Creating a `Uni` merely declares an asynchronous pipeline; the underlying I/O or computation only starts when a subscriber asks for it. + +image::images/diagrams/mermaid-ccfeac935876.svg[mermaid diagram] + +== What is Multi? + +`Multi++<++T++>++` represents an *asynchronous stream* of items: it can emit zero, one, or many items of type `T`, optionally followed by a completion or failure signal. +It is the Mutiny equivalent of Vert.x's `ReadStream++<++T++>++`. + +`Multi` supports *back-pressure*: a subscriber controls how many items it is ready to receive, preventing the producer from overwhelming it. + +== The three method variants + +For every Vert.x method that returns a `Future++<++T++>++`, the code generator produces three variants: + +[width="100%",cols="34%,33%,33%",options="header",] +|=== +|Variant |Signature |Behavior +|Reactive |`Uni++<++T++>++ method()` |Lazy. Returns a `Uni` that starts the operation on subscription. +|Blocking |`T methodAndAwait()` |Subscribes and blocks the calling thread until the result arrives. +|Fire-and-forget |`EnclosingType methodAndForget()` |Subscribes immediately, discards the result, and returns `this` for fluent chaining. +|=== + +For example, given a Vert.x method `Future++<++HttpServer++>++ listen(int port)` on the `HttpServer` class, the generator produces: + +[source,java] +---- +Uni listen(int port); // reactive +HttpServer listenAndAwait(int port); // blocking helper +HttpServer listenAndForget(int port); // fire-and-forget (returns this) +---- + +Use `methodAndAwait()` in tests and setup code where blocking is acceptable. +Use `methodAndForget()` for one-way operations where you do not need the result. +Prefer `Uni`-returning methods in production reactive pipelines. + +== Side-by-side comparison: event bus + +[.tabs] +==== + +Mutiny Bindings:: ++ +-- + +[source,java] +---- +EventBus bus = vertx.eventBus(); + +bus.consumer("address", message -> message.reply("world")) + .completionAndAwait(); + +bus.request("address", "hello") + .onItem().invoke(reply -> System.out.println(reply.body())) + .onFailure().invoke(Throwable::printStackTrace) + .await().atMost(Duration.ofSeconds(10)); +---- + +-- + +Vanilla Vert.x:: ++ +-- + +[source,java] +---- +EventBus bus = vertx.eventBus(); + +bus.consumer("address", message -> message.reply("world")); + +bus.request("address", "hello") + .onSuccess(reply -> System.out.println(reply.body())) + .onFailure(err -> err.printStackTrace()); +---- + +-- + +==== + +With the Mutiny bindings, `request` returns a `Uni++<++Message++<++Object++>>++`. +You compose over it with explicit operators, then subscribe: either by calling `.await()` or `.subscribe().with(...)`. + +== Multi from an event bus consumer + +A `MessageConsumer` can be turned into a `Multi` of messages. +Each message published or sent to the address becomes an item in the stream: + +[.tabs] +==== + +Mutiny Bindings:: ++ +-- + +[source,java] +---- +EventBus bus = vertx.eventBus(); + +bus.consumer("address").toMulti() + .onItem().transform(Message::body) + .subscribe().with(body -> System.out.println("Received: " + body)); + +bus.send("address", 1); +bus.send("address", 2); +bus.send("address", 3); +---- + +-- + +Vanilla Vert.x:: ++ +-- + +[source,java] +---- +EventBus bus = vertx.eventBus(); + +bus.consumer("address") + .handler(message -> System.out.println("Received: " + message.body())); + +bus.send("address", 1); +bus.send("address", 2); +bus.send("address", 3); +---- + +-- + +==== + +The `toMulti()` method bridges the Vert.x `ReadStream` into a Mutiny `Multi`. + +== Chaining Uni operations + +Mutiny encourages explicit operator names that describe what each step does. +Here is an HTTP client pipeline that chains several asynchronous steps: + +[.tabs] +==== + +Mutiny Bindings:: ++ +-- + +[source,java] +---- +Buffer payload = vertx.createHttpClient() + .request(HttpMethod.GET, port, "localhost", "/") + .onItem().transformToUni(HttpClientRequest::send) + .onItem().transformToUni(HttpClientResponse::body) + .await().atMost(Duration.ofSeconds(5)); +---- + +-- + +Vanilla Vert.x:: ++ +-- + +[source,java] +---- +Buffer payload = vertx.createHttpClient() + .request(HttpMethod.GET, port, "localhost", "/") + .compose(HttpClientRequest::send) + .compose(HttpClientResponse::body) + .toCompletionStage().toCompletableFuture() + .get(5, TimeUnit.SECONDS); +---- + +-- + +==== + +Each operator has a clear role: + +* `.onItem().transformToUni(HttpClientRequest::send)`: when the request object arrives, send it and return the response as a new `Uni`. +* `.onItem().transformToUni(HttpClientResponse::body)`: read the response body as another asynchronous step. +* `.await().atMost(Duration.ofSeconds(5))`: block and wait for the final result with a timeout. + +The key operators for chaining are: + +[width="100%",cols="50%,50%",options="header",] +|=== +|Operator |Purpose +|`.onItem().transform(fn)` |Synchronously transform the item (like `map`). +|`.onItem().transformToUni(fn)` |Asynchronously transform the item into another `Uni` (like `flatMap`). +|`.onItem().invoke(fn)` |Perform a side effect without changing the item. +|`.onItem().call(fn)` |Perform an asynchronous side effect (returns a `Uni++<++Void++>++`), then continue with the original item. +|`.onFailure().invoke(fn)` |Perform a side effect when a failure occurs. +|`.onFailure().recoverWithItem(fn)` |Replace a failure with a fallback item. +|=== + +== Subscription model + +A `Uni` does nothing until subscribed. +This is different from Vert.x's `Future`, which represents an already-started operation. + +This laziness gives you an important advantage: you can build up a pipeline, pass it around, and decide _when_ to trigger it. +Multiple subscriptions re-execute the pipeline each time, which is useful for retry logic and repeated operations. + +[.tabs] +==== + +Mutiny Bindings:: ++ +-- + +[source,java] +---- +// Build the pipeline once — nothing happens yet +Uni> pipeline = bus.request("address", "hello"); + +// Subscribe later, possibly multiple times +pipeline.subscribe().with( + reply -> System.out.println(reply.body()), + failure -> failure.printStackTrace() +); +---- + +-- + +Vanilla Vert.x:: ++ +-- + +[source,java] +---- +// The operation starts immediately — the Future is already in progress +Future> future = bus.request("address", "hello"); + +// Attach callbacks after the fact +future + .onSuccess(reply -> System.out.println(reply.body())) + .onFailure(Throwable::printStackTrace); +---- + +-- + +==== + +== ReadStream to Multi: WebSocket example + +Any Vert.x `ReadStream` is exposed as a `Multi` through the `toMulti()` method. +WebSocket is a practical example: incoming frames arrive as a stream of messages. + +*Server side*: handle each incoming WebSocket connection and read messages as a `Multi`: + +[.tabs] +==== + +Mutiny Bindings:: ++ +-- + +[source,java] +---- +vertx.createHttpServer(new HttpServerOptions().setIdleTimeout(2)) + .webSocketHandler(ws -> { + ws.toMulti() + .subscribe().with(msg -> { + System.out.println("Server received: " + msg); + ws.writeTextMessageAndForget("pong"); + }); + }) + .listenAndAwait(0, "localhost"); +---- + +-- + +Vanilla Vert.x:: ++ +-- + +[source,java] +---- +vertx.createHttpServer(new HttpServerOptions().setIdleTimeout(2)) + .webSocketHandler(ws -> { + ws.handler(msg -> { + System.out.println("Server received: " + msg); + ws.writeTextMessage("pong"); + }); + }) + .listen(0, "localhost") + .onSuccess(server -> System.out.println("Listening on " + server.actualPort())); +---- + +-- + +==== + +*Client side*: connect, send a message, then read responses as a `Multi`: + +[.tabs] +==== + +Mutiny Bindings:: ++ +-- + +[source,java] +---- +WebSocketClient client = vertx.createWebSocketClient(); + +client.connect(port, "localhost", "/") + .onItem().call(ws -> ws.writeTextMessage("ping")) + .onItem().transformToMulti(WebSocket::toMulti) + .subscribe().with(msg -> System.out.println("Client received: " + msg)); +---- + +-- + +Vanilla Vert.x:: ++ +-- + +[source,java] +---- +WebSocketClient client = vertx.createWebSocketClient(); + +client.connect(port, "localhost", "/") + .onSuccess(ws -> { + ws.handler(msg -> System.out.println("Client received: " + msg)); + ws.writeTextMessage("ping"); + }); +---- + +-- + +==== + +The client pipeline demonstrates a common pattern: + +[arabic] +. `connect(...)` returns a `Uni++<++WebSocket++>++`. +. `.onItem().call(...)` sends a message as an asynchronous side effect, then continues with the same `WebSocket`. +. `.onItem().transformToMulti(...)` switches from a single `Uni++<++WebSocket++>++` to a `Multi` of incoming messages. +. `.subscribe().with(...)` starts the whole pipeline. diff --git a/content/docs/smallrye-mutiny-vertx-bindings/main/using-the-generator.adoc b/content/docs/smallrye-mutiny-vertx-bindings/main/using-the-generator.adoc new file mode 100644 index 000000000..0538ffe59 --- /dev/null +++ b/content/docs/smallrye-mutiny-vertx-bindings/main/using-the-generator.adoc @@ -0,0 +1,202 @@ += Using the Generator +include::_attributes.adoc[] + +The Mutiny code generator takes Vert.x source files and produces Mutiny shim classes that wrap the original Vert.x APIs with `Uni` and `Multi` return types. You point it at a directory of Vert.x sources, and it outputs ready-to-compile Java files in the `io.vertx.mutiny.++*++` package. + +== CLI Options + +[width="100%",cols="34%,33%,33%",options="header",] +|=== +|Option |Required |Description +|`--source ++<++path++>++` |Yes |Vert.x Java source directory +|`--output ++<++path++>++` |No |Output directory (dry run if omitted) +|`--additional-source ++<++path++>++` |No, repeatable |Additional sources for symbol resolution +|`--module-name ++<++name++>++` |No |Module name for the generated bindings +|=== + +== CLI Usage + +[source,bash] +---- +java -cp generator.jar io.smallrye.mutiny.vertx.apigenerator.Main \ + --source=path/to/vertx-sources \ + --output=path/to/output \ + --module-name=vertx-web-client +---- + +If `--output` is omitted the generator performs a dry run, parsing and resolving types without writing any files. + +== Maven Integration + +Most users run the generator as part of a Maven build. The pattern uses three plugins executed during the `generate-sources` phase. + +=== 1. Unpack Vert.x Sources + +The `maven-dependency-plugin` extracts Vert.x source JARs into `target/sources/java` so the generator can parse them. + +[source,xml] +---- + + maven-dependency-plugin + + io.vertx + vertx-web-client + sources + jar + + + + unpack-java + generate-sources + unpack-dependencies + + io/vertx/**/*.java + **/impl/**/*.java + ${project.build.directory}/sources/java + + + + + unpack-vertx-core + generate-sources + unpack-dependencies + + io/vertx/**/*.java + **/impl/**/*.java + vertx-core,vertx-auth-common + ${project.basedir}/target/additional-sources/vertx-core + + + + +---- + +[TIP] +.Additional sources +==== + +You might need to generate Mutiny bindings for modules `moduleA` and `moduleB`, where `moduleB` uses classes or interfaces from `moduleA`. +In this instance, you should add `moduleA` as additional sources to ensure that you are using the generated mutiny API for `moduleA` in `moduleB`, and not the original one. +This is easily achievable by setting the `maven-dependency-plugin` for `moduleB` like below. + +[source,xml] +---- + + maven-dependency-plugin + + org.acme + moduleB + sources + jar + + + + unpack-java + generate-sources + unpack-dependencies + + io/vertx/**/*.java + **/impl/**/*.java + ${project.build.directory}/sources/java + + + + + unpack-vertx-core + generate-sources + unpack-dependencies + + io/vertx/**/*.java + **/impl/**/*.java + vertx-core,moduleA + ${project.basedir}/target/additional-sources/vertx-core + + + + +---- + +==== + +=== 2. Run the Generator + +The `exec-maven-plugin` invokes the generator with the unpacked sources as input. + +[source,xml] +---- + + org.codehaus.mojo + exec-maven-plugin + + + generate + generate-sources + java + + io.smallrye.mutiny.vertx.apigenerator.Main + + --module-name=vertx-web-client + --source=${project.basedir}/target/sources + --output=${project.basedir}/target/generated-sources/codegen + --additional-source=${project.basedir}/target/additional-sources/vertx-core + + true + + + + + + io.smallrye.reactive + vertx-mutiny-code-generator + ${project.version} + + + +---- + +=== 3. Add Generated Sources to the Build + +The `build-helper-maven-plugin` adds the generated output directory as a source root so Maven compiles it alongside handwritten code. + +[source,xml] +---- + + org.codehaus.mojo + build-helper-maven-plugin + + + add-source + generate-sources + add-source + + + target/generated-sources/codegen + + + + + +---- + +== Cross-Module Type Resolution + +When a Vert.x module references types defined in another module (for example, `vertx-web-client` uses types from `vertx-core`), the generator needs access to those additional sources to resolve types correctly. Use `--additional-source` to provide them. This flag is repeatable: pass it once for each extra source tree: + +[source,bash] +---- +java -cp generator.jar io.smallrye.mutiny.vertx.apigenerator.Main \ + --source=target/sources \ + --output=target/generated-sources/codegen \ + --additional-source=target/additional-sources/vertx-core \ + --additional-source=target/additional-sources/vertx-auth-common +---- + +In the Maven setup, this corresponds to unpacking the additional module sources into separate directories (as shown in the optional execution above) and passing them via `++<++arg++>++` elements. + +== When You Need the Generator + +Most application developers consume the pre-built Mutiny bindings as Maven dependencies and never run the generator directly. You need it if you are: + +* *A project maintainer* rebuilding the bindings after a Vert.x upgrade. +* *A custom `@VertxGen` API author* who wants Mutiny bindings for your own Vert.x-style interfaces. +* *A Vert.x extension author* producing a new client module that follows the Vert.x codegen conventions. diff --git a/content/docs/smallrye-mutiny-zero/0/_attributes.adoc b/content/docs/smallrye-mutiny-zero/0/_attributes.adoc new file mode 100644 index 000000000..45cae6f9f --- /dev/null +++ b/content/docs/smallrye-mutiny-zero/0/_attributes.adoc @@ -0,0 +1,20 @@ +:project-version: 0.5.0 +// Attributes shared by every page. smallrye.io overrides the ones it needs when it publishes. +// Pages in subdirectories set :version-root: (../ per level) before including this file. +ifndef::version-root[:version-root:] +:idprefix: +:idseparator: - +:sectanchors: +:examples-dir: {version-root}_examples +:partialsdir: {version-root}_partials +:project-name: Mutiny Zero +:javadoc-version: latest +:generated-dir: {version-root}_generated + +// Added by smallrye.io (tools/docs/Docs.java sync) +:javadoc-version: 0.5.0 +:relfileprefix: /docs/smallrye-mutiny-zero/0/ +:imagesdir: /docs/smallrye-mutiny-zero/0 +:docs-project: smallrye-mutiny-zero +:docs-version: 0 +:docs-ref: 0.5.0 diff --git a/content/docs/smallrye-mutiny-zero/0/_examples/mutiny-zero-vertx-publishers/src/test/java/docsamples/UrbanObservatoryHttpClient.java b/content/docs/smallrye-mutiny-zero/0/_examples/mutiny-zero-vertx-publishers/src/test/java/docsamples/UrbanObservatoryHttpClient.java new file mode 100644 index 000000000..3d7ddf2f7 --- /dev/null +++ b/content/docs/smallrye-mutiny-zero/0/_examples/mutiny-zero-vertx-publishers/src/test/java/docsamples/UrbanObservatoryHttpClient.java @@ -0,0 +1,67 @@ +package docsamples; + +import java.nio.charset.StandardCharsets; +import java.util.concurrent.Executors; +import java.util.concurrent.Flow; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.HttpClient; +import io.vertx.core.http.HttpClientRequest; +import io.vertx.core.http.HttpMethod; +import io.vertx.core.http.RequestOptions; +import mutiny.zero.vertxpublishers.VertxPublisher; + +public class UrbanObservatoryHttpClient { + + public static void main(String[] args) { + + Vertx vertx = Vertx.vertx(); + ScheduledExecutorService executor = Executors.newScheduledThreadPool(1); + + RequestOptions opts = new RequestOptions() + .setSsl(true) + .setHost("api.usb.urbanobservatory.ac.uk") + .setPort(443) + .setMethod(HttpMethod.GET) + .addHeader("Accept", "application/json") + .setURI("/api/v2.0a/sensors/entity"); + + HttpClient httpClient = vertx.createHttpClient(); + + Flow.Publisher publisher = VertxPublisher.fromFuture(() -> httpClient + .request(opts) + .compose(HttpClientRequest::send)); + + publisher.subscribe(new Flow.Subscriber<>() { + + private Flow.Subscription subscription; + + @Override + public void onSubscribe(Flow.Subscription s) { + System.out.println("======================================"); + this.subscription = s; + s.request(1L); + } + + @Override + public void onNext(Buffer buffer) { + System.out.print(buffer.toString(StandardCharsets.UTF_8)); + executor.schedule(() -> subscription.request(1L), 500, TimeUnit.MILLISECONDS); + } + + @Override + public void onError(Throwable t) { + System.out.println("======================================"); + t.printStackTrace(); + } + + @Override + public void onComplete() { + System.out.println("======================================"); + } + }); + } +} diff --git a/content/docs/smallrye-mutiny-zero/0/_examples/mutiny-zero/src/test/java/docsamples/FromCompletionStage.java b/content/docs/smallrye-mutiny-zero/0/_examples/mutiny-zero/src/test/java/docsamples/FromCompletionStage.java new file mode 100644 index 000000000..62b3cc273 --- /dev/null +++ b/content/docs/smallrye-mutiny-zero/0/_examples/mutiny-zero/src/test/java/docsamples/FromCompletionStage.java @@ -0,0 +1,14 @@ +package docsamples; + +import static java.util.concurrent.CompletableFuture.supplyAsync; +import static mutiny.zero.ZeroPublisher.fromCompletionStage; + +import java.util.concurrent.Flow; + +public class FromCompletionStage { + + public static void main(String[] args) { + + Flow.Publisher pun = fromCompletionStage(() -> supplyAsync(() -> 58L)); + } +} diff --git a/content/docs/smallrye-mutiny-zero/0/_examples/mutiny-zero/src/test/java/docsamples/FromKnownValues.java b/content/docs/smallrye-mutiny-zero/0/_examples/mutiny-zero/src/test/java/docsamples/FromKnownValues.java new file mode 100644 index 000000000..0b14665f3 --- /dev/null +++ b/content/docs/smallrye-mutiny-zero/0/_examples/mutiny-zero/src/test/java/docsamples/FromKnownValues.java @@ -0,0 +1,22 @@ +package docsamples; + +import static mutiny.zero.ZeroPublisher.*; + +import java.io.IOException; +import java.util.Arrays; +import java.util.concurrent.Flow.Publisher; + +public class FromKnownValues { + + public static void main(String[] args) { + + // From values + Publisher pub1 = fromItems(1, 2, 3); + + // From an iterable + Publisher pub2 = fromIterable(Arrays.asList(1, 2, 3)); + + // From a failure + Publisher pub3 = fromFailure(new IOException("Broken pipe")); + } +} diff --git a/content/docs/smallrye-mutiny-zero/0/_examples/mutiny-zero/src/test/java/docsamples/FromTube.java b/content/docs/smallrye-mutiny-zero/0/_examples/mutiny-zero/src/test/java/docsamples/FromTube.java new file mode 100644 index 000000000..6a78a2cb8 --- /dev/null +++ b/content/docs/smallrye-mutiny-zero/0/_examples/mutiny-zero/src/test/java/docsamples/FromTube.java @@ -0,0 +1,48 @@ +package docsamples; + +import static mutiny.zero.ZeroPublisher.create; + +import java.util.concurrent.Flow.Publisher; + +import mutiny.zero.BackpressureStrategy; +import mutiny.zero.TubeConfiguration; + +public class FromTube { + + public static void main(String[] args) { + + SampleAsyncSource source = new SampleAsyncSource(); + + TubeConfiguration configuration = new TubeConfiguration() + .withBackpressureStrategy(BackpressureStrategy.BUFFER) + .withBufferSize(256); + + Publisher pub = create(configuration, tube -> { + + // Start + source.start(); + + // Allow items to be received + tube.whenRequested(n -> source.resume()); + + // Termination cases + tube.whenCancelled(source::close); + tube.whenTerminates(() -> System.out.println("Done")); + + // Emit items, pause the source if need be + source.onItem(str -> { + tube.send(str); + if (tube.outstandingRequests() == 0L) { + source.pause(); + } + }); + + // Error + source.onError(tube::fail); + + // Completion + source.onEnd(tube::complete); + + }); + } +} diff --git a/content/docs/smallrye-mutiny-zero/0/_nav.yml b/content/docs/smallrye-mutiny-zero/0/_nav.yml new file mode 100644 index 000000000..4d513c700 --- /dev/null +++ b/content/docs/smallrye-mutiny-zero/0/_nav.yml @@ -0,0 +1,7 @@ +title: Mutiny Zero +nav: +- title: Overview + page: index +- page: quick-start +- page: flow-adapters +- page: vertx-based-reactive-streams-publishers diff --git a/content/docs/smallrye-mutiny-zero/0/assets/reactive-ecosystem.png b/content/docs/smallrye-mutiny-zero/0/assets/reactive-ecosystem.png new file mode 100644 index 000000000..79c0ac63e Binary files /dev/null and b/content/docs/smallrye-mutiny-zero/0/assets/reactive-ecosystem.png differ diff --git a/content/docs/smallrye-mutiny-zero/0/assets/reactive-minimal.png b/content/docs/smallrye-mutiny-zero/0/assets/reactive-minimal.png new file mode 100644 index 000000000..fb41efc82 Binary files /dev/null and b/content/docs/smallrye-mutiny-zero/0/assets/reactive-minimal.png differ diff --git a/content/docs/smallrye-mutiny-zero/0/assets/reactive-pollution.png b/content/docs/smallrye-mutiny-zero/0/assets/reactive-pollution.png new file mode 100644 index 000000000..76abf4679 Binary files /dev/null and b/content/docs/smallrye-mutiny-zero/0/assets/reactive-pollution.png differ diff --git a/content/docs/smallrye-mutiny-zero/0/assets/redhat_reversed.svg b/content/docs/smallrye-mutiny-zero/0/assets/redhat_reversed.svg new file mode 100644 index 000000000..37fe83c48 --- /dev/null +++ b/content/docs/smallrye-mutiny-zero/0/assets/redhat_reversed.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/content/docs/smallrye-mutiny-zero/0/flow-adapters.adoc b/content/docs/smallrye-mutiny-zero/0/flow-adapters.adoc new file mode 100644 index 000000000..461db8c05 --- /dev/null +++ b/content/docs/smallrye-mutiny-zero/0/flow-adapters.adoc @@ -0,0 +1,29 @@ += JDK Flow from/to Reactive Streams Adapters +include::_attributes.adoc[] + +The `mutiny-zero-flow-adapters` library (Maven coordinates `io.smallrye.reactive:mutiny-zero-flow-adapters`) provides a _clean room_ implementation of adapters from and to the https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/concurrent/Flow.html[JDK Flow interfaces] that match those from Reactive Streams. + +== Why another adapter library? + +The implementation of our adapters is similar in spirit to https://github.com/reactive-streams/reactive-streams-jvm/tree/master/api/src/main/java9/org/reactivestreams[those from the Reactive Streams library], but they differ by: + +* failing early rather than passing `null` through in some cases, +* shipping under a https://www.apache.org/licenses/LICENSE-2.0[proper open source license] while the Reactive Streams library took a long time to make any progress towards publishing a new release, see https://github.com/reactive-streams/reactive-streams-jvm/issues/536[#536] and https://github.com/reactive-streams/reactive-streams-jvm/issues/530[#530] +* having correct JPMS (Java modules) descriptors for those who might need modules rather than the classpath. + +== How to use it? + +The public API exposes 2 types: + +* `AdaptersToFlow` to convert Reactive Streams types to `Flow` types, and +* `AdaptersToReactiveStreams` to convert `Flow` types to Reactive Streams types. + +Each type offers factory methods to convert from one type to the other. +For instance here's how you can convert from a Reactive Streams `Publisher` to a `Flow.Publisher`: + +[source,java] +---- +Publisher rsPublisher = connect("foo"); // ... where 'connect' returns a Publisher + +Flow.Publisher flowPublisher = AdaptersToFlow.publisher(rsPublisher); +---- diff --git a/content/docs/smallrye-mutiny-zero/0/index.adoc b/content/docs/smallrye-mutiny-zero/0/index.adoc new file mode 100644 index 000000000..821070a4e --- /dev/null +++ b/content/docs/smallrye-mutiny-zero/0/index.adoc @@ -0,0 +1,71 @@ += Overview +include::_attributes.adoc[] + +Mutiny Zero is a minimal API for creating https://www.reactive-streams.org[reactive streams] compliant publishers. + +It weights less than 50K and has 0 external dependency as it uses the https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/concurrent/Flow.html[Java Flow APIs]). + +== What are reactive streams? + +From the https://www.reactive-streams.org[reactive streams website]: + +____ +Reactive Streams is an initiative to provide a standard for asynchronous stream processing with non-blocking back pressure. +____ + +*Reactive streams are the _lingua franca_ for an open and vendor-neutral asynchronous programming ecosystem in Java.* + +In a _reactive streams_ ecosystem _sources_ with asynchronous I/O can be wrapped as _publishers_, and applications can _subscribe_ to these publishers: + +.Reactive Streams ecosystem +image::assets/reactive-ecosystem.png[Reactive Streams ecosystem] + +Since reactive streams are defined as a set of minimal interfaces (see the https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/concurrent/Flow.html[Java Flow interfaces]), it is possible to mix and match publishers, processors and subscribers from different software stacks. +For instance an event streaming service and a large data store can expose reactive streams compliant clients. +In turn applications can be made end-to-end reactive as long as the code honors reactive streams protocols and semantics. + +Reactive streams is a low-level protocol and applications shall use higher-level reactive programming libraries such as https://smallrye.io/smallrye-mutiny/[Mutiny]. + +== Why is Mutiny Zero any needed? + +The reactive streams interfaces are quite simple, so where is the catch? + +=== It's all about the protocol! + +In a perfect world middleware and service vendors would _simply_ implement these interfaces and ship client libraries. +*The problem with reactive streams is not the interfaces: the protocol is the hard part.* + +https://github.com/reactive-streams/reactive-streams-jvm/blob/v1.0.4/README.md#specification[Reactive streams come with a specification] and a https://github.com/reactive-streams/reactive-streams-jvm/tree/v1.0.4/tck[TCK to check for publishers, processors and subscribers conformance]. +There are many subtle rules that the TCK verifies, and it can be surprisingly hard to respect all of them. + +Of course one may be tempted to just ignore some rules and just focus on the Java interfaces, but then combining reactive bits will likely break at runtime due to back-pressure and/or threading problems. + +=== Why does it matter to keep the ecosystem open and vendor-neutral? + +Developing reactive streams compliant libraries is difficult, so vendors have been tempted to _simply_ reuse a high-level reactive programming library. + +All of these libraries have ways to create publishers from different eventing models, and because they are _supposed_ to pass the reactive streams TCK then the resulting publishers shall also be compliant. +This is a technically correct yet a poor answer in our opinion. + +.Reactive Streams pollution +image::assets/reactive-pollution.png[Reactive Streams pollution] + +We dived into the code of several reactive streams client libraries, and we found out that more often than not they had become tightly-coupled to the peculiarities of certain reactive programming libraries. + +This is a major problem because strong adherence to a complete reactive programming library annihilates the benefits of the reactive streams specification: + +* applications may want to use a different reactive programming library than the one chosen by a vendor, bringing unwanted dependencies (who needs 3 reactive programming libraries in their code base?), +* vendors become bound to the release cycle of the chosen reactive programming library, +* conflicts between versions are more likely to happen because different vendors may depend on incompatible versions of a given reactive programming library. + +== The promise of Mutiny Zero + +The mission of _Mutiny Zero_ is to provide a minimal library for implementing reactive streams compliant publishers. + +.Reactive Streams minimalism +image::assets/reactive-minimal.png[Reactive Streams minimalism] + +Mutiny Zero does not want to be a complete reactive programming library for Java, so you will only find APIs and helpers deemed useful to write publishers. +Nothing else, nothing more. + +Application developers must be free to choose their reactive stacks and not be imposed opinionated choices by vendors! diff --git a/content/docs/smallrye-mutiny-zero/0/quick-start.adoc b/content/docs/smallrye-mutiny-zero/0/quick-start.adoc new file mode 100644 index 000000000..bf48cd7c8 --- /dev/null +++ b/content/docs/smallrye-mutiny-zero/0/quick-start.adoc @@ -0,0 +1,85 @@ += Quick start +include::_attributes.adoc[] + +== Getting Mutiny Zero + +You can get Mutiny Zero through the following Maven coordinates: + +* `groupId`: `io.smallrye.reactive` +* `artifactId`: `mutiny-zero` + +Mutiny Zero exposes publishers (see https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/concurrent/Flow.html[`java.util.concurrent.Flow`]). + +== Creating publishers + +Your main entry point in the Mutiny Zero API is the `mutiny.zero.ZeroPublisher` interface that exposes factory methods. + +=== Creating from known values + +If you already know the values to be emitted (or a failure), then you can use following factory methods: + +[source,java] +---- +include::{examples-dir}/mutiny-zero/src/test/java/docsamples/FromKnownValues.java[] +---- + +=== Creating from `CompletionStage` + +`CompletionStage` is the Java SDK API for asynchronous operations that emit either a result or a failure. + +Mutiny Zero can create a `Publisher` from a `CompletionStage` that emits exactly 1 item or a failure, then a completion signal: + +[source,java] +---- +include::{examples-dir}/mutiny-zero/src/test/java/docsamples/FromCompletionStage.java[] +---- + +=== Creating using the general-purpose `Tube` API + +For all other cases you should use the `mutiny.zero.Tube` API. + +A `Tube` represents an object you can interact with to emit values, error and completion. +It is aware of cancellation and items requests made by the subscriber. + +A `Tube` is a good abstraction if you want to pass events from an existing asynchronous I/O source as they arrive. +Here is a not so fictional example where `SampleAsyncSource` (an asynchronous I/O API) has to be adapted to a `Publisher`: + +[source,java] +---- +include::{examples-dir}/mutiny-zero/src/test/java/docsamples/FromTube.java[] +---- + +Since `SampleAsyncSource` does not support reactive streams but can be paused and resumed, the `Tube` API is used not just to send items but also to control `SampleAsyncSource`. +The `Tube` also has a buffer of 256 items in case of overflow to cope with the fact that pausing `SampleAsyncSource` may not be immediate. + +Several back-pressure strategies are offered by `Tube`: + +* buffer items (with or without a bound), +* drop items, +* signal an error, +* ignore back-pressure and still send items, +* keep only the last values in a fixed-size buffer. + +`Tube` enforces the reactive streams protocol. +For instance if you have several threads competing to send items, then items will still be emitted _serially_ by one of the threads rather than concurrently. + +== Helpers + +Mutiny Zero offers a few helpers for commonly-needed tasks, but without the intention of becoming a full-fledge reactive programming API. + +Introducing new helpers will always be done carefully by observing real world implementation patterns. + +=== `CompletionStage` helpers + +The `AsyncHelpers` class offers a few helpers to simplify developing against the `CompletionStage`, especially before Java 11. + +* `failedFuture` creates a `CompletionStage` that has already failed. +* `applyExceptionally` applies a function to map a failure `Throwable` to another `Throwable`. +* `composeExceptionally` applies a function to compose a failure `Throwable` to another `CompletionStage`. + +=== `Publisher` helpers + +More often than not we need a little help when dealing with a `Publisher`. + +* `collectToList` assembles all items from a `Publisher` to a `CompletionStage++<++List++>++`. +* `map` returns a `Publisher` that applies a function to all items from the original `Publisher`. diff --git a/content/docs/smallrye-mutiny-zero/0/vertx-based-reactive-streams-publishers.adoc b/content/docs/smallrye-mutiny-zero/0/vertx-based-reactive-streams-publishers.adoc new file mode 100644 index 000000000..e422a1b88 --- /dev/null +++ b/content/docs/smallrye-mutiny-zero/0/vertx-based-reactive-streams-publishers.adoc @@ -0,0 +1,29 @@ += Vert.x-based Reactive Streams Publishers +include::_attributes.adoc[] + +The `mutiny-zero-vertx-publishers` library (Maven coordinates `io.smallrye.reactive:mutiny-zero-vertx-publishers`) allows creating _Reactive Streams_ publishers from https://vertx.io/[Vert.x] streams. + +This library acts as a thin adapter between Vert.x `ReadStream` and `java.util.concurrent.Flow.Publisher` and uses _Mutiny Zero_ to expose _Reactive Streams_ compliant publishers. + +== API overview + +The entry point is the `mutiny.zero.vertxpublishers.VertxPublisher` interface that exposes 2 static factory methods. + +* `Publisher++<++T++>++ fromSupplier(Supplier++<++ReadStream++<++T++>>++ streamSupplier)` is to be used when some Vert.x API returns a `ReadStream++<++T++>++`. +* `Publisher++<++T++>++ fromFuture(Supplier++<++Future++<++? extends ReadStream++<++T++>>>++ futureStreamSupplier)` is to be used when some Vert.x API asynchronously returns a `ReadStream++<++T++>++` through a `Future`. + +The factory methods use suppliers so that the `ReadStream` instances to be adapted are on a per-subscriber basis. + +== Sample usage + +The following example makes HTTP requests to the https://api.usb.urbanobservatory.ac.uk/[Newcastle University Urban Observatory API] using the Vert.x HTTP client: + +[source,java] +---- +include::{examples-dir}/mutiny-zero-vertx-publishers/src/test/java/docsamples/UrbanObservatoryHttpClient.java[] +---- + +A new HTTP connection is issued everytime the publisher is being subscribed. +In this example the subscriber controls demand by requesting a new Vert.x `Buffer` every 500ms. + +If you run this program then you will see the JSON response being progressively printed to the standard console in chunks, every 500 milliseconds. diff --git a/content/docs/smallrye-mutiny-zero/latest/_attributes.adoc b/content/docs/smallrye-mutiny-zero/latest/_attributes.adoc new file mode 100644 index 000000000..375d29a1c --- /dev/null +++ b/content/docs/smallrye-mutiny-zero/latest/_attributes.adoc @@ -0,0 +1,20 @@ +:project-version: 1.3.1 +// Attributes shared by every page. smallrye.io overrides the ones it needs when it publishes. +// Pages in subdirectories set :version-root: (../ per level) before including this file. +ifndef::version-root[:version-root:] +:idprefix: +:idseparator: - +:sectanchors: +:examples-dir: {version-root}_examples +:partialsdir: {version-root}_partials +:project-name: Mutiny Zero +:javadoc-version: latest +:generated-dir: {version-root}_generated + +// Added by smallrye.io (tools/docs/Docs.java sync) +:javadoc-version: 1.3.1 +:relfileprefix: /docs/smallrye-mutiny-zero/latest/ +:imagesdir: /docs/smallrye-mutiny-zero/latest +:docs-project: smallrye-mutiny-zero +:docs-version: latest +:docs-ref: 1.3.1 diff --git a/content/docs/smallrye-mutiny-zero/latest/_examples/mutiny-zero-vertx-publishers/src/test/java/docsamples/UrbanObservatoryHttpClient.java b/content/docs/smallrye-mutiny-zero/latest/_examples/mutiny-zero-vertx-publishers/src/test/java/docsamples/UrbanObservatoryHttpClient.java new file mode 100644 index 000000000..3d7ddf2f7 --- /dev/null +++ b/content/docs/smallrye-mutiny-zero/latest/_examples/mutiny-zero-vertx-publishers/src/test/java/docsamples/UrbanObservatoryHttpClient.java @@ -0,0 +1,67 @@ +package docsamples; + +import java.nio.charset.StandardCharsets; +import java.util.concurrent.Executors; +import java.util.concurrent.Flow; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.HttpClient; +import io.vertx.core.http.HttpClientRequest; +import io.vertx.core.http.HttpMethod; +import io.vertx.core.http.RequestOptions; +import mutiny.zero.vertxpublishers.VertxPublisher; + +public class UrbanObservatoryHttpClient { + + public static void main(String[] args) { + + Vertx vertx = Vertx.vertx(); + ScheduledExecutorService executor = Executors.newScheduledThreadPool(1); + + RequestOptions opts = new RequestOptions() + .setSsl(true) + .setHost("api.usb.urbanobservatory.ac.uk") + .setPort(443) + .setMethod(HttpMethod.GET) + .addHeader("Accept", "application/json") + .setURI("/api/v2.0a/sensors/entity"); + + HttpClient httpClient = vertx.createHttpClient(); + + Flow.Publisher publisher = VertxPublisher.fromFuture(() -> httpClient + .request(opts) + .compose(HttpClientRequest::send)); + + publisher.subscribe(new Flow.Subscriber<>() { + + private Flow.Subscription subscription; + + @Override + public void onSubscribe(Flow.Subscription s) { + System.out.println("======================================"); + this.subscription = s; + s.request(1L); + } + + @Override + public void onNext(Buffer buffer) { + System.out.print(buffer.toString(StandardCharsets.UTF_8)); + executor.schedule(() -> subscription.request(1L), 500, TimeUnit.MILLISECONDS); + } + + @Override + public void onError(Throwable t) { + System.out.println("======================================"); + t.printStackTrace(); + } + + @Override + public void onComplete() { + System.out.println("======================================"); + } + }); + } +} diff --git a/content/docs/smallrye-mutiny-zero/latest/_examples/mutiny-zero/src/test/java/docsamples/FromCompletionStage.java b/content/docs/smallrye-mutiny-zero/latest/_examples/mutiny-zero/src/test/java/docsamples/FromCompletionStage.java new file mode 100644 index 000000000..62b3cc273 --- /dev/null +++ b/content/docs/smallrye-mutiny-zero/latest/_examples/mutiny-zero/src/test/java/docsamples/FromCompletionStage.java @@ -0,0 +1,14 @@ +package docsamples; + +import static java.util.concurrent.CompletableFuture.supplyAsync; +import static mutiny.zero.ZeroPublisher.fromCompletionStage; + +import java.util.concurrent.Flow; + +public class FromCompletionStage { + + public static void main(String[] args) { + + Flow.Publisher pun = fromCompletionStage(() -> supplyAsync(() -> 58L)); + } +} diff --git a/content/docs/smallrye-mutiny-zero/latest/_examples/mutiny-zero/src/test/java/docsamples/FromKnownValues.java b/content/docs/smallrye-mutiny-zero/latest/_examples/mutiny-zero/src/test/java/docsamples/FromKnownValues.java new file mode 100644 index 000000000..0b14665f3 --- /dev/null +++ b/content/docs/smallrye-mutiny-zero/latest/_examples/mutiny-zero/src/test/java/docsamples/FromKnownValues.java @@ -0,0 +1,22 @@ +package docsamples; + +import static mutiny.zero.ZeroPublisher.*; + +import java.io.IOException; +import java.util.Arrays; +import java.util.concurrent.Flow.Publisher; + +public class FromKnownValues { + + public static void main(String[] args) { + + // From values + Publisher pub1 = fromItems(1, 2, 3); + + // From an iterable + Publisher pub2 = fromIterable(Arrays.asList(1, 2, 3)); + + // From a failure + Publisher pub3 = fromFailure(new IOException("Broken pipe")); + } +} diff --git a/content/docs/smallrye-mutiny-zero/latest/_examples/mutiny-zero/src/test/java/docsamples/FromTube.java b/content/docs/smallrye-mutiny-zero/latest/_examples/mutiny-zero/src/test/java/docsamples/FromTube.java new file mode 100644 index 000000000..6a78a2cb8 --- /dev/null +++ b/content/docs/smallrye-mutiny-zero/latest/_examples/mutiny-zero/src/test/java/docsamples/FromTube.java @@ -0,0 +1,48 @@ +package docsamples; + +import static mutiny.zero.ZeroPublisher.create; + +import java.util.concurrent.Flow.Publisher; + +import mutiny.zero.BackpressureStrategy; +import mutiny.zero.TubeConfiguration; + +public class FromTube { + + public static void main(String[] args) { + + SampleAsyncSource source = new SampleAsyncSource(); + + TubeConfiguration configuration = new TubeConfiguration() + .withBackpressureStrategy(BackpressureStrategy.BUFFER) + .withBufferSize(256); + + Publisher pub = create(configuration, tube -> { + + // Start + source.start(); + + // Allow items to be received + tube.whenRequested(n -> source.resume()); + + // Termination cases + tube.whenCancelled(source::close); + tube.whenTerminates(() -> System.out.println("Done")); + + // Emit items, pause the source if need be + source.onItem(str -> { + tube.send(str); + if (tube.outstandingRequests() == 0L) { + source.pause(); + } + }); + + // Error + source.onError(tube::fail); + + // Completion + source.onEnd(tube::complete); + + }); + } +} diff --git a/content/docs/smallrye-mutiny-zero/latest/_nav.yml b/content/docs/smallrye-mutiny-zero/latest/_nav.yml new file mode 100644 index 000000000..4d513c700 --- /dev/null +++ b/content/docs/smallrye-mutiny-zero/latest/_nav.yml @@ -0,0 +1,7 @@ +title: Mutiny Zero +nav: +- title: Overview + page: index +- page: quick-start +- page: flow-adapters +- page: vertx-based-reactive-streams-publishers diff --git a/content/docs/smallrye-mutiny-zero/latest/assets/reactive-ecosystem.png b/content/docs/smallrye-mutiny-zero/latest/assets/reactive-ecosystem.png new file mode 100644 index 000000000..79c0ac63e Binary files /dev/null and b/content/docs/smallrye-mutiny-zero/latest/assets/reactive-ecosystem.png differ diff --git a/content/docs/smallrye-mutiny-zero/latest/assets/reactive-minimal.png b/content/docs/smallrye-mutiny-zero/latest/assets/reactive-minimal.png new file mode 100644 index 000000000..fb41efc82 Binary files /dev/null and b/content/docs/smallrye-mutiny-zero/latest/assets/reactive-minimal.png differ diff --git a/content/docs/smallrye-mutiny-zero/latest/assets/reactive-pollution.png b/content/docs/smallrye-mutiny-zero/latest/assets/reactive-pollution.png new file mode 100644 index 000000000..76abf4679 Binary files /dev/null and b/content/docs/smallrye-mutiny-zero/latest/assets/reactive-pollution.png differ diff --git a/content/docs/smallrye-mutiny-zero/latest/assets/redhat_reversed.svg b/content/docs/smallrye-mutiny-zero/latest/assets/redhat_reversed.svg new file mode 100644 index 000000000..37fe83c48 --- /dev/null +++ b/content/docs/smallrye-mutiny-zero/latest/assets/redhat_reversed.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/content/docs/smallrye-mutiny-zero/latest/flow-adapters.adoc b/content/docs/smallrye-mutiny-zero/latest/flow-adapters.adoc new file mode 100644 index 000000000..461db8c05 --- /dev/null +++ b/content/docs/smallrye-mutiny-zero/latest/flow-adapters.adoc @@ -0,0 +1,29 @@ += JDK Flow from/to Reactive Streams Adapters +include::_attributes.adoc[] + +The `mutiny-zero-flow-adapters` library (Maven coordinates `io.smallrye.reactive:mutiny-zero-flow-adapters`) provides a _clean room_ implementation of adapters from and to the https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/concurrent/Flow.html[JDK Flow interfaces] that match those from Reactive Streams. + +== Why another adapter library? + +The implementation of our adapters is similar in spirit to https://github.com/reactive-streams/reactive-streams-jvm/tree/master/api/src/main/java9/org/reactivestreams[those from the Reactive Streams library], but they differ by: + +* failing early rather than passing `null` through in some cases, +* shipping under a https://www.apache.org/licenses/LICENSE-2.0[proper open source license] while the Reactive Streams library took a long time to make any progress towards publishing a new release, see https://github.com/reactive-streams/reactive-streams-jvm/issues/536[#536] and https://github.com/reactive-streams/reactive-streams-jvm/issues/530[#530] +* having correct JPMS (Java modules) descriptors for those who might need modules rather than the classpath. + +== How to use it? + +The public API exposes 2 types: + +* `AdaptersToFlow` to convert Reactive Streams types to `Flow` types, and +* `AdaptersToReactiveStreams` to convert `Flow` types to Reactive Streams types. + +Each type offers factory methods to convert from one type to the other. +For instance here's how you can convert from a Reactive Streams `Publisher` to a `Flow.Publisher`: + +[source,java] +---- +Publisher rsPublisher = connect("foo"); // ... where 'connect' returns a Publisher + +Flow.Publisher flowPublisher = AdaptersToFlow.publisher(rsPublisher); +---- diff --git a/content/docs/smallrye-mutiny-zero/latest/index.adoc b/content/docs/smallrye-mutiny-zero/latest/index.adoc new file mode 100644 index 000000000..821070a4e --- /dev/null +++ b/content/docs/smallrye-mutiny-zero/latest/index.adoc @@ -0,0 +1,71 @@ += Overview +include::_attributes.adoc[] + +Mutiny Zero is a minimal API for creating https://www.reactive-streams.org[reactive streams] compliant publishers. + +It weights less than 50K and has 0 external dependency as it uses the https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/concurrent/Flow.html[Java Flow APIs]). + +== What are reactive streams? + +From the https://www.reactive-streams.org[reactive streams website]: + +____ +Reactive Streams is an initiative to provide a standard for asynchronous stream processing with non-blocking back pressure. +____ + +*Reactive streams are the _lingua franca_ for an open and vendor-neutral asynchronous programming ecosystem in Java.* + +In a _reactive streams_ ecosystem _sources_ with asynchronous I/O can be wrapped as _publishers_, and applications can _subscribe_ to these publishers: + +.Reactive Streams ecosystem +image::assets/reactive-ecosystem.png[Reactive Streams ecosystem] + +Since reactive streams are defined as a set of minimal interfaces (see the https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/concurrent/Flow.html[Java Flow interfaces]), it is possible to mix and match publishers, processors and subscribers from different software stacks. +For instance an event streaming service and a large data store can expose reactive streams compliant clients. +In turn applications can be made end-to-end reactive as long as the code honors reactive streams protocols and semantics. + +Reactive streams is a low-level protocol and applications shall use higher-level reactive programming libraries such as https://smallrye.io/smallrye-mutiny/[Mutiny]. + +== Why is Mutiny Zero any needed? + +The reactive streams interfaces are quite simple, so where is the catch? + +=== It's all about the protocol! + +In a perfect world middleware and service vendors would _simply_ implement these interfaces and ship client libraries. +*The problem with reactive streams is not the interfaces: the protocol is the hard part.* + +https://github.com/reactive-streams/reactive-streams-jvm/blob/v1.0.4/README.md#specification[Reactive streams come with a specification] and a https://github.com/reactive-streams/reactive-streams-jvm/tree/v1.0.4/tck[TCK to check for publishers, processors and subscribers conformance]. +There are many subtle rules that the TCK verifies, and it can be surprisingly hard to respect all of them. + +Of course one may be tempted to just ignore some rules and just focus on the Java interfaces, but then combining reactive bits will likely break at runtime due to back-pressure and/or threading problems. + +=== Why does it matter to keep the ecosystem open and vendor-neutral? + +Developing reactive streams compliant libraries is difficult, so vendors have been tempted to _simply_ reuse a high-level reactive programming library. + +All of these libraries have ways to create publishers from different eventing models, and because they are _supposed_ to pass the reactive streams TCK then the resulting publishers shall also be compliant. +This is a technically correct yet a poor answer in our opinion. + +.Reactive Streams pollution +image::assets/reactive-pollution.png[Reactive Streams pollution] + +We dived into the code of several reactive streams client libraries, and we found out that more often than not they had become tightly-coupled to the peculiarities of certain reactive programming libraries. + +This is a major problem because strong adherence to a complete reactive programming library annihilates the benefits of the reactive streams specification: + +* applications may want to use a different reactive programming library than the one chosen by a vendor, bringing unwanted dependencies (who needs 3 reactive programming libraries in their code base?), +* vendors become bound to the release cycle of the chosen reactive programming library, +* conflicts between versions are more likely to happen because different vendors may depend on incompatible versions of a given reactive programming library. + +== The promise of Mutiny Zero + +The mission of _Mutiny Zero_ is to provide a minimal library for implementing reactive streams compliant publishers. + +.Reactive Streams minimalism +image::assets/reactive-minimal.png[Reactive Streams minimalism] + +Mutiny Zero does not want to be a complete reactive programming library for Java, so you will only find APIs and helpers deemed useful to write publishers. +Nothing else, nothing more. + +Application developers must be free to choose their reactive stacks and not be imposed opinionated choices by vendors! diff --git a/content/docs/smallrye-mutiny-zero/latest/quick-start.adoc b/content/docs/smallrye-mutiny-zero/latest/quick-start.adoc new file mode 100644 index 000000000..b6702c04d --- /dev/null +++ b/content/docs/smallrye-mutiny-zero/latest/quick-start.adoc @@ -0,0 +1,83 @@ += Quick start +include::_attributes.adoc[] + +== Getting Mutiny Zero + +You can get Mutiny Zero through the following Maven coordinates: + +* `groupId`: `io.smallrye.reactive` +* `artifactId`: `mutiny-zero` + +Mutiny Zero exposes publishers (see https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/concurrent/Flow.html[`java.util.concurrent.Flow`]). + +== Creating publishers + +Your main entry point in the Mutiny Zero API is the `mutiny.zero.ZeroPublisher` interface that exposes factory methods. + +=== Creating from known values + +If you already know the values to be emitted (or a failure), then you can use following factory methods: + +[source,java] +---- +include::{examples-dir}/mutiny-zero/src/test/java/docsamples/FromKnownValues.java[] +---- + +=== Creating from `CompletionStage` + +`CompletionStage` is the Java SDK API for asynchronous operations that emit either a result or a failure. + +Mutiny Zero can create a `Publisher` from a `CompletionStage` that emits exactly 1 item or a failure, then a completion signal: + +[source,java] +---- +include::{examples-dir}/mutiny-zero/src/test/java/docsamples/FromCompletionStage.java[] +---- + +=== Creating using the general-purpose `Tube` API + +For all other cases you should use the `mutiny.zero.Tube` API. + +A `Tube` represents an object you can interact with to emit values, error and completion. +It is aware of cancellation and items requests made by the subscriber. + +A `Tube` is a good abstraction if you want to pass events from an existing asynchronous I/O source as they arrive. +Here is a not so fictional example where `SampleAsyncSource` (an asynchronous I/O API) has to be adapted to a `Publisher`: + +[source,java] +---- +include::{examples-dir}/mutiny-zero/src/test/java/docsamples/FromTube.java[] +---- + +Since `SampleAsyncSource` does not support reactive streams but can be paused and resumed, the `Tube` API is used not just to send items but also to control `SampleAsyncSource`. +The `Tube` also has a buffer of 256 items in case of overflow to cope with the fact that pausing `SampleAsyncSource` may not be immediate. + +Several back-pressure strategies are offered by `Tube`: + +* buffer items (with or without a bound), +* drop items, +* signal an error, +* ignore back-pressure and still send items, +* keep only the last values in a fixed-size buffer. + +`Tube` enforces the reactive streams protocol. +For instance if you have several threads competing to send items, then items will still be emitted _serially_ by one of the threads rather than concurrently. + +== Helpers + +Mutiny Zero offers a few helpers for commonly-needed tasks, but without the intention of becoming a full-fledge reactive programming API. + +Introducing new helpers will always be done carefully by observing real world implementation patterns. + +=== `CompletionStage` helpers + +The `AsyncHelpers` class offers a few helpers to simplify developing against the `CompletionStage`, especially before Java 11. + +* `applyExceptionally` applies a function to map a failure `Throwable` to another `Throwable`. +* `composeExceptionally` applies a function to compose a failure `Throwable` to another `CompletionStage`. + +=== `Publisher` helpers + +More often than not we need a little help when dealing with a `Publisher`. + +* `collectToList` assembles all items from a `Publisher` to a `CompletionStage++<++List++>++`. diff --git a/content/docs/smallrye-mutiny-zero/latest/vertx-based-reactive-streams-publishers.adoc b/content/docs/smallrye-mutiny-zero/latest/vertx-based-reactive-streams-publishers.adoc new file mode 100644 index 000000000..e422a1b88 --- /dev/null +++ b/content/docs/smallrye-mutiny-zero/latest/vertx-based-reactive-streams-publishers.adoc @@ -0,0 +1,29 @@ += Vert.x-based Reactive Streams Publishers +include::_attributes.adoc[] + +The `mutiny-zero-vertx-publishers` library (Maven coordinates `io.smallrye.reactive:mutiny-zero-vertx-publishers`) allows creating _Reactive Streams_ publishers from https://vertx.io/[Vert.x] streams. + +This library acts as a thin adapter between Vert.x `ReadStream` and `java.util.concurrent.Flow.Publisher` and uses _Mutiny Zero_ to expose _Reactive Streams_ compliant publishers. + +== API overview + +The entry point is the `mutiny.zero.vertxpublishers.VertxPublisher` interface that exposes 2 static factory methods. + +* `Publisher++<++T++>++ fromSupplier(Supplier++<++ReadStream++<++T++>>++ streamSupplier)` is to be used when some Vert.x API returns a `ReadStream++<++T++>++`. +* `Publisher++<++T++>++ fromFuture(Supplier++<++Future++<++? extends ReadStream++<++T++>>>++ futureStreamSupplier)` is to be used when some Vert.x API asynchronously returns a `ReadStream++<++T++>++` through a `Future`. + +The factory methods use suppliers so that the `ReadStream` instances to be adapted are on a per-subscriber basis. + +== Sample usage + +The following example makes HTTP requests to the https://api.usb.urbanobservatory.ac.uk/[Newcastle University Urban Observatory API] using the Vert.x HTTP client: + +[source,java] +---- +include::{examples-dir}/mutiny-zero-vertx-publishers/src/test/java/docsamples/UrbanObservatoryHttpClient.java[] +---- + +A new HTTP connection is issued everytime the publisher is being subscribed. +In this example the subscriber controls demand by requesting a new Vert.x `Buffer` every 500ms. + +If you run this program then you will see the JSON response being progressively printed to the standard console in chunks, every 500 milliseconds. diff --git a/content/docs/smallrye-mutiny-zero/main/_attributes.adoc b/content/docs/smallrye-mutiny-zero/main/_attributes.adoc new file mode 100644 index 000000000..46ff4a3ae --- /dev/null +++ b/content/docs/smallrye-mutiny-zero/main/_attributes.adoc @@ -0,0 +1,20 @@ +:project-version: 1.3.1 +// Attributes shared by every page. smallrye.io overrides the ones it needs when it publishes. +// Pages in subdirectories set :version-root: (../ per level) before including this file. +ifndef::version-root[:version-root:] +:idprefix: +:idseparator: - +:sectanchors: +:examples-dir: {version-root}_examples +:partialsdir: {version-root}_partials +:project-name: Mutiny Zero +:javadoc-version: latest +:generated-dir: {version-root}_generated + +// Added by smallrye.io (tools/docs/Docs.java sync) +:javadoc-version: 1.3.1 +:relfileprefix: /docs/smallrye-mutiny-zero/main/ +:imagesdir: /docs/smallrye-mutiny-zero/main +:docs-project: smallrye-mutiny-zero +:docs-version: main +:docs-ref: main diff --git a/content/docs/smallrye-mutiny-zero/main/_examples/mutiny-zero-vertx-publishers/src/test/java/docsamples/UrbanObservatoryHttpClient.java b/content/docs/smallrye-mutiny-zero/main/_examples/mutiny-zero-vertx-publishers/src/test/java/docsamples/UrbanObservatoryHttpClient.java new file mode 100644 index 000000000..3d7ddf2f7 --- /dev/null +++ b/content/docs/smallrye-mutiny-zero/main/_examples/mutiny-zero-vertx-publishers/src/test/java/docsamples/UrbanObservatoryHttpClient.java @@ -0,0 +1,67 @@ +package docsamples; + +import java.nio.charset.StandardCharsets; +import java.util.concurrent.Executors; +import java.util.concurrent.Flow; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.HttpClient; +import io.vertx.core.http.HttpClientRequest; +import io.vertx.core.http.HttpMethod; +import io.vertx.core.http.RequestOptions; +import mutiny.zero.vertxpublishers.VertxPublisher; + +public class UrbanObservatoryHttpClient { + + public static void main(String[] args) { + + Vertx vertx = Vertx.vertx(); + ScheduledExecutorService executor = Executors.newScheduledThreadPool(1); + + RequestOptions opts = new RequestOptions() + .setSsl(true) + .setHost("api.usb.urbanobservatory.ac.uk") + .setPort(443) + .setMethod(HttpMethod.GET) + .addHeader("Accept", "application/json") + .setURI("/api/v2.0a/sensors/entity"); + + HttpClient httpClient = vertx.createHttpClient(); + + Flow.Publisher publisher = VertxPublisher.fromFuture(() -> httpClient + .request(opts) + .compose(HttpClientRequest::send)); + + publisher.subscribe(new Flow.Subscriber<>() { + + private Flow.Subscription subscription; + + @Override + public void onSubscribe(Flow.Subscription s) { + System.out.println("======================================"); + this.subscription = s; + s.request(1L); + } + + @Override + public void onNext(Buffer buffer) { + System.out.print(buffer.toString(StandardCharsets.UTF_8)); + executor.schedule(() -> subscription.request(1L), 500, TimeUnit.MILLISECONDS); + } + + @Override + public void onError(Throwable t) { + System.out.println("======================================"); + t.printStackTrace(); + } + + @Override + public void onComplete() { + System.out.println("======================================"); + } + }); + } +} diff --git a/content/docs/smallrye-mutiny-zero/main/_examples/mutiny-zero/src/test/java/docsamples/FromCompletionStage.java b/content/docs/smallrye-mutiny-zero/main/_examples/mutiny-zero/src/test/java/docsamples/FromCompletionStage.java new file mode 100644 index 000000000..62b3cc273 --- /dev/null +++ b/content/docs/smallrye-mutiny-zero/main/_examples/mutiny-zero/src/test/java/docsamples/FromCompletionStage.java @@ -0,0 +1,14 @@ +package docsamples; + +import static java.util.concurrent.CompletableFuture.supplyAsync; +import static mutiny.zero.ZeroPublisher.fromCompletionStage; + +import java.util.concurrent.Flow; + +public class FromCompletionStage { + + public static void main(String[] args) { + + Flow.Publisher pun = fromCompletionStage(() -> supplyAsync(() -> 58L)); + } +} diff --git a/content/docs/smallrye-mutiny-zero/main/_examples/mutiny-zero/src/test/java/docsamples/FromKnownValues.java b/content/docs/smallrye-mutiny-zero/main/_examples/mutiny-zero/src/test/java/docsamples/FromKnownValues.java new file mode 100644 index 000000000..0b14665f3 --- /dev/null +++ b/content/docs/smallrye-mutiny-zero/main/_examples/mutiny-zero/src/test/java/docsamples/FromKnownValues.java @@ -0,0 +1,22 @@ +package docsamples; + +import static mutiny.zero.ZeroPublisher.*; + +import java.io.IOException; +import java.util.Arrays; +import java.util.concurrent.Flow.Publisher; + +public class FromKnownValues { + + public static void main(String[] args) { + + // From values + Publisher pub1 = fromItems(1, 2, 3); + + // From an iterable + Publisher pub2 = fromIterable(Arrays.asList(1, 2, 3)); + + // From a failure + Publisher pub3 = fromFailure(new IOException("Broken pipe")); + } +} diff --git a/content/docs/smallrye-mutiny-zero/main/_examples/mutiny-zero/src/test/java/docsamples/FromTube.java b/content/docs/smallrye-mutiny-zero/main/_examples/mutiny-zero/src/test/java/docsamples/FromTube.java new file mode 100644 index 000000000..6a78a2cb8 --- /dev/null +++ b/content/docs/smallrye-mutiny-zero/main/_examples/mutiny-zero/src/test/java/docsamples/FromTube.java @@ -0,0 +1,48 @@ +package docsamples; + +import static mutiny.zero.ZeroPublisher.create; + +import java.util.concurrent.Flow.Publisher; + +import mutiny.zero.BackpressureStrategy; +import mutiny.zero.TubeConfiguration; + +public class FromTube { + + public static void main(String[] args) { + + SampleAsyncSource source = new SampleAsyncSource(); + + TubeConfiguration configuration = new TubeConfiguration() + .withBackpressureStrategy(BackpressureStrategy.BUFFER) + .withBufferSize(256); + + Publisher pub = create(configuration, tube -> { + + // Start + source.start(); + + // Allow items to be received + tube.whenRequested(n -> source.resume()); + + // Termination cases + tube.whenCancelled(source::close); + tube.whenTerminates(() -> System.out.println("Done")); + + // Emit items, pause the source if need be + source.onItem(str -> { + tube.send(str); + if (tube.outstandingRequests() == 0L) { + source.pause(); + } + }); + + // Error + source.onError(tube::fail); + + // Completion + source.onEnd(tube::complete); + + }); + } +} diff --git a/content/docs/smallrye-mutiny-zero/main/_nav.yml b/content/docs/smallrye-mutiny-zero/main/_nav.yml new file mode 100644 index 000000000..4d513c700 --- /dev/null +++ b/content/docs/smallrye-mutiny-zero/main/_nav.yml @@ -0,0 +1,7 @@ +title: Mutiny Zero +nav: +- title: Overview + page: index +- page: quick-start +- page: flow-adapters +- page: vertx-based-reactive-streams-publishers diff --git a/content/docs/smallrye-mutiny-zero/main/assets/reactive-ecosystem.png b/content/docs/smallrye-mutiny-zero/main/assets/reactive-ecosystem.png new file mode 100644 index 000000000..79c0ac63e Binary files /dev/null and b/content/docs/smallrye-mutiny-zero/main/assets/reactive-ecosystem.png differ diff --git a/content/docs/smallrye-mutiny-zero/main/assets/reactive-minimal.png b/content/docs/smallrye-mutiny-zero/main/assets/reactive-minimal.png new file mode 100644 index 000000000..fb41efc82 Binary files /dev/null and b/content/docs/smallrye-mutiny-zero/main/assets/reactive-minimal.png differ diff --git a/content/docs/smallrye-mutiny-zero/main/assets/reactive-pollution.png b/content/docs/smallrye-mutiny-zero/main/assets/reactive-pollution.png new file mode 100644 index 000000000..76abf4679 Binary files /dev/null and b/content/docs/smallrye-mutiny-zero/main/assets/reactive-pollution.png differ diff --git a/content/docs/smallrye-mutiny-zero/main/assets/redhat_reversed.svg b/content/docs/smallrye-mutiny-zero/main/assets/redhat_reversed.svg new file mode 100644 index 000000000..37fe83c48 --- /dev/null +++ b/content/docs/smallrye-mutiny-zero/main/assets/redhat_reversed.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/content/docs/smallrye-mutiny-zero/main/flow-adapters.adoc b/content/docs/smallrye-mutiny-zero/main/flow-adapters.adoc new file mode 100644 index 000000000..461db8c05 --- /dev/null +++ b/content/docs/smallrye-mutiny-zero/main/flow-adapters.adoc @@ -0,0 +1,29 @@ += JDK Flow from/to Reactive Streams Adapters +include::_attributes.adoc[] + +The `mutiny-zero-flow-adapters` library (Maven coordinates `io.smallrye.reactive:mutiny-zero-flow-adapters`) provides a _clean room_ implementation of adapters from and to the https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/concurrent/Flow.html[JDK Flow interfaces] that match those from Reactive Streams. + +== Why another adapter library? + +The implementation of our adapters is similar in spirit to https://github.com/reactive-streams/reactive-streams-jvm/tree/master/api/src/main/java9/org/reactivestreams[those from the Reactive Streams library], but they differ by: + +* failing early rather than passing `null` through in some cases, +* shipping under a https://www.apache.org/licenses/LICENSE-2.0[proper open source license] while the Reactive Streams library took a long time to make any progress towards publishing a new release, see https://github.com/reactive-streams/reactive-streams-jvm/issues/536[#536] and https://github.com/reactive-streams/reactive-streams-jvm/issues/530[#530] +* having correct JPMS (Java modules) descriptors for those who might need modules rather than the classpath. + +== How to use it? + +The public API exposes 2 types: + +* `AdaptersToFlow` to convert Reactive Streams types to `Flow` types, and +* `AdaptersToReactiveStreams` to convert `Flow` types to Reactive Streams types. + +Each type offers factory methods to convert from one type to the other. +For instance here's how you can convert from a Reactive Streams `Publisher` to a `Flow.Publisher`: + +[source,java] +---- +Publisher rsPublisher = connect("foo"); // ... where 'connect' returns a Publisher + +Flow.Publisher flowPublisher = AdaptersToFlow.publisher(rsPublisher); +---- diff --git a/content/docs/smallrye-mutiny-zero/main/index.adoc b/content/docs/smallrye-mutiny-zero/main/index.adoc new file mode 100644 index 000000000..821070a4e --- /dev/null +++ b/content/docs/smallrye-mutiny-zero/main/index.adoc @@ -0,0 +1,71 @@ += Overview +include::_attributes.adoc[] + +Mutiny Zero is a minimal API for creating https://www.reactive-streams.org[reactive streams] compliant publishers. + +It weights less than 50K and has 0 external dependency as it uses the https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/concurrent/Flow.html[Java Flow APIs]). + +== What are reactive streams? + +From the https://www.reactive-streams.org[reactive streams website]: + +____ +Reactive Streams is an initiative to provide a standard for asynchronous stream processing with non-blocking back pressure. +____ + +*Reactive streams are the _lingua franca_ for an open and vendor-neutral asynchronous programming ecosystem in Java.* + +In a _reactive streams_ ecosystem _sources_ with asynchronous I/O can be wrapped as _publishers_, and applications can _subscribe_ to these publishers: + +.Reactive Streams ecosystem +image::assets/reactive-ecosystem.png[Reactive Streams ecosystem] + +Since reactive streams are defined as a set of minimal interfaces (see the https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/concurrent/Flow.html[Java Flow interfaces]), it is possible to mix and match publishers, processors and subscribers from different software stacks. +For instance an event streaming service and a large data store can expose reactive streams compliant clients. +In turn applications can be made end-to-end reactive as long as the code honors reactive streams protocols and semantics. + +Reactive streams is a low-level protocol and applications shall use higher-level reactive programming libraries such as https://smallrye.io/smallrye-mutiny/[Mutiny]. + +== Why is Mutiny Zero any needed? + +The reactive streams interfaces are quite simple, so where is the catch? + +=== It's all about the protocol! + +In a perfect world middleware and service vendors would _simply_ implement these interfaces and ship client libraries. +*The problem with reactive streams is not the interfaces: the protocol is the hard part.* + +https://github.com/reactive-streams/reactive-streams-jvm/blob/v1.0.4/README.md#specification[Reactive streams come with a specification] and a https://github.com/reactive-streams/reactive-streams-jvm/tree/v1.0.4/tck[TCK to check for publishers, processors and subscribers conformance]. +There are many subtle rules that the TCK verifies, and it can be surprisingly hard to respect all of them. + +Of course one may be tempted to just ignore some rules and just focus on the Java interfaces, but then combining reactive bits will likely break at runtime due to back-pressure and/or threading problems. + +=== Why does it matter to keep the ecosystem open and vendor-neutral? + +Developing reactive streams compliant libraries is difficult, so vendors have been tempted to _simply_ reuse a high-level reactive programming library. + +All of these libraries have ways to create publishers from different eventing models, and because they are _supposed_ to pass the reactive streams TCK then the resulting publishers shall also be compliant. +This is a technically correct yet a poor answer in our opinion. + +.Reactive Streams pollution +image::assets/reactive-pollution.png[Reactive Streams pollution] + +We dived into the code of several reactive streams client libraries, and we found out that more often than not they had become tightly-coupled to the peculiarities of certain reactive programming libraries. + +This is a major problem because strong adherence to a complete reactive programming library annihilates the benefits of the reactive streams specification: + +* applications may want to use a different reactive programming library than the one chosen by a vendor, bringing unwanted dependencies (who needs 3 reactive programming libraries in their code base?), +* vendors become bound to the release cycle of the chosen reactive programming library, +* conflicts between versions are more likely to happen because different vendors may depend on incompatible versions of a given reactive programming library. + +== The promise of Mutiny Zero + +The mission of _Mutiny Zero_ is to provide a minimal library for implementing reactive streams compliant publishers. + +.Reactive Streams minimalism +image::assets/reactive-minimal.png[Reactive Streams minimalism] + +Mutiny Zero does not want to be a complete reactive programming library for Java, so you will only find APIs and helpers deemed useful to write publishers. +Nothing else, nothing more. + +Application developers must be free to choose their reactive stacks and not be imposed opinionated choices by vendors! diff --git a/content/docs/smallrye-mutiny-zero/main/quick-start.adoc b/content/docs/smallrye-mutiny-zero/main/quick-start.adoc new file mode 100644 index 000000000..b6702c04d --- /dev/null +++ b/content/docs/smallrye-mutiny-zero/main/quick-start.adoc @@ -0,0 +1,83 @@ += Quick start +include::_attributes.adoc[] + +== Getting Mutiny Zero + +You can get Mutiny Zero through the following Maven coordinates: + +* `groupId`: `io.smallrye.reactive` +* `artifactId`: `mutiny-zero` + +Mutiny Zero exposes publishers (see https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/concurrent/Flow.html[`java.util.concurrent.Flow`]). + +== Creating publishers + +Your main entry point in the Mutiny Zero API is the `mutiny.zero.ZeroPublisher` interface that exposes factory methods. + +=== Creating from known values + +If you already know the values to be emitted (or a failure), then you can use following factory methods: + +[source,java] +---- +include::{examples-dir}/mutiny-zero/src/test/java/docsamples/FromKnownValues.java[] +---- + +=== Creating from `CompletionStage` + +`CompletionStage` is the Java SDK API for asynchronous operations that emit either a result or a failure. + +Mutiny Zero can create a `Publisher` from a `CompletionStage` that emits exactly 1 item or a failure, then a completion signal: + +[source,java] +---- +include::{examples-dir}/mutiny-zero/src/test/java/docsamples/FromCompletionStage.java[] +---- + +=== Creating using the general-purpose `Tube` API + +For all other cases you should use the `mutiny.zero.Tube` API. + +A `Tube` represents an object you can interact with to emit values, error and completion. +It is aware of cancellation and items requests made by the subscriber. + +A `Tube` is a good abstraction if you want to pass events from an existing asynchronous I/O source as they arrive. +Here is a not so fictional example where `SampleAsyncSource` (an asynchronous I/O API) has to be adapted to a `Publisher`: + +[source,java] +---- +include::{examples-dir}/mutiny-zero/src/test/java/docsamples/FromTube.java[] +---- + +Since `SampleAsyncSource` does not support reactive streams but can be paused and resumed, the `Tube` API is used not just to send items but also to control `SampleAsyncSource`. +The `Tube` also has a buffer of 256 items in case of overflow to cope with the fact that pausing `SampleAsyncSource` may not be immediate. + +Several back-pressure strategies are offered by `Tube`: + +* buffer items (with or without a bound), +* drop items, +* signal an error, +* ignore back-pressure and still send items, +* keep only the last values in a fixed-size buffer. + +`Tube` enforces the reactive streams protocol. +For instance if you have several threads competing to send items, then items will still be emitted _serially_ by one of the threads rather than concurrently. + +== Helpers + +Mutiny Zero offers a few helpers for commonly-needed tasks, but without the intention of becoming a full-fledge reactive programming API. + +Introducing new helpers will always be done carefully by observing real world implementation patterns. + +=== `CompletionStage` helpers + +The `AsyncHelpers` class offers a few helpers to simplify developing against the `CompletionStage`, especially before Java 11. + +* `applyExceptionally` applies a function to map a failure `Throwable` to another `Throwable`. +* `composeExceptionally` applies a function to compose a failure `Throwable` to another `CompletionStage`. + +=== `Publisher` helpers + +More often than not we need a little help when dealing with a `Publisher`. + +* `collectToList` assembles all items from a `Publisher` to a `CompletionStage++<++List++>++`. diff --git a/content/docs/smallrye-mutiny-zero/main/vertx-based-reactive-streams-publishers.adoc b/content/docs/smallrye-mutiny-zero/main/vertx-based-reactive-streams-publishers.adoc new file mode 100644 index 000000000..e422a1b88 --- /dev/null +++ b/content/docs/smallrye-mutiny-zero/main/vertx-based-reactive-streams-publishers.adoc @@ -0,0 +1,29 @@ += Vert.x-based Reactive Streams Publishers +include::_attributes.adoc[] + +The `mutiny-zero-vertx-publishers` library (Maven coordinates `io.smallrye.reactive:mutiny-zero-vertx-publishers`) allows creating _Reactive Streams_ publishers from https://vertx.io/[Vert.x] streams. + +This library acts as a thin adapter between Vert.x `ReadStream` and `java.util.concurrent.Flow.Publisher` and uses _Mutiny Zero_ to expose _Reactive Streams_ compliant publishers. + +== API overview + +The entry point is the `mutiny.zero.vertxpublishers.VertxPublisher` interface that exposes 2 static factory methods. + +* `Publisher++<++T++>++ fromSupplier(Supplier++<++ReadStream++<++T++>>++ streamSupplier)` is to be used when some Vert.x API returns a `ReadStream++<++T++>++`. +* `Publisher++<++T++>++ fromFuture(Supplier++<++Future++<++? extends ReadStream++<++T++>>>++ futureStreamSupplier)` is to be used when some Vert.x API asynchronously returns a `ReadStream++<++T++>++` through a `Future`. + +The factory methods use suppliers so that the `ReadStream` instances to be adapted are on a per-subscriber basis. + +== Sample usage + +The following example makes HTTP requests to the https://api.usb.urbanobservatory.ac.uk/[Newcastle University Urban Observatory API] using the Vert.x HTTP client: + +[source,java] +---- +include::{examples-dir}/mutiny-zero-vertx-publishers/src/test/java/docsamples/UrbanObservatoryHttpClient.java[] +---- + +A new HTTP connection is issued everytime the publisher is being subscribed. +In this example the subscriber controls demand by requesting a new Vert.x `Buffer` every 500ms. + +If you run this program then you will see the JSON response being progressively printed to the standard console in chunks, every 500 milliseconds. diff --git a/content/docs/smallrye-mutiny/2/_attributes.adoc b/content/docs/smallrye-mutiny/2/_attributes.adoc new file mode 100644 index 000000000..9bd5c899d --- /dev/null +++ b/content/docs/smallrye-mutiny/2/_attributes.adoc @@ -0,0 +1,22 @@ +:project-version: 2.9.5 +// Attributes shared by every page. smallrye.io overrides the ones it needs when it publishes. +// Pages in subdirectories set :version-root: (../ per level) before including this file. +ifndef::version-root[:version-root:] +:idprefix: +:idseparator: - +:sectanchors: +:examples-dir: {version-root}_examples +:partialsdir: {version-root}_partials +:project-name: SmallRye Mutiny +:javadoc-version: latest +:versions-mutiny: 2.9.5 +:versions-vertx-bindings: 3.20.0 +:generated-dir: {version-root}_generated + +// Added by smallrye.io (tools/docs/Docs.java sync) +:javadoc-version: 2.9.5 +:relfileprefix: /docs/smallrye-mutiny/2/ +:imagesdir: /docs/smallrye-mutiny/2 +:docs-project: smallrye-mutiny +:docs-version: 2 +:docs-ref: 2.9.5 diff --git a/content/docs/smallrye-mutiny/2/_examples/java/FirstProgram.java b/content/docs/smallrye-mutiny/2/_examples/java/FirstProgram.java new file mode 100644 index 000000000..87d40de5d --- /dev/null +++ b/content/docs/smallrye-mutiny/2/_examples/java/FirstProgram.java @@ -0,0 +1,11 @@ +import io.smallrye.mutiny.Uni; + +public class FirstProgram { + + public static void main(String[] args) { + Uni.createFrom().item("hello") + .onItem().transform(item -> item + " mutiny") + .onItem().transform(String::toUpperCase) + .subscribe().with(item -> System.out.println(">> " + item)); + } +} \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/2/_examples/java/FirstProgramTest.java b/content/docs/smallrye-mutiny/2/_examples/java/FirstProgramTest.java new file mode 100644 index 000000000..d5c464230 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/_examples/java/FirstProgramTest.java @@ -0,0 +1,44 @@ +import guides.extension.SystemOut; +import guides.extension.SystemOutCaptureExtension; +import io.smallrye.mutiny.Uni; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(SystemOutCaptureExtension.class) +public class FirstProgramTest { + + @Test + public void testFirstProgram(SystemOut out) { + FirstProgram.main(new String[0]); + assertThat(out.get()).contains(">> HELLO MUTINY"); + } + + @Test + public void testUniBuilder(SystemOut out) { + // tag::uni[] + Uni uni1 = Uni.createFrom().item("hello"); + Uni uni2 = uni1.onItem().transform(item -> item + " mutiny"); + Uni uni3 = uni2.onItem().transform(String::toUpperCase); + + uni3.subscribe().with(item -> System.out.println(">> " + item)); + // end::uni[] + assertThat(out.get()).contains("HELLO MUTINY"); + assertThat(uni3.await().indefinitely()).isEqualTo("HELLO MUTINY"); + } + + @Test + public void testUniBuilder2(SystemOut out) { + // tag::uni2[] + Uni uni = Uni.createFrom().item("hello"); + + uni.onItem().transform(item -> item + " mutiny"); + uni.onItem().transform(String::toUpperCase); + + uni.subscribe().with(item -> System.out.println(">> " + item)); + // end::uni2[] + assertThat(out.get()).contains("hello"); + assertThat(uni.await().indefinitely()).isEqualTo("hello"); + } +} diff --git a/content/docs/smallrye-mutiny/2/_examples/java/guides/BranchingTest.java b/content/docs/smallrye-mutiny/2/_examples/java/guides/BranchingTest.java new file mode 100644 index 000000000..320469593 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/_examples/java/guides/BranchingTest.java @@ -0,0 +1,64 @@ +package guides; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import org.junit.jupiter.api.Test; + +import java.util.Random; + +public class BranchingTest { + + @Test + public void uni() { + // tag::pipeline[] + Random random = new Random(); + Uni.createFrom().item(() -> random.nextInt(100)) + .onItem().transformToUni(n -> { + if (n % 2 == 0) { + return evenOperation(n); + } else { + return oddOperation(n); + } + }) + .subscribe().with(System.out::println); + // end::pipeline[] + } + + // tag::branches[] + Uni evenOperation(int n) { + return Uni.createFrom().item("Even number: " + n) + .onItem().invoke(() -> System.out.println("(even branch)")); + } + + Uni oddOperation(int n) { + return Uni.createFrom().item("Odd number: " + n) + .onItem().invoke(() -> System.out.println("(odd branch)")); + } + // end::branches[] + + @Test + public void multi() { + // tag::multi-pipeline[] + Random random = new Random(); + Multi.createBy().repeating().supplier(random::nextInt).atMost(20) + .onItem().transformToUniAndMerge(n -> { + System.out.println("----"); + if (n < 0) { + return drop(); + } else if (n % 2 == 0) { + return evenOperation(n); + } else { + return oddOperation(n); + } + }) + .subscribe().with(str -> System.out.println("=> " + str)); + // end::multi-pipeline[] + } + + // tag::drop[] + Uni drop() { + return Uni.createFrom().nullItem() + .onItem().invoke(() -> System.out.println("(dropping negative value)")); + } + // end::drop[] +} diff --git a/content/docs/smallrye-mutiny/2/_examples/java/guides/CompletionStageTest.java b/content/docs/smallrye-mutiny/2/_examples/java/guides/CompletionStageTest.java new file mode 100644 index 000000000..27b0fd6ba --- /dev/null +++ b/content/docs/smallrye-mutiny/2/_examples/java/guides/CompletionStageTest.java @@ -0,0 +1,77 @@ +package guides; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.assertj.core.api.Assertions.assertThat; + +public class CompletionStageTest { + + @Test + public void testSubscribeAsCompletionStage() { + AtomicInteger counter = new AtomicInteger(); + Uni uni = Uni.createFrom().item(() -> "hello-" + counter.getAndIncrement()); + // tag::uni-subscribe-cs[] + CompletionStage cs = uni.subscribeAsCompletionStage(); + // end::uni-subscribe-cs[] + + assertThat(cs.toCompletableFuture().join()).isEqualTo("hello-0"); + + // tag::uni-subscribe-cs-twice[] + // Trigger the underlying operation twice: + CompletionStage cs1 = uni.subscribeAsCompletionStage(); + CompletionStage cs2 = uni.subscribeAsCompletionStage(); + // end::uni-subscribe-cs-twice[] + assertThat(cs1.toCompletableFuture().join()).isEqualTo("hello-1"); + assertThat(cs2.toCompletableFuture().join()).isEqualTo("hello-2"); + } + + + @Test + public void test() { + Executor executor = Runnable::run; + // tag::create-uni[] + Uni uni1 = Uni + // Create from a Completion Stage + .createFrom().completionStage( + CompletableFuture.supplyAsync(() -> "hello", executor) + ) + .onItem().transform(String::toUpperCase); + + Uni uni2 = Uni + // Create from a Completion Stage supplier (recommended) + .createFrom().completionStage( + () -> CompletableFuture.supplyAsync(() -> "hello", executor) + ) + .onItem().transform(String::toUpperCase); + // end::create-uni[] + + assertThat(uni1.await().indefinitely()).isEqualTo("HELLO"); + assertThat(uni2.await().indefinitely()).isEqualTo("HELLO"); + + // tag::create-multi[] + Multi multi1 = Multi + .createFrom().completionStage( + CompletableFuture.supplyAsync(() -> "hello", executor) + ) + .onItem().transform(String::toUpperCase); + + Multi multi2 = Multi + .createFrom().completionStage(() -> + CompletableFuture.supplyAsync(() -> "hello", executor) + ) + .onItem().transform(String::toUpperCase); + // end::create-multi[] + + assertThat(multi1.collect().asList().await().indefinitely()).containsExactly("HELLO"); + assertThat(multi2.collect().asList().await().indefinitely()).containsExactly("HELLO"); + + } +} diff --git a/content/docs/smallrye-mutiny/2/_examples/java/guides/ContextPassingTest.java b/content/docs/smallrye-mutiny/2/_examples/java/guides/ContextPassingTest.java new file mode 100644 index 000000000..00897d788 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/_examples/java/guides/ContextPassingTest.java @@ -0,0 +1,99 @@ +package guides; + +import io.smallrye.mutiny.Context; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class ContextPassingTest { + + void contextManipulation() { + + // tag::contextManipulation[] + // Create a context using key / value pairs + Context context = Context.of( + "X-CUSTOMER-ID", "1234", + "X-SPAN-ID", "foo-bar-baz" + ); + + // Get an entry + System.out.println( + context.get("X-SPAN-ID")); + + // Get an entry, use a supplier for a default value if the key is not present + System.out.println( + context.getOrElse("X-SPAN-ID", () -> "")); + + // Add an entry + context.put("foo", "bar"); + + // Remove an entry + context.delete("foo"); + // end::contextManipulation[] + } + + @Test + void sampleUsage() { + Multi pipeline = Multi.createFrom().range(1, 10); + String customerId = "1234"; + + // tag::contextSampleUsage[] + Context context = Context.of("X-CUSTOMER-ID", customerId); + + pipeline.withContext((multi, ctx) -> multi.onItem().transformToUniAndMerge(item -> makeRequest(item, ctx.get("X-CUSTOMER-ID")))) + .subscribe().with(context, item -> handleResponse(item), err -> handleFailure(err)); + // end::contextSampleUsage[] + } + + @Test + void sampleUsageAttachedContext() { + Multi pipeline = Multi.createFrom().range(1, 10); + String customerId = "1234"; + + // tag::contextAttachedSampleUsage[] + Context context = Context.of("X-CUSTOMER-ID", customerId); + + pipeline.attachContext() + .onItem().transformToUniAndMerge(item -> makeRequest(item.get(), item.context().get("X-CUSTOMER-ID"))) + .subscribe().with(context, item -> handleResponse(item), err -> handleFailure(err)); + // end::contextAttachedSampleUsage[] + } + + @Test + void builderUsage() { + Context context = Context.of("X-SPAN-ID", "1234"); + + // tag::builderUsage[] + Uni.createFrom().context(ctx -> makeRequest("db1", ctx.get("X-SPAN-ID"))) + .subscribe().with(context, item -> handleResponse(item), err -> handleFailure(err)); + // end::builderUsage[] + } + + @Test + void emitterUsage() { + Context context = Context.of("X-SPAN-ID", "1234"); + + // tag::emitterUsage[] + Multi.createFrom().emitter(emitter -> { + String customerId = emitter.context().get("X-SPAN-ID"); + for (int i = 0; i < 10; i++) { + emitter.emit("@" + i + " [" + customerId + "]"); + } + emitter.complete(); + }); + // end::emitterUsage[] + } + + private void handleFailure(Throwable err) { + Assertions.fail(err); + } + + private void handleResponse(String item) { + Assertions.assertTrue(item.endsWith("::1234")); + } + + private Uni makeRequest(Object item, Object o) { + return Uni.createFrom().item(item + "::" + o); + } +} diff --git a/content/docs/smallrye-mutiny/2/_examples/java/guides/EventsTest.java b/content/docs/smallrye-mutiny/2/_examples/java/guides/EventsTest.java new file mode 100644 index 000000000..85fd1d709 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/_examples/java/guides/EventsTest.java @@ -0,0 +1,60 @@ +package guides; + +import guides.extension.SystemOut; +import guides.extension.SystemOutCaptureExtension; +import io.smallrye.mutiny.Uni; +import org.junit.jupiter.api.Test; + +import io.smallrye.mutiny.Multi; +import org.junit.jupiter.api.extension.ExtendWith; + +import static org.awaitility.Awaitility.await; + +@ExtendWith(SystemOutCaptureExtension.class) +public class EventsTest { + + private void log(String s) { + System.out.println(s); + } + + @SuppressWarnings("Convert2MethodRef") + @Test + public void test(SystemOut out) { + // tag::code[] + Multi source = Multi.createFrom().items("a", "b", "c"); + source + .onItem() // Called for every item + .invoke(item -> log("Received item " + item)) + .onFailure() // Called on failure + .invoke(failure -> log("Failed with " + failure)) + .onCompletion() // Called when the stream completes + .invoke(() -> log("Completed")) + .onSubscription() // Called when the upstream is ready + .invoke(subscription -> log("We are subscribed!")) + .onCancellation() // Called when the downstream cancels + .invoke(() -> log("Cancelled :-(")) + .onRequest() // Called on downstream requests + .invoke(n -> log("Downstream requested " + n + " items")) + .subscribe() + .with(item -> log("Subscriber received " + item)); + // end::code[] + + // + Multi multi = Multi.createFrom().items("a", "b", "c"); + multi.invoke(item -> System.out.println("Received item " + item)); + // + + // + multi.call(item -> executeAnAsyncAction(item)); + // + + await().until(() -> out.get().contains("We are subscribed!")); + await().until(() -> out.get().contains("Downstream requested")); + await().until(() -> out.get().contains("Received item c")); + await().until(() -> out.get().contains("Completed")); + } + + private Uni executeAnAsyncAction(String item) { + return Uni.createFrom().nullItem(); + } +} diff --git a/content/docs/smallrye-mutiny/2/_examples/java/guides/PlugTest.java b/content/docs/smallrye-mutiny/2/_examples/java/guides/PlugTest.java new file mode 100644 index 000000000..c696891a3 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/_examples/java/guides/PlugTest.java @@ -0,0 +1,58 @@ +package guides; + +import guides.extension.SystemOut; +import guides.extension.SystemOutCaptureExtension; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.operators.multi.AbstractMultiOperator; +import io.smallrye.mutiny.operators.multi.MultiOperatorProcessor; +import io.smallrye.mutiny.subscription.MultiSubscriber; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.util.concurrent.ThreadLocalRandom; + +import static org.assertj.core.api.Assertions.assertThat; + +@SuppressWarnings("InnerClassMayBeStatic") +@ExtendWith(SystemOutCaptureExtension.class) +public class PlugTest { + + @Test + public void plug(SystemOut out) { + // tag::plug[] + Multi.createFrom() + .range(1, 101) + .plug(RandomDrop::new) + .subscribe().with(System.out::println); + // end::plug[] + + assertThat(out.get()).isNotBlank(); + } + + // tag::custom-operator[] + public class RandomDrop extends AbstractMultiOperator { + public RandomDrop(Multi upstream) { + super(upstream); + } + + @Override + public void subscribe(MultiSubscriber downstream) { + upstream.subscribe().withSubscriber(new DropProcessor(downstream)); + } + + private class DropProcessor extends MultiOperatorProcessor { + DropProcessor(MultiSubscriber downstream) { + super(downstream); + } + + @Override + public void onItem(T item) { + if (ThreadLocalRandom.current().nextBoolean()) { + super.onItem(item); + } + } + } + } + // end::custom-operator[] +} diff --git a/content/docs/smallrye-mutiny/2/_examples/java/guides/RxTest.java b/content/docs/smallrye-mutiny/2/_examples/java/guides/RxTest.java new file mode 100644 index 000000000..ba439fe5c --- /dev/null +++ b/content/docs/smallrye-mutiny/2/_examples/java/guides/RxTest.java @@ -0,0 +1,111 @@ +package guides; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class RxTest { + + @Test + public void rx() { + Multi multi = Multi.createFrom().range(1, 3); + Uni uni = Uni.createFrom().item(1); + // tag::rx[] + + int result = uni + .map(i -> i + 1) + .await().indefinitely(); + + int result2 = uni + .flatMap(i -> Uni.createFrom().item(i + 1)) + .await().indefinitely(); + + List list = multi + .map(i -> i + 1) + .collect().asList() + .await().indefinitely(); + + List list2 = multi + .flatMap(i -> Multi.createFrom().items(i, i)) + .collect().asList() + .await().indefinitely(); + + List list3 = multi + .concatMap(i -> Multi.createFrom().items(i, i)) + .collect().asList() + .await().indefinitely(); + + // end::rx[] + assertThat(result).isEqualTo(2); + assertThat(result2).isEqualTo(2); + assertThat(list).containsExactly(2, 3); + assertThat(list2).containsExactly(1, 1, 2, 2); + assertThat(list3).containsExactly(1, 1, 2, 2); + } + + @Test + public void mutiny() { + Multi multi = Multi.createFrom().range(1, 3); + Uni uni = Uni.createFrom().item(1); + // tag::mutiny[] + + int result = uni + .onItem().transform(i -> i + 1) + .await().indefinitely(); + + int result2 = uni + .onItem().transformToUni(i -> Uni.createFrom().item(i + 1)) + .await().indefinitely(); + + // Shortcut for .onItem().transformToUni + int result3 = uni + .chain(i -> Uni.createFrom().item(i + 1)) + .await().indefinitely(); + + List list = multi + .onItem().transform(i -> i + 1) + .collect().asList() + .await().indefinitely(); + + List list2 = multi + .onItem().transformToMultiAndMerge(i -> Multi.createFrom().items(i, i)) + .collect().asList() + .await().indefinitely(); + + // Equivalent to transformToMultiAndMerge but let you configure the flattening process, + // failure management, concurrency... + List list3 = multi + .onItem().transformToMulti(i -> Multi.createFrom().items(i, i)).merge() + .collect().asList() + .await().indefinitely(); + + List list4 = multi + .onItem().transformToMultiAndConcatenate(i -> Multi.createFrom().items(i, i)) + .collect().asList() + .await().indefinitely(); + + // Equivalent to transformToMultiAndConcatenate but let you configure the flattening process, + // failure management... + List list5 = multi + .onItem().transformToMulti(i -> Multi.createFrom().items(i, i)).concatenate() + .collect().asList() + .await().indefinitely(); + + + // end::mutiny[] + assertThat(result).isEqualTo(2); + assertThat(result2).isEqualTo(2); + assertThat(result3).isEqualTo(2); + assertThat(list).containsExactly(2, 3); + assertThat(list2).containsExactly(1, 1, 2, 2); + assertThat(list3).containsExactly(1, 1, 2, 2); + assertThat(list4).containsExactly(1, 1, 2, 2); + assertThat(list5).containsExactly(1, 1, 2, 2); + } + + +} diff --git a/content/docs/smallrye-mutiny/2/_examples/java/guides/ShortcutsTest.java b/content/docs/smallrye-mutiny/2/_examples/java/guides/ShortcutsTest.java new file mode 100644 index 000000000..1d08c9982 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/_examples/java/guides/ShortcutsTest.java @@ -0,0 +1,29 @@ +package guides; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import org.junit.jupiter.api.Test; + +public class ShortcutsTest { + + + @Test + public void test() { + + // tag::invoke[] + Multi multi = Multi.createFrom().items("a", "b", "c"); + multi.invoke(item -> System.out.println("Received item " + item)); + // end::invoke[] + + // tag::call[] + multi.call(item -> executeAnAsyncAction(item)); + // end::call[] + + multi.subscribe().with(x -> {}); + } + + private Uni executeAnAsyncAction(String item) { + return Uni.createFrom().nullItem(); + } + +} diff --git a/content/docs/smallrye-mutiny/2/_examples/java/guides/SpiesTest.java b/content/docs/smallrye-mutiny/2/_examples/java/guides/SpiesTest.java new file mode 100644 index 000000000..bc2c297dc --- /dev/null +++ b/content/docs/smallrye-mutiny/2/_examples/java/guides/SpiesTest.java @@ -0,0 +1,40 @@ +package guides; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.helpers.spies.MultiGlobalSpy; +import io.smallrye.mutiny.helpers.spies.MultiOnCompletionSpy; +import io.smallrye.mutiny.helpers.spies.MultiOnRequestSpy; +import io.smallrye.mutiny.helpers.spies.Spy; +import org.junit.jupiter.api.Test; + +public class SpiesTest { + + @Test + public void trackItems() { + // tag::selected[] + Multi multi = Multi.createFrom().items(1, 2, 3); + MultiOnRequestSpy requestSpy = Spy.onRequest(multi); + MultiOnCompletionSpy completionSpy = Spy.onCompletion(requestSpy); + + completionSpy.subscribe().with(System.out::println); + + System.out.println("Number of requests: " + requestSpy.requestedCount()); + System.out.println("Completed? " + completionSpy.invoked()); + // end::selected[] + } + + @Test + public void trackEverything() { + // tag::global[] + Multi multi = Multi.createFrom().items(1, 2, 3); + MultiGlobalSpy spy = Spy.globally(multi); + + spy.subscribe().with(System.out::println); + + System.out.println("Number of requests: " + spy.onRequestSpy().requestedCount()); + System.out.println("Cancelled? " + spy.onCancellationSpy().isCancelled()); + System.out.println("Failure? " + spy.onFailureSpy().lastFailure()); + System.out.println("Items: " + spy.onItemSpy().items()); + // end::global[] + } +} \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/2/_examples/java/guides/TestSubscribersTest.java b/content/docs/smallrye-mutiny/2/_examples/java/guides/TestSubscribersTest.java new file mode 100644 index 000000000..3def15e38 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/_examples/java/guides/TestSubscribersTest.java @@ -0,0 +1,54 @@ +package guides; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.helpers.test.AssertSubscriber; +import io.smallrye.mutiny.helpers.test.UniAssertSubscriber; +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +public class TestSubscribersTest { + + @Test + void uni() { + // tag::uni[] + Uni uni = Uni.createFrom().item(63); + + UniAssertSubscriber subscriber = uni + .subscribe().withSubscriber(UniAssertSubscriber.create()); + + subscriber + .awaitItem() + .assertItem(63); + // end::uni[] + } + + @Test + void multi() { + // tag::multi[] + Multi multi = Multi.createFrom().range(1, 5) + .onItem().transform(n -> n * 10); + + AssertSubscriber subscriber = multi.subscribe().withSubscriber(AssertSubscriber.create(10)); + + subscriber + .awaitCompletion() + .assertItems(10, 20, 30, 40); + // end::multi[] + } + + @Test + void failing() { + // tag::failing[] + Multi multi = Multi.createFrom().failure(() -> new IOException("Boom")); + + AssertSubscriber subscriber = multi + .subscribe().withSubscriber(AssertSubscriber.create(10)); + + subscriber + .awaitFailure() + .assertFailedWith(IOException.class, "Boom"); + // end::failing[] + } +} diff --git a/content/docs/smallrye-mutiny/2/_examples/java/guides/UncheckedTest.java b/content/docs/smallrye-mutiny/2/_examples/java/guides/UncheckedTest.java new file mode 100644 index 000000000..1d15e5bd4 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/_examples/java/guides/UncheckedTest.java @@ -0,0 +1,69 @@ +package guides; + +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.unchecked.Unchecked; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.io.UncheckedIOException; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@SuppressWarnings("Convert2MethodRef") +public class UncheckedTest { + + @Test + public void rethrow() { + Uni item = Uni.createFrom().item(1); + + // tag::rethrow[] + Uni uni = item.onItem().transform(i -> { + try { + return methodThrowingIoException(i); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + // end::rethrow[] + + assertThatThrownBy(() -> uni.await().indefinitely()) + .isInstanceOf(RuntimeException.class).hasCauseInstanceOf(IOException.class); + + } + + @Test + public void transformExample() { + Uni item = Uni.createFrom().item(1); + + // tag::transform[] + Uni uni = item.onItem().transform(Unchecked.function(i -> { + // Can throw checked exception + return methodThrowingIoException(i); + })); + // end::transform[] + + assertThatThrownBy(() -> uni.await().indefinitely()) + .isInstanceOf(RuntimeException.class).hasCauseInstanceOf(IOException.class); + + } + + @Test + public void invokeExample() { + Uni item = Uni.createFrom().item(1); + + // tag::invoke[] + Uni uni = item.onItem().invoke(Unchecked.consumer(i -> { + // Can throw checked exception + throw new IOException("boom"); + })); + // end::invoke[] + + assertThatThrownBy(() -> uni.await().indefinitely()) + .isInstanceOf(RuntimeException.class).hasCauseInstanceOf(IOException.class); + + } + + private int methodThrowingIoException(Integer ignored) throws IOException { + throw new IOException("boom"); + } +} diff --git a/content/docs/smallrye-mutiny/2/_examples/java/guides/UniMultiComparisonTest.java b/content/docs/smallrye-mutiny/2/_examples/java/guides/UniMultiComparisonTest.java new file mode 100644 index 000000000..d1df9017c --- /dev/null +++ b/content/docs/smallrye-mutiny/2/_examples/java/guides/UniMultiComparisonTest.java @@ -0,0 +1,63 @@ +package guides; + +import guides.extension.SystemOut; +import guides.extension.SystemOutCaptureExtension; +import org.junit.jupiter.api.Test; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import org.junit.jupiter.api.extension.ExtendWith; + +import static org.awaitility.Awaitility.await; + +@ExtendWith(SystemOutCaptureExtension.class) +public class UniMultiComparisonTest { + + @Test + public void comparison(SystemOut out) { + //tag::code[] + Multi.createFrom().items("a", "b", "c") + .onItem().transform(String::toUpperCase) + .subscribe().with( + item -> System.out.println("Received: " + item), + failure -> System.out.println("Failed with " + failure) + ); + + Uni.createFrom().item("a") + .onItem().transform(String::toUpperCase) + .subscribe().with( + item -> System.out.println("Received: " + item), + failure -> System.out.println("Failed with " + failure) + ); + //end::code[] + + await().until(() -> out.get().contains("Received: C")); + } + + @Test + public void conversion(SystemOut out) { + // + Multi.createFrom().items("a", "b", "c") + .onItem().transform(String::toUpperCase) + // Convert the multi to uni + // It only emits the first item ("a") + .toUni() + .subscribe().with( + item -> System.out.println("Received: " + item), + failure -> System.out.println("Failed with " + failure)); + + Uni.createFrom().item("a") + .onItem().transform(String::toUpperCase) + // Convert the uni to a multi, + // the completion event will be fired after "a": + .toMulti() + .subscribe().with( + item -> System.out.println("Received: " + item), + failure -> System.out.println("Failed with " + failure) + ); + + // + + await().until(() -> out.get().contains("Received: A")); + } +} diff --git a/content/docs/smallrye-mutiny/2/_examples/java/guides/UniNullTest.java b/content/docs/smallrye-mutiny/2/_examples/java/guides/UniNullTest.java new file mode 100644 index 000000000..53ddbeb02 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/_examples/java/guides/UniNullTest.java @@ -0,0 +1,48 @@ +package guides; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class UniNullTest { + + @Test + public void uni() { + Uni uni = Uni.createFrom().item(() -> null); + // tag::code[] + uni.onItem().ifNull().continueWith("hello"); + uni.onItem().ifNull().switchTo(() -> Uni.createFrom().item("hello")); + uni.onItem().ifNull().failWith(() -> new Exception("Boom!")); + // end::code[] + + assertThat(uni.onItem().ifNull().continueWith("hello").await().indefinitely()).isEqualTo("hello"); + } + + @Test + public void uniNotNull() { + Uni uni = Uni.createFrom().item(() -> null); + // tag::code-not-null[] + uni + .onItem().ifNotNull().transform(String::toUpperCase) + .onItem().ifNull().continueWith("yolo!"); + // end::code-not-null[] + + String r = uni + .onItem().ifNotNull().transform(String::toUpperCase) + .onItem().ifNull().continueWith("yolo!") + .await().indefinitely(); + assertThat(r).isEqualTo("yolo!"); + } + + @Test + public void accumulate() { + Multi multi = Multi.createFrom().range(1, 3); + // + Multi added = multi.onItem().scan(() -> 0, (item, acc) -> acc + item); + // + assertThat(added.subscribe().asIterable()).containsExactly(0, 1, 3); + + } +} diff --git a/content/docs/smallrye-mutiny/2/_examples/java/guides/UniTimeoutTest.java b/content/docs/smallrye-mutiny/2/_examples/java/guides/UniTimeoutTest.java new file mode 100644 index 000000000..3dcd602ca --- /dev/null +++ b/content/docs/smallrye-mutiny/2/_examples/java/guides/UniTimeoutTest.java @@ -0,0 +1,102 @@ +package guides; + +import io.smallrye.mutiny.TimeoutException; +import io.smallrye.mutiny.Uni; +import org.junit.jupiter.api.Test; + +import javax.naming.ServiceUnavailableException; +import java.time.Duration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@SuppressWarnings("Convert2MethodRef") +public class UniTimeoutTest { + + @Test + public void test() { + Uni uni = Uni.createFrom().nothing(); + // tag::code[] + Uni uniWithTimeout = uni + .ifNoItem().after(Duration.ofMillis(100)) + .recoverWithItem("some fallback item"); + // end::code[] + String item = uniWithTimeout + .await().indefinitely(); + assertThat(item).isEqualTo("some fallback item"); + } + + @Test + public void testFail() { + Uni uni = Uni.createFrom().nothing(); + // tag::fail[] + Uni uniWithTimeout = uni + .ifNoItem().after(Duration.ofMillis(100)).fail(); + // end::fail[] + assertThatThrownBy(() -> + uniWithTimeout + .await().indefinitely() + ).isInstanceOf(TimeoutException.class); + } + + @Test + public void testFailRecover() { + Uni uni = Uni.createFrom().nothing(); + // tag::fail-recover[] + Uni uniWithTimeout = uni + .ifNoItem().after(Duration.ofMillis(100)).fail() + .onFailure(TimeoutException.class).recoverWithItem("we got a timeout"); + // end::fail-recover[] + String item = uniWithTimeout + .await().indefinitely(); + assertThat(item).isEqualTo("we got a timeout"); + } + + @Test + public void testFailWithSpecificException() { + Uni uni = Uni.createFrom().nothing(); + // tag::fail-with[] + Uni uniWithTimeout = uni + .ifNoItem().after(Duration.ofMillis(100)).failWith(() -> new ServiceUnavailableException()); + // end::fail-with[] + assertThatThrownBy(() -> + uniWithTimeout + .await().indefinitely() + ).isInstanceOf(ServiceUnavailableException.class); + } + + @Test + public void testFallback() { + Uni uni = Uni.createFrom().nothing(); + // tag::fallback[] + Uni uniWithTimeout = uni + .ifNoItem().after(Duration.ofMillis(100)).recoverWithItem(() -> "fallback"); + // end::fallback[] + String item = uniWithTimeout + .await().indefinitely(); + assertThat(item).isEqualTo("fallback"); + } + + @Test + public void testFallbackUni() { + Uni uni = Uni.createFrom().nothing(); + // tag::fallback-uni[] + Uni uniWithTimeout = uni + .ifNoItem().after(Duration.ofMillis(100)).recoverWithUni(() -> someFallbackUni()); + // end::fallback-uni[] + String item = uniWithTimeout + .await().indefinitely(); + assertThat(item).isEqualTo("fallback"); + } + + private Uni someFallbackUni() { + return Uni.createFrom().item("fallback"); + } + + + private static class ServiceUnavailableException extends RuntimeException { + + } + + +} diff --git a/content/docs/smallrye-mutiny/2/_examples/java/guides/infrastructure/DroppedExceptionTest.java b/content/docs/smallrye-mutiny/2/_examples/java/guides/infrastructure/DroppedExceptionTest.java new file mode 100644 index 000000000..980ecd9b7 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/_examples/java/guides/infrastructure/DroppedExceptionTest.java @@ -0,0 +1,54 @@ +package guides.infrastructure; + +import guides.extension.SystemOut; +import guides.extension.SystemOutCaptureExtension; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.infrastructure.Infrastructure; +import io.smallrye.mutiny.subscription.Cancellable; +import io.smallrye.mutiny.subscription.UniEmitter; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; + +@ExtendWith(SystemOutCaptureExtension.class) +public class DroppedExceptionTest { + + @Test + public void droppedExceptionTest(SystemOut out) { + // tag::override-handler[] + Infrastructure.setDroppedExceptionHandler(err -> + log(Level.SEVERE, "Mutiny dropped exception", err) + ); + // end::override-handler[] + + // tag::code[] + Cancellable cancellable = Uni.createFrom() + .emitter(this::emitter) + .onCancellation().call(() -> Uni.createFrom().failure(new IOException("boom"))) + .subscribe().with(this::onItem, this::onFailure); + + cancellable.cancel(); + // end::code[] + Assertions.assertThat(out.get()).contains("Mutiny dropped exception"); + } + + private void log(Level severe, String message, Throwable err) { + System.out.println(severe.getName() + ":" + message + " " + err.getMessage()); + } + + private void onFailure(Throwable t) { + // Nothing, the snippet is for pure documentation purposes + } + + private void onItem(Object o) { + // Nothing, the snippet is for pure documentation purposes + } + + private void emitter(UniEmitter uniEmitter) { + // Nothing, the snippet is for pure documentation purposes + } +} diff --git a/content/docs/smallrye-mutiny/2/_examples/java/guides/infrastructure/InfrastructureTest.java b/content/docs/smallrye-mutiny/2/_examples/java/guides/infrastructure/InfrastructureTest.java new file mode 100644 index 000000000..61da6e036 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/_examples/java/guides/infrastructure/InfrastructureTest.java @@ -0,0 +1,39 @@ +package guides.infrastructure; + +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.infrastructure.Infrastructure; +import io.smallrye.mutiny.infrastructure.MutinyScheduler; +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.util.concurrent.Executor; + +import static org.assertj.core.api.Assertions.assertThat; + +public class InfrastructureTest { + + @Test + public void test() { + // tag::infra[] + Uni uni1 = Uni.createFrom().item(1) + .emitOn(Infrastructure.getDefaultExecutor()); + + Uni uni2 = Uni.createFrom().item(2) + .onItem().delayIt() + .onExecutor(Infrastructure.getDefaultWorkerPool()) + .by(Duration.ofMillis(10)); + // end::infra[] + + assertThat(uni1.await().indefinitely()).isEqualTo(1); + assertThat(uni2.await().indefinitely()).isEqualTo(2); + + + Executor executor = Runnable::run; + // tag::set-infra[] + Infrastructure.setDefaultExecutor(executor); + // end::set-infra[] + + assertThat(Infrastructure.getDefaultExecutor()).isEqualTo(executor); + assertThat(Infrastructure.getDefaultWorkerPool()).isInstanceOf(MutinyScheduler.class); + } +} diff --git a/content/docs/smallrye-mutiny/2/_examples/java/guides/infrastructure/OperatorLoggingTest.java b/content/docs/smallrye-mutiny/2/_examples/java/guides/infrastructure/OperatorLoggingTest.java new file mode 100644 index 000000000..32746b70a --- /dev/null +++ b/content/docs/smallrye-mutiny/2/_examples/java/guides/infrastructure/OperatorLoggingTest.java @@ -0,0 +1,36 @@ +package guides.infrastructure; + +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.infrastructure.Infrastructure; + +public class OperatorLoggingTest { + + @Test + public void test() { + // tag::set-logger[] + Infrastructure.setOperatorLogger((id, event, value, err) -> { + Logger logger = LoggerFactory.getLogger(id); + if (err != null) { + logger.info(event + "(" + err.getClass() + "(" + err.getMessage() + "))"); + } else { + if (value != null) { + logger.info(event + "(" + value + ")"); + } else { + logger.info(event + "()"); + } + } + }); + // end::set-logger[] + + // tag::log[] + Multi.createFrom().items(1, 2, 3) + .onItem().transform(n -> n * 10) + .log() + .subscribe().with(item -> System.out.println(">>> " + item)); + // end::log[] + } +} diff --git a/content/docs/smallrye-mutiny/2/_examples/java/guides/integration/ImperativeToReactiveTest.java b/content/docs/smallrye-mutiny/2/_examples/java/guides/integration/ImperativeToReactiveTest.java new file mode 100644 index 000000000..cc4484456 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/_examples/java/guides/integration/ImperativeToReactiveTest.java @@ -0,0 +1,60 @@ +package guides.integration; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.infrastructure.Infrastructure; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ImperativeToReactiveTest { + + + @Test + public void uniRunSubscriptionOn() { + // tag::uni-runSubscriptionOn[] + Uni uni = Uni.createFrom() + .item(this::invokeRemoteServiceUsingBlockingIO) + .runSubscriptionOn(Infrastructure.getDefaultWorkerPool()); + // end::uni-runSubscriptionOn[] + + String res = uni.await().indefinitely(); + assertThat(res).isEqualTo("Hello"); + } + + @Test + public void multiEmitOn() { + // tag::multi-emitOn[] + Multi multi = Multi.createFrom().items("john", "jack", "sue") + .emitOn(Infrastructure.getDefaultWorkerPool()) + .onItem().transform(this::invokeRemoteServiceUsingBlockingIO); + // end::multi-emitOn[] + + List strings = multi.subscribe().asStream().collect(Collectors.toList()); + assertThat(strings).containsExactly("JOHN", "JACK", "SUE"); + } + + public String invokeRemoteServiceUsingBlockingIO() { + assertThat(Thread.currentThread().getName()).doesNotContain("main"); + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + return "Hello"; + } + + public String invokeRemoteServiceUsingBlockingIO(String s) { + assertThat(Thread.currentThread().getName()).doesNotContain("main"); + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + return s.toUpperCase(); + } + +} diff --git a/content/docs/smallrye-mutiny/2/_examples/java/guides/integration/ReactiveToImperativeTest.java b/content/docs/smallrye-mutiny/2/_examples/java/guides/integration/ReactiveToImperativeTest.java new file mode 100644 index 000000000..d4cb2397e --- /dev/null +++ b/content/docs/smallrye-mutiny/2/_examples/java/guides/integration/ReactiveToImperativeTest.java @@ -0,0 +1,57 @@ +package guides.integration; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; + +@SuppressWarnings("unchecked") +public class ReactiveToImperativeTest { + + @Test + public void testAwait() { + Uni uni = (Uni) Uni.createFrom().item(() -> 1); + + // tag::await[] + T t = uni.await().indefinitely(); + // end::await[] + + assertThat(t).isEqualTo(1); + } + + @Test + public void testAwaitAtMost() { + Uni uni = (Uni) Uni.createFrom().item(() -> 1); + + // tag::atMost[] + T t = uni.await().atMost(Duration.ofSeconds(1)); + // end::atMost[] + + assertThat(t).isEqualTo(1); + } + + @Test + public void test() { + Multi multi = (Multi) Multi.createFrom().items(1, 2, 3); + + // tag::iterable[] + Iterable iterable = multi.subscribe().asIterable(); + for (T item : iterable) { + doSomethingWithItem(item); + } + // end::iterable[] + + // tag::stream[] + Stream stream = multi.subscribe().asStream(); + stream.forEach(this::doSomethingWithItem); + // end::stream[] + } + + private void doSomethingWithItem(T item) { + assertThat(item).isIn(1, 2, 3); + } +} diff --git a/content/docs/smallrye-mutiny/2/_examples/java/guides/integration/ReactorTest.java b/content/docs/smallrye-mutiny/2/_examples/java/guides/integration/ReactorTest.java new file mode 100644 index 000000000..83f629379 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/_examples/java/guides/integration/ReactorTest.java @@ -0,0 +1,158 @@ +package guides.integration; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.converters.multi.MultiReactorConverters; +import io.smallrye.mutiny.converters.uni.UniReactorConverters; +import mutiny.zero.flow.adapters.AdaptersToFlow; +import mutiny.zero.flow.adapters.AdaptersToReactiveStreams; +import org.junit.jupiter.api.Test; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.List; +import java.util.concurrent.Flow; + +import static org.assertj.core.api.Assertions.assertThat; + +@SuppressWarnings("unchecked") +public class ReactorTest { + + @Test + public void testMultiCreation() { + Flux flux = getFlux(); + Mono mono = getMono(); + + // tag::reactor-multi-create[] + Flow.Publisher fluxAsPublisher = AdaptersToFlow.publisher(flux); + Multi multiFromFlux = Multi.createFrom().publisher(fluxAsPublisher); + + Flow.Publisher monoAsPublisher = AdaptersToFlow.publisher(mono); + Multi multiFromMono = Multi.createFrom().publisher(monoAsPublisher); + // end::reactor-multi-create[] + + List list = multiFromFlux + .onItem().transform(Object::toString) + .collect().asList().await().indefinitely(); + assertThat(list).containsExactly("a", "b", "c"); + + list = multiFromMono + .onItem().transform(Object::toString) + .collect().asList().await().indefinitely(); + assertThat(list).containsExactly("a"); + + } + + @Test + public void testUniCreation() { + Flux flux = getFlux(); + Mono mono = getMono(); + + // tag::reactor-uni-create[] + Flow.Publisher fluxAsPublisher = AdaptersToFlow.publisher(flux); + Uni uniFromFlux = Uni.createFrom().publisher(fluxAsPublisher); + + Flow.Publisher monoAsPublisher = AdaptersToFlow.publisher(mono); + Uni uniFromMono = Uni.createFrom().publisher(monoAsPublisher); + // end::reactor-uni-create[] + + String s = uniFromFlux + .onItem().transform(Object::toString) + .await().indefinitely(); + assertThat(s).isEqualTo("a"); + + s = uniFromMono + .onItem().transform(Object::toString) + .await().indefinitely(); + assertThat(s).isEqualTo("a"); + } + + @Test + public void testCreationFromMulti() { + Multi multi = getMulti(); + + // tag::reactor-create-multi[] + Publisher multiAsLegacyPublisher = AdaptersToReactiveStreams.publisher(multi); + + Flux fluxFromMulti = Flux.from(multiAsLegacyPublisher); + Mono monoFromMulti = Mono.from(multiAsLegacyPublisher); + // end::reactor-create-multi[] + + List list = fluxFromMulti + .map(Object::toString) + .collectList() + .block(); + assertThat(list).containsExactly("a", "b", "c"); + + String s = monoFromMulti + .map(Object::toString) + .block(); + assertThat(s).isEqualTo("a"); + } + + @Test + public void testCreationFromUni() { + Uni uni = getUni(); + + // tag::reactor-create-uni[] + Flux fluxFromUni = uni.convert().with(UniReactorConverters.toFlux()); + Mono monoFromUni = uni.convert().with(UniReactorConverters.toMono()); + // end::reactor-create-uni[] + + List list = fluxFromUni + .map(Object::toString) + .collectList() + .block(); + assertThat(list).containsExactly("a"); + + String s = monoFromUni + .map(Object::toString) + .block(); + assertThat(s).isEqualTo("a"); + } + + private Flux getFlux() { + return Flux.just("a", "b", "c") + .map(s -> (T) s); + } + + private Mono getMono() { + return Mono.just("a") + .map(s -> (T) s); + } + + private Multi getMulti() { + return Multi.createFrom().items("a", "b", "c") + .map(s -> (T) s); + } + + private Uni getUni() { + return Uni.createFrom().item("a") + .map(s -> (T) s); + } + + @Test + public void uniExportToReactor() { + Uni uni = Uni.createFrom().item("hello"); + // tag::uni-export[] + Mono mono = uni.convert().with(UniReactorConverters.toMono()); + Flux flux = uni.convert().with(UniReactorConverters.toFlux()); + // end::uni-export[] + + assertThat(mono.block()).isEqualTo("hello"); + assertThat(flux.blockFirst()).isEqualTo("hello"); + } + + @Test + public void multiExportToReactor() { + Multi multi = Multi.createFrom().items("hello", "bonjour"); + // tag::multi-export[] + Mono mono = multi.convert().with(MultiReactorConverters.toMono()); + Flux flux = multi.convert().with(MultiReactorConverters.toFlux()); + // end::multi-export[] + + assertThat(mono.block()).isEqualTo("hello"); + assertThat(flux.toIterable()).containsExactly("hello", "bonjour"); + } +} diff --git a/content/docs/smallrye-mutiny/2/_examples/java/guides/integration/RxJavaTest.java b/content/docs/smallrye-mutiny/2/_examples/java/guides/integration/RxJavaTest.java new file mode 100644 index 000000000..dd1ae117c --- /dev/null +++ b/content/docs/smallrye-mutiny/2/_examples/java/guides/integration/RxJavaTest.java @@ -0,0 +1,277 @@ +package guides.integration; + +import io.reactivex.rxjava3.core.*; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.converters.multi.MultiRx3Converters; +import io.smallrye.mutiny.converters.uni.UniRx3Converters; +import mutiny.zero.flow.adapters.AdaptersToFlow; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Optional; +import java.util.concurrent.Flow; + +import static org.assertj.core.api.Assertions.assertThat; + +@SuppressWarnings("unchecked") +public class RxJavaTest { + + @Test + public void testMultiCreation() { + Observable observable = getObservable(); + Flowable flowable = getFlowable(); + + // tag::rx-multi-create-observable[] + Multi multiFromObservable = Multi.createFrom() + .converter(MultiRx3Converters.fromObservable(), observable); + // end::rx-multi-create-observable[] + + // tag::rx-multi-create-flowable[] + Flow.Publisher flowableAsPublisher = AdaptersToFlow.publisher(flowable); + + Multi multiFromFlowable = Multi.createFrom().publisher(flowableAsPublisher); + // end::rx-multi-create-flowable[] + + List list = multiFromObservable + .onItem().transform(Object::toString) + .collect().asList().await().indefinitely(); + assertThat(list).containsExactly("a", "b", "c"); + + list = multiFromFlowable + .onItem().transform(Object::toString) + .collect().asList().await().indefinitely(); + assertThat(list).containsExactly("a", "b", "c"); + + Completable completable = getCompletable(); + Single single = getSingle(); + Maybe maybe = getMaybe(); + Maybe emptyMaybe = getEmptyMaybe(); + + // tag::rx-multi-create-single[] + Multi multiFromCompletable = Multi.createFrom() + .converter(MultiRx3Converters.fromCompletable(), completable); + Multi multiFromSingle = Multi.createFrom() + .converter(MultiRx3Converters.fromSingle(), single); + Multi multiFromMaybe = Multi.createFrom() + .converter(MultiRx3Converters.fromMaybe(), maybe); + Multi multiFromEmptyMaybe = Multi.createFrom() + .converter(MultiRx3Converters.fromMaybe(), emptyMaybe); + // end::rx-multi-create-single[] + + list = multiFromCompletable + .onItem().transform(Object::toString) + .collect().asList().await().indefinitely(); + assertThat(list).isEmpty(); + + list = multiFromSingle + .onItem().transform(Object::toString) + .collect().asList().await().indefinitely(); + assertThat(list).containsExactly("a"); + + list = multiFromMaybe + .onItem().transform(Object::toString) + .collect().asList().await().indefinitely(); + assertThat(list).containsExactly("a"); + + list = multiFromEmptyMaybe + .onItem().transform(Object::toString) + .collect().asList().await().indefinitely(); + assertThat(list).isEmpty(); + } + + @Test + public void testUniCreation() { + Observable observable = getObservable(); + Flowable flowable = getFlowable(); + + // tag::rx-uni-create-observable[] + Uni uniFromObservable = Uni.createFrom().converter( + UniRx3Converters.fromObservable(), observable); + // end::rx-uni-create-observable[] + + // tag::rx-uni-create-flowable[] + Flow.Publisher flowableAsPublisher = AdaptersToFlow.publisher(flowable); + + Uni uniFromFlowable = Uni.createFrom().publisher(flowableAsPublisher); + // end::rx-uni-create-flowable[] + + String s = uniFromFlowable + .onItem().transform(Object::toString) + .await().indefinitely(); + assertThat(s).isEqualTo("a"); + + s = uniFromObservable + .onItem().transform(Object::toString) + .await().indefinitely(); + assertThat(s).isEqualTo("a"); + + Completable completable = getCompletable(); + Single single = getSingle(); + Maybe maybe = getMaybe(); + Maybe emptyMaybe = getEmptyMaybe(); + + // tag::rx-uni-create-single[] + Uni multiFromCompletable = Uni.createFrom() + .converter(UniRx3Converters.fromCompletable(), completable); + Uni multiFromSingle = Uni.createFrom() + .converter(UniRx3Converters.fromSingle(), single); + Uni multiFromMaybe = Uni.createFrom() + .converter(UniRx3Converters.fromMaybe(), maybe); + Uni multiFromEmptyMaybe = Uni.createFrom() + .converter(UniRx3Converters.fromMaybe(), emptyMaybe); + // end::rx-uni-create-single[] + + Void v = multiFromCompletable + .await().indefinitely(); + assertThat(v).isNull(); + + s = multiFromSingle + .onItem().transform(Object::toString) + .await().indefinitely(); + assertThat(s).isEqualTo("a"); + + s = multiFromMaybe + .onItem().transform(Object::toString) + .await().indefinitely(); + assertThat(s).isEqualTo("a"); + + s = multiFromEmptyMaybe + .onItem().castTo(String.class) + .await().indefinitely(); + assertThat(s).isNull(); + } + + @Test + public void testCreatingRxFromMulti() { + Multi multi = getMulti(); + + // tag::create-rx-from-multi[] + Completable completable = multi.convert() + .with(MultiRx3Converters.toCompletable()); + Single> single = multi.convert() + .with(MultiRx3Converters.toSingle()); + Single single2 = multi.convert() + .with(MultiRx3Converters + .toSingle().onEmptyThrow(() -> new Exception("D'oh!"))); + Maybe maybe = multi.convert() + .with(MultiRx3Converters.toMaybe()); + Observable observable = multi.convert() + .with(MultiRx3Converters.toObservable()); + Flowable flowable = multi.convert() + .with(MultiRx3Converters.toFlowable()); + // end::create-rx-from-multi[] + + completable.blockingAwait(); + assertThat(single.blockingGet()).contains((T) "a"); + assertThat(single2.blockingGet()).isEqualTo("a"); + assertThat(maybe.blockingGet()).isEqualTo("a"); + assertThat(observable.blockingIterable()).containsExactly((T) "a", (T) "b", (T) "c"); + assertThat(flowable.blockingIterable()).containsExactly((T) "a", (T) "b", (T) "c"); + } + + @Test + public void testCreatingRxFromUni() { + Uni uni = getUni(); + + // tag::create-rx-from-uni[] + Completable completable = uni.convert().with(UniRx3Converters.toCompletable()); + Single> single = uni.convert().with(UniRx3Converters.toSingle()); + Single single2 = uni.convert().with(UniRx3Converters.toSingle().failOnNull()); + Maybe maybe = uni.convert().with(UniRx3Converters.toMaybe()); + Observable observable = uni.convert().with(UniRx3Converters.toObservable()); + Flowable flowable = uni.convert().with(UniRx3Converters.toFlowable()); + // end::create-rx-from-uni[] + + completable.blockingAwait(); + assertThat(single.blockingGet()).contains((T) "a"); + assertThat(single2.blockingGet()).isEqualTo("a"); + assertThat(maybe.blockingGet()).isEqualTo("a"); + assertThat(observable.blockingIterable()).containsExactly((T) "a"); + assertThat(flowable.blockingIterable()).containsExactly((T) "a"); + } + + @Test + public void uniExportToRx() { + Uni uni = Uni.createFrom().item("hello"); + // + Completable completable = uni.convert().with(UniRx3Converters.toCompletable()); + Single> single = uni.convert().with(UniRx3Converters.toSingle()); + Single single2 = uni.convert().with(UniRx3Converters.toSingle().failOnNull()); + Maybe maybe = uni.convert().with(UniRx3Converters.toMaybe()); + Observable observable = uni.convert().with(UniRx3Converters.toObservable()); + Flowable flowable = uni.convert().with(UniRx3Converters.toFlowable()); + // + + completable.test().assertComplete(); + single.test().assertValue(o -> o.isPresent() && o.get().equals("hello")); + single2.test().assertValue("hello"); + maybe.test().assertValue("hello"); + observable.test().assertValue("hello").assertComplete(); + flowable.test().assertValue("hello").assertComplete(); + } + + @Test + public void multiExportToRx() { + Multi multi = Multi.createFrom().items("hello", "bonjour"); + // + Completable completable = multi.convert() + .with(MultiRx3Converters.toCompletable()); + Single> single = multi.convert() + .with(MultiRx3Converters.toSingle()); + Single single2 = multi.convert() + .with(MultiRx3Converters.toSingle().onEmptyThrow(() -> new Exception("D'oh!"))); + Maybe maybe = multi.convert() + .with(MultiRx3Converters.toMaybe()); + Observable observable = multi.convert() + .with(MultiRx3Converters.toObservable()); + Flowable flowable = multi.convert() + .with(MultiRx3Converters.toFlowable()); + // + + completable.test().assertComplete(); + single.test().assertValue(o -> o.isPresent() && o.get().equals("hello")); + single2.test().assertValue("hello"); + maybe.test().assertValue("hello"); + observable.test().assertValues("hello", "bonjour").assertComplete(); + flowable.test().assertValues("hello", "bonjour").assertComplete(); + } + + private Observable getObservable() { + return Observable.just("a", "b", "c") + .map(s -> (T) s); + } + + private Flowable getFlowable() { + return Flowable.just("a", "b", "c") + .map(s -> (T) s); + } + + private Single getSingle() { + return Single.just("a") + .map(s -> (T) s); + } + + private Maybe getMaybe() { + return Maybe.just("a") + .map(s -> (T) s); + } + + private Maybe getEmptyMaybe() { + return Maybe.empty(); + } + + private Completable getCompletable() { + return Completable.complete(); + } + + private Multi getMulti() { + return Multi.createFrom().items("a", "b", "c") + .map(s -> (T) s); + } + + private Uni getUni() { + return Uni.createFrom().item("a") + .map(s -> (T) s); + } +} diff --git a/content/docs/smallrye-mutiny/2/_examples/java/guides/operators/BroadcastProcessorTest.java b/content/docs/smallrye-mutiny/2/_examples/java/guides/operators/BroadcastProcessorTest.java new file mode 100644 index 000000000..36172511c --- /dev/null +++ b/content/docs/smallrye-mutiny/2/_examples/java/guides/operators/BroadcastProcessorTest.java @@ -0,0 +1,35 @@ +package guides.operators; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.helpers.test.AssertSubscriber; +import io.smallrye.mutiny.operators.multi.processors.BroadcastProcessor; +import org.junit.jupiter.api.Test; + +public class BroadcastProcessorTest { + + @Test + public void test() { + // tag::code[] + BroadcastProcessor processor = BroadcastProcessor.create(); + Multi multi = processor + .onItem().transform(String::toUpperCase) + .onFailure().recoverWithItem("d'oh"); + + new Thread(() -> { + for (int i = 0; i < 1000; i++) { + processor.onNext(Integer.toString(i)); + } + processor.onComplete(); + }).start(); + + // Subscribers can subscribe at any time. + // They will only receive items emitted after their subscription. + // If the source is already terminated (by a completion or a failure signal) + // the subscriber receives this signal. + + // end::code[] + AssertSubscriber subscriber = AssertSubscriber.create(Long.MAX_VALUE); + multi.subscribe().withSubscriber(subscriber) + .awaitCompletion(); + } +} diff --git a/content/docs/smallrye-mutiny/2/_examples/java/guides/operators/CollectingItemsTest.java b/content/docs/smallrye-mutiny/2/_examples/java/guides/operators/CollectingItemsTest.java new file mode 100644 index 000000000..b48d62c53 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/_examples/java/guides/operators/CollectingItemsTest.java @@ -0,0 +1,99 @@ +package guides.operators; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.tuples.Tuple2; +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.util.*; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.*; + +@SuppressWarnings("Convert2MethodRef") +public class CollectingItemsTest { + + @Test + public void testList() { + // tag::list[] + Multi multi = getMulti(); + Uni> uni = multi.collect().asList(); + // end::list[] + + assertThat(uni.await().indefinitely()).containsExactly("a", "b", "c"); + + // tag::first[] + Uni first = multi.collect().first(); + Uni last = multi.collect().last(); + // end::first[] + + assertThat(first.await().indefinitely()).isEqualTo("a"); + assertThat(last.await().indefinitely()).isEqualTo("c"); + } + + @Test + public void testMap() { + // tag::map[] + Multi multi = getMulti(); + Uni> uni = + multi.collect() + .asMap(item -> getUniqueKeyForItem(item)); + // end::map[] + + assertThat(uni.await().indefinitely()).hasSize(3); + } + + @Test + public void testMultiMap() { + // tag::multimap[] + Multi multi = getMulti(); + Uni>> uni = + multi.collect() + .asMultiMap(item -> getKeyForItem(item)); + // end::multimap[] + + assertThat(uni.await().indefinitely()).hasSize(2); + } + + @Test + public void testCustomAccumulator() { + // tag::accumulator[] + Multi multi = getMulti(); + Uni uni = multi.collect() + .in(MyCollection::new, (col, item) -> col.add(item)); + // end::accumulator[] + + assertThat(uni.await().indefinitely()).hasSize(3); + + // tag::collector[] + Uni count = multi.collect() + .with(Collectors.counting()); + // end::collector[] + + assertThat(count.await().indefinitely()).isEqualTo(3); + } + + private String getKeyForItem(String item) { + if( item.equalsIgnoreCase("b")) { + return "b"; + } else { + return "a"; + } + } + + private String getUniqueKeyForItem(String item) { + return item; + } + + private Multi getMulti() { + return Multi.createFrom().items("a", "b", "c"); + } + + private static class MyCollection extends ArrayList { + + } +} diff --git a/content/docs/smallrye-mutiny/2/_examples/java/guides/operators/CombiningItemsTest.java b/content/docs/smallrye-mutiny/2/_examples/java/guides/operators/CombiningItemsTest.java new file mode 100644 index 000000000..1f02148fd --- /dev/null +++ b/content/docs/smallrye-mutiny/2/_examples/java/guides/operators/CombiningItemsTest.java @@ -0,0 +1,177 @@ +package guides.operators; + +import guides.extension.SystemOut; +import guides.extension.SystemOutCaptureExtension; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.tuples.Tuple2; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.time.Duration; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +@SuppressWarnings({ "unchecked", "Convert2MethodRef" }) +@ExtendWith(SystemOutCaptureExtension.class) +public class CombiningItemsTest { + + @Test + public void testWithUni(SystemOut out) throws InterruptedException { + // tag::invocations[] + Uni uniA = invokeHttpServiceA(); + Uni uniB = invokeHttpServiceB(); + // end::invocations[] + + // tag::combination[] + Uni> responses = Uni.combine() + .all().unis(uniA, uniB).asTuple(); + // end::combination[] + + // tag::subscription[] + Uni.combine().all().unis(uniA, uniB).asTuple() + .subscribe().with(tuple -> { + System.out.println("Response from A: " + tuple.getItem1()); + System.out.println("Response from B: " + tuple.getItem2()); + }); + // end::subscription[] + + await().until(() -> out.get().contains("Response from A: Response{content='A'}")); + await().until(() -> out.get().contains("Response from B: Response{content='B'}")); + + // tag::combined-with[] + Uni> uni = Uni.combine() + .all().unis(uniA, uniB).with( + listOfResponses -> { + Map map = new LinkedHashMap<>(); + map.put("A", (Response) listOfResponses.get(0)); + map.put("B", (Response) listOfResponses.get(1)); + return map; + } + ); + // end::combined-with[] + + assertThat(uni.await().indefinitely()).containsKeys("A", "B"); + + // + Uni> uni1 = Uni.combine() + .all().unis(uniA, uniB).withUni( + listOfResponses -> { + Map map = new LinkedHashMap<>(); + map.put("A", (Response) listOfResponses.get(0)); + map.put("B", (Response) listOfResponses.get(1)); + return Uni.createFrom().item(map); + } + ); + // + + assertThat(uni1.await().indefinitely()).containsKeys("A", "B"); + + + Thread.sleep(100); + } + + @Test + public void testWithMulti() throws InterruptedException { + Multi multiA = getMultiA(); + Multi multiB = getMultiB(); + + // tag::combine-multi[] + Multi> combined = Multi.createBy().combining() + .streams(multiA, multiB).asTuple(); + // end::combine-multi[] + + CountDownLatch latch = new CountDownLatch(1); + AtomicInteger i = new AtomicInteger(); + combined.subscribe().with( + tuple -> { + int v = i.incrementAndGet(); + assertThat(tuple.getItem1()).isEqualTo("a" + v); + assertThat(tuple.getItem2()).isEqualTo("b" + v); + }, + () -> latch.countDown() + ); + + assertThat(latch.await(10, TimeUnit.MILLISECONDS)).isTrue(); + + // tag::combine-multi-with[] + Multi.createBy().combining() + .streams(multiA, multiB).using(list -> combineItems(list)) + .subscribe().with(x -> { + // do something with the combined items + }); + // end::combine-multi-with[] + } + + @Test + public void testCombineLast() { + Multi multiA = getMultiA(); + Multi multiB = getMultiB(); + + // tag::combine-last[] + Multi> multi1 = Multi.createBy().combining() + .streams(multiA, multiB) + .latestItems().asTuple(); + + // or + + Multi multi2 = Multi.createBy().combining() + .streams(multiA, multiB) + .latestItems().using(list -> combineItems(list)); + // end::combine-last[] + + List> list = multi1.collect().asList().await().indefinitely(); + assertThat(list).hasSize(3); + assertThat(list.toString()).contains("a3", "b1", "b2", "b3").doesNotContain("a1", "a2"); + List strings = multi2.collect().asList().await().indefinitely(); + assertThat(strings).hasSize(3).containsExactly("a3b1", "a3b2", "a3b3"); + } + + private String combineItems(List list) { + return "" + list.get(0) + list.get(1); + } + + private Multi getMultiA() { + return Multi.createFrom().items("a1", "a2", "a3") + .onItem().transform(s -> (A) s); + } + + private Multi getMultiB() { + return Multi.createFrom().items("b1", "b2", "b3") + .onItem().transform(s -> (B) s); + } + + private Uni invokeHttpServiceA() { + return Uni.createFrom().item(new Response("A")) + .onItem().delayIt().by(Duration.ofMillis(10)); + } + + private Uni invokeHttpServiceB() { + return Uni.createFrom().item(new Response("B")) + .onItem().delayIt().by(Duration.ofMillis(30)); + } + + static class Response { + + private final String content; + + public Response(String content) { + this.content = content; + } + + @Override + public String toString() { + return "Response{" + + "content='" + content + '\'' + + '}'; + } + } + +} diff --git a/content/docs/smallrye-mutiny/2/_examples/java/guides/operators/ControlDemandTest.java b/content/docs/smallrye-mutiny/2/_examples/java/guides/operators/ControlDemandTest.java new file mode 100644 index 000000000..2ea2694e1 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/_examples/java/guides/operators/ControlDemandTest.java @@ -0,0 +1,93 @@ +package guides.operators; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.helpers.test.AssertSubscriber; +import io.smallrye.mutiny.infrastructure.Infrastructure; +import io.smallrye.mutiny.subscription.DemandPacer; +import io.smallrye.mutiny.subscription.FixedDemandPacer; +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; + +import static org.assertj.core.api.Assertions.assertThat; + +class ControlDemandTest { + + @Test + void pacing() { + // tag::pacing[] + FixedDemandPacer pacer = new FixedDemandPacer(25L, Duration.ofMillis(100L)); + + Multi multi = Multi.createFrom().range(0, 100) + .paceDemand().on(Infrastructure.getDefaultWorkerPool()).using(pacer); + // end::pacing[] + + AssertSubscriber sub = multi.subscribe().withSubscriber(AssertSubscriber.create()); + sub.awaitCompletion(); + assertThat(sub.getItems()).hasSize(100); + } + + @Test + void customPacer() { + // tag::custom-pacer[] + DemandPacer pacer = new DemandPacer() { + + @Override + public Request initial() { + return new Request(10L, Duration.ofMillis(100L)); + } + + @Override + public Request apply(Request previousRequest, long observedItemsCount) { + return new Request(previousRequest.demand() * 2, previousRequest.delay().plus(10, ChronoUnit.MILLIS)); + } + }; + // end::custom-pacer[] + } + + @Test + void capping() { + // tag::capConstant[] + AssertSubscriber sub = AssertSubscriber.create(); + + sub = Multi.createFrom().range(0, 100) + .capDemandsTo(50L) + .subscribe().withSubscriber(sub); + + // A first batch of 50 (capped), 25 remain outstanding + sub.request(75L).assertNotTerminated(); + assertThat(sub.getItems()).hasSize(50); + + // Second batch: 25 + 25 = 50 + sub.request(25L).assertCompleted(); + assertThat(sub.getItems()).hasSize(100); + // end::capConstant[] + } + + @Test + void cappingFunction() { + // tag::capFunction[] + AssertSubscriber sub = AssertSubscriber.create(); + + sub = Multi.createFrom().range(0, 100) + .capDemandsUsing(n -> { + if (n > 1) { + return (long) (((double) n) * 0.75d); + } else { + return n; + } + }) + .subscribe().withSubscriber(sub); + + sub.request(100L).assertNotTerminated(); + assertThat(sub.getItems()).hasSize(75); + + sub.request(1L).assertNotTerminated(); + assertThat(sub.getItems()).hasSize(94); + + sub.request(Long.MAX_VALUE).assertCompleted(); + assertThat(sub.getItems()).hasSize(100); + // end::capFunction[] + } +} diff --git a/content/docs/smallrye-mutiny/2/_examples/java/guides/operators/DelayTest.java b/content/docs/smallrye-mutiny/2/_examples/java/guides/operators/DelayTest.java new file mode 100644 index 000000000..052004e8b --- /dev/null +++ b/content/docs/smallrye-mutiny/2/_examples/java/guides/operators/DelayTest.java @@ -0,0 +1,87 @@ +package guides.operators; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.util.List; +import java.util.Random; + +import static org.assertj.core.api.Assertions.assertThat; + +public class DelayTest { + + @Test + public void testDelayBy() { + // tag::delay-by[] + Uni delayed = Uni.createFrom().item("hello") + .onItem().delayIt().by(Duration.ofMillis(10)); + // end::delay-by[] + String r = delayed.map(s -> "Delayed " + s) + .await().indefinitely(); + assertThat(r).isEqualTo("Delayed hello"); + } + + @Test + public void testDelayUntil() { + // tag::delay-until[] + Uni delayed = Uni.createFrom().item("hello") + // The write method returns a Uni completed + // when the operation is done. + .onItem().delayIt().until(this::write); + // end::delay-until[] + String r = delayed + .map(s -> "Written " + s) + .await().indefinitely(); + assertThat(r).isEqualTo("Written hello"); + } + + @Test + public void testDelayMulti() { + Multi multi = Multi.createFrom().items(1, 2, 3, 4, 5); + // tag::delay-multi[] + Multi delayed = multi + .onItem().call(i -> + // Delay the emission until the returned uni emits its item + Uni.createFrom().nullItem().onItem().delayIt().by(Duration.ofMillis(10)) + ); + // end::delay-multi[] + assertThat(delayed.collect().asList().await().indefinitely()).containsExactly(1, 2, 3, 4, 5); + } + + @Test + public void testThrottling() { + Multi multi = Multi.createFrom().items(1, 2, 3, 4, 5); + // tag::throttling-multi[] + // Introduce a one second delay between each item + Multi ticks = Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .onOverflow().drop(); + Multi delayed = Multi.createBy().combining().streams(ticks, multi) + .using((x, item) -> item); + // end::throttling-multi[] + assertThat(delayed.collect().asList().await().indefinitely()).containsExactly(1, 2, 3, 4, 5); + } + + @Test + public void testDelayMultiRandom() { + // tag::delay-multi-random[] + Random random = new Random(); + Multi delayed = Multi.createFrom().items(1, 2, 3, 4, 5) + .onItem().call(i -> { + Duration delay = Duration.ofMillis(random.nextInt(100) + 1); + return Uni.createFrom().nullItem().onItem().delayIt().by(delay); + }); + // end::delay-multi-random[] + assertThat(delayed.collect().asList() + .await().indefinitely()) + .containsExactlyInAnyOrder(1, 2, 3, 4, 5); + } + + private Uni write(String s) { + return Uni.createFrom().item(s) + .onItem().delayIt().by(Duration.ofMillis(20)) + .onItem().ignore().andContinueWithNull(); + } + +} diff --git a/content/docs/smallrye-mutiny/2/_examples/java/guides/operators/EmitOnTest.java b/content/docs/smallrye-mutiny/2/_examples/java/guides/operators/EmitOnTest.java new file mode 100644 index 000000000..c9398aa36 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/_examples/java/guides/operators/EmitOnTest.java @@ -0,0 +1,62 @@ +package guides.operators; + +import guides.extension.SystemOut; +import guides.extension.SystemOutCaptureExtension; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import static org.assertj.core.api.Assertions.assertThat; + +@SuppressWarnings("Convert2MethodRef") +@ExtendWith(SystemOutCaptureExtension.class) +public class EmitOnTest { + + @Test + public void testEmitOn() { + // tag::example[] + Uni uni = Uni.createFrom().emitter(emitter -> + new Thread(() -> + emitter.complete("hello from " + + Thread.currentThread().getName()) + ).start() + ) + .onItem().transform(item -> { + // Called on the emission thread. + return item.toUpperCase(); + }); + // end::example[] + + assertThat(uni.await().indefinitely()).startsWith("HELLO FROM").doesNotContain("MAIN"); + } + + @Test + public void test(SystemOut out) { + Uni uni = Uni.createFrom().item("hello"); + Multi multi = Multi.createFrom().items("a", "b", "c"); + + ExecutorService executor = Executors.newFixedThreadPool(4); + // tag::code[] + String res0 = uni.emitOn(executor) + .onItem() + .invoke(s -> System.out.println("Received item `" + s + "` on thread: " + + Thread.currentThread().getName())) + .await().indefinitely(); + + String res1 = multi.emitOn(executor) + .onItem() + .invoke(s -> System.out.println("Received item `" + s + "` on thread: " + + Thread.currentThread().getName())) + .collect().first() + .await().indefinitely(); + // end::code[] + + assertThat(out.get()).doesNotContain("main").contains("pool-").contains("hello", "a"); + assertThat(res0).isEqualTo("hello"); + assertThat(res1).isEqualTo("a"); + } +} diff --git a/content/docs/smallrye-mutiny/2/_examples/java/guides/operators/FilterTest.java b/content/docs/smallrye-mutiny/2/_examples/java/guides/operators/FilterTest.java new file mode 100644 index 000000000..39e72d024 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/_examples/java/guides/operators/FilterTest.java @@ -0,0 +1,101 @@ +package guides.operators; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class FilterTest { + + @Test + public void filter() { + Multi multi = Multi.createFrom().range(1, 11); + // tag::filter[] + List list = multi + .select().where(i -> i > 6) + .collect().asList() + .await().indefinitely(); + // end::filter[] + + // tag::test[] + List list2 = multi + .select().when(i -> Uni.createFrom().item(i > 6)) + .collect().asList() + .await().indefinitely(); + // end::test[] + + assertThat(list).containsExactly(7, 8, 9, 10); + assertThat(list2).containsExactly(7, 8, 9, 10); + } + + @Test + public void take() { + Multi multi = Multi.createFrom().range(1, 11); + // + List list = multi + .select().first(2) + .collect().asList() + .await().indefinitely(); + + List list2 = multi + .select().first(i -> i < 3) + .collect().asList() + .await().indefinitely(); + + List list3 = multi + .select().last(2) + .collect().asList() + .await().indefinitely(); + // + assertThat(list).containsExactly(1, 2); + assertThat(list2).containsExactly(1, 2); + assertThat(list3).containsExactly(9, 10); + } + + @Test + public void skip() { + Multi multi = Multi.createFrom().range(1, 11); + // + List list = multi + .skip().first(8) + .collect().asList() + .await().indefinitely(); + + List list2 = multi + .skip().first(i -> i < 9) + .collect().asList() + .await().indefinitely(); + + List list3 = multi + .skip().last(8) + .collect().asList() + .await().indefinitely(); + // + assertThat(list).containsExactly(9, 10); + assertThat(list2).containsExactly(9, 10); + assertThat(list3).containsExactly(1, 2); + } + + @Test + public void distinct() { + Multi multi = Multi.createFrom().items(1, 1, 2, 3, 4, 5, 5, 6); + // + List list = multi + .select().distinct() + .collect().asList() + .await().indefinitely(); + // + + // + List list2 = multi + .skip().repetitions() + .collect().asList() + .await().indefinitely(); + // + assertThat(list).containsExactly(1, 2, 3, 4, 5, 6); + assertThat(list2).containsExactly(1, 2, 3, 4, 5, 6); + } +} diff --git a/content/docs/smallrye-mutiny/2/_examples/java/guides/operators/MergeConcatTest.java b/content/docs/smallrye-mutiny/2/_examples/java/guides/operators/MergeConcatTest.java new file mode 100644 index 000000000..580404e7e --- /dev/null +++ b/content/docs/smallrye-mutiny/2/_examples/java/guides/operators/MergeConcatTest.java @@ -0,0 +1,119 @@ +package guides.operators; + +import guides.extension.SystemOut; +import guides.extension.SystemOutCaptureExtension; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.subscription.Cancellable; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.time.Duration; +import java.util.List; +import java.util.Random; +import java.util.concurrent.CopyOnWriteArrayList; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +@SuppressWarnings({ "unchecked", "Convert2MethodRef" }) +@ExtendWith(SystemOutCaptureExtension.class) +public class MergeConcatTest { + + private final Random random = new Random(); + + @Test + public void testMerge() { + // tag::merge[] + Multi multi1 = getFirstMulti(); + Multi multi2 = getSecondMulti(); + + Multi merged = Multi.createBy().merging().streams(multi1, multi2); + // end::merge[] + + List received = new CopyOnWriteArrayList<>(); + merged.subscribe().with(received::add); + await().until(() -> received.size() == 9); + assertThat(received).contains(1, 2, 3, 4, 5, 6, 7, 8, 9); + } + + @Test + public void testConcat() { + // tag::concat[] + Multi multi1 = getFirstMulti(); + Multi multi2 = getSecondMulti(); + + Multi concatenated = Multi.createBy().concatenating().streams(multi1, multi2); + // end::concat[] + + List received = new CopyOnWriteArrayList<>(); + concatenated.subscribe().with(received::add); + await().until(() -> received.size() == 9); + assertThat(received).containsExactly(1, 2, 3, 4, 5, 6, 7, 8, 9); + } + + @Test + public void testMergeTicks(SystemOut out) { + // tag::merge-ticks[] + Multi first = Multi.createFrom().ticks().every(Duration.ofMillis(10)) + .onItem().transform(l -> "Stream 1 - " + l); + + Multi second = Multi.createFrom().ticks().every(Duration.ofMillis(15)) + .onItem().transform(l -> "Stream 2 - " + l); + + Multi third = Multi.createFrom().ticks().every(Duration.ofMillis(5)) + .onItem().transform(l -> "Stream 3 - " + l); + + Cancellable cancellable = Multi.createBy().merging().streams(first, second, third) + .subscribe().with(s -> System.out.println("Got item: " + s)); + // end::merge-ticks[] + + await().until(() -> out.get().contains("Got item: Stream 3 - 10")); + cancellable.cancel(); + } + + @Test + public void testConcatenateStrings(SystemOut out) { + // tag::concatenate-strings[] + Multi first = Multi.createFrom().items("A1", "A2", "A3"); + Multi second = Multi.createFrom().items("B1", "B2", "B3"); + + Multi.createBy().concatenating().streams(first, second) + .subscribe().with(item -> System.out.print(item)); // "A1A2A3B1B2B3" + + Multi.createBy().concatenating().streams(second, first) + .subscribe().with(item -> System.out.print(item)); // "B1B2B3A1A2A3" + // end::concatenate-strings[] + + await().until(() -> out.get().contains("A1A2A3B1B2B3")); + await().until(() -> out.get().contains("B1B2B3A1A2A3")); + + assertThat( + Multi.createBy().concatenating().streams(first, second) + .collect().in(StringBuffer::new, StringBuffer::append) + .await().indefinitely() + ).isEqualToIgnoringCase("A1A2A3B1B2B3"); + + assertThat( + Multi.createBy().concatenating().streams(second, first) + .collect().in(StringBuffer::new, StringBuffer::append) + .await().indefinitely() + ).isEqualToIgnoringCase("B1B2B3A1A2A3"); + + } + + private Multi getFirstMulti() { + return Multi.createFrom().items(1, 2, 3, 4, 5, 6) + .onItem().call(() -> Uni.createFrom().nullItem().onItem().delayIt() + .by(Duration.ofMillis(random.nextInt(100) + 1))) + .onItem().transform(i -> (T) i); + } + + private Multi getSecondMulti() { + return Multi.createFrom().items(7, 8, 9) + .onItem().call(() -> Uni.createFrom().nullItem().onItem().delayIt() + .by(Duration.ofMillis(random.nextInt(100) + 1))) + .onItem().transform(i -> (T) i); + } +} diff --git a/content/docs/smallrye-mutiny/2/_examples/java/guides/operators/PaginationTest.java b/content/docs/smallrye-mutiny/2/_examples/java/guides/operators/PaginationTest.java new file mode 100644 index 000000000..e032de2a6 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/_examples/java/guides/operators/PaginationTest.java @@ -0,0 +1,97 @@ +package guides.operators; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.helpers.test.AssertSubscriber; +import org.junit.jupiter.api.Test; + +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.assertj.core.api.Assertions.assertThat; + +public class PaginationTest { + + @SuppressWarnings("Convert2MethodRef") + @Test + public void test() { + // tag::code[] + PaginatedApi api = new PaginatedApi(); + + Multi stream = Multi.createBy().repeating() + .completionStage( + () -> new AtomicInteger(), + state -> api.getPage(state.getAndIncrement())) + .until(list -> list.isEmpty()) + .onItem().disjoint(); + // end::code[] + stream.subscribe().withSubscriber(AssertSubscriber.create(10)) + .assertCompleted() + .assertItems("a", "b", "c", "d", "e", "f", "g", "h"); + + } + + @SuppressWarnings("Convert2MethodRef") + @Test + public void test2() { + // tag::code2[] + PaginatedApi api = new PaginatedApi(); + + Multi stream = Multi.createBy().repeating() + .uni( + () -> new AtomicInteger(), + state -> api.retrieve(state.getAndIncrement())) + .whilst(page -> page.hasNext()); + // end::code2[] + AssertSubscriber subscriber = stream.subscribe() + .withSubscriber(AssertSubscriber.create(10)) + .assertCompleted(); + + assertThat(subscriber.getItems()).hasSize(3); + + } + + private static class Page { + final boolean hasNext; + + private Page(boolean hasNext) { + this.hasNext = hasNext; + } + + public boolean hasNext() { + return hasNext; + } + } + + + private class PaginatedApi { + + Map> pages = new LinkedHashMap<>(); + + public PaginatedApi() { + pages.put(0, Arrays.asList("a", "b", "c")); + pages.put(1, Arrays.asList("d", "e", "f")); + pages.put(2, Arrays.asList("g", "h")); + pages.put(3, Collections.emptyList()); + } + + CompletionStage> getPage(int page) { + List strings = pages.get(page); + if (strings == null) { + strings = Collections.emptyList(); + } + return CompletableFuture.completedFuture(strings); + } + + + Uni retrieve(int page) { + if (page == 2) { + return Uni.createFrom().item(new Page(false)); + } + return Uni.createFrom().item(new Page(true)); + } + } + +} diff --git a/content/docs/smallrye-mutiny/2/_examples/java/guides/operators/PollableSourceTest.java b/content/docs/smallrye-mutiny/2/_examples/java/guides/operators/PollableSourceTest.java new file mode 100644 index 000000000..d5b10e736 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/_examples/java/guides/operators/PollableSourceTest.java @@ -0,0 +1,84 @@ +package guides.operators; + +import guides.extension.SystemOut; +import guides.extension.SystemOutCaptureExtension; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.subscription.Cancellable; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.awaitility.Awaitility.await; + +@ExtendWith(SystemOutCaptureExtension.class) +public class PollableSourceTest { + + private final ExecutorService executor = Executors.newSingleThreadExecutor(); + + @Test + public void test(SystemOut out) { // NOSONAR + // tag::code[] + PollableDataSource source = new PollableDataSource(); + // First creates a uni that emit the polled item. + // Because `poll` blocks, let's use a specific executor + Uni pollItemFromSource = Uni.createFrom().item(source::poll) + .runSubscriptionOn(executor); + // To get the stream of items, just repeat the uni indefinitely + Multi stream = pollItemFromSource.repeat().indefinitely(); + + Cancellable cancellable = stream.subscribe().with(item -> System.out.println("Polled item: " + item)); + // end::code[] + await().until(() -> source.counter.get() >= 4); + // tag::code[] + // ... later .. + // when you don't want the items anymore, cancel the subscription and close the source if needed. + cancellable.cancel(); + source.close(); + // end::code[] + } + + @SuppressWarnings("Convert2MethodRef") + @Test + public void test2(SystemOut out) { // NOSONAR + // tag::code2[] + PollableDataSource source = new PollableDataSource(); + Multi stream = Multi.createBy().repeating() + .supplier(source::poll) + .until(s -> s == null) + .runSubscriptionOn(executor); + + stream.subscribe().with(item -> System.out.println("Polled item: " + item)); + // end::code2[] + await().until(() -> source.counter.get() >= 5); + } + + private static class PollableDataSource { + + private final AtomicInteger counter = new AtomicInteger(); + + String poll() { + block(); + if (counter.get() == 5) { + return null; + } + return Integer.toString(counter.getAndIncrement()); + } + + private void block() { + try { + Thread.sleep(100); // NOSONAR + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + public void close() { + // do nothing. + } + } + +} diff --git a/content/docs/smallrye-mutiny/2/_examples/java/guides/operators/RepetitionsTest.java b/content/docs/smallrye-mutiny/2/_examples/java/guides/operators/RepetitionsTest.java new file mode 100644 index 000000000..c32dcf760 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/_examples/java/guides/operators/RepetitionsTest.java @@ -0,0 +1,31 @@ +package guides.operators; + +import io.smallrye.mutiny.Multi; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class RepetitionsTest { + + @Test + public void distinct() { + Multi multi = Multi.createFrom().items(1, 1, 2, 3, 4, 5, 5, 6, 1, 4, 4); + // tag::distinct[] + List list = multi + .select().distinct() + .collect().asList() + .await().indefinitely(); + // end::distinct[] + + // tag::repetition[] + List list2 = multi + .skip().repetitions() + .collect().asList() + .await().indefinitely(); + // end::repetition[] + assertThat(list).containsExactly(1, 2, 3, 4, 5, 6); + assertThat(list2).containsExactly(1, 2, 3, 4, 5, 6, 1, 4); + } +} diff --git a/content/docs/smallrye-mutiny/2/_examples/java/guides/operators/ReplayTest.java b/content/docs/smallrye-mutiny/2/_examples/java/guides/operators/ReplayTest.java new file mode 100644 index 000000000..268421678 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/_examples/java/guides/operators/ReplayTest.java @@ -0,0 +1,82 @@ +package guides.operators; + +import io.smallrye.mutiny.Multi; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ReplayTest { + + @Test + public void replayAll() { + // tag::replay-all[] + Multi upstream = Multi.createFrom().range(0, 10); + + Multi replay = Multi.createBy().replaying().ofMulti(upstream); + + List items_1 = replay.collect().asList().await().indefinitely(); + List items_2 = replay.collect().asList().await().indefinitely(); + // end::replay-all[] + + assertThat(items_1) + .isEqualTo(items_2) + .hasSize(10); + System.out.println(items_1); + } + + @Test + public void replayLast() { + // tag::replay-last[] + Multi upstream = Multi.createFrom().range(0, 10); + + Multi replay = Multi.createBy().replaying().upTo(3).ofMulti(upstream); + + List items_1 = replay.collect().asList().await().indefinitely(); + List items_2 = replay.collect().asList().await().indefinitely(); + // end::replay-last[] + + assertThat(items_1) + .isEqualTo(items_2) + .hasSize(3); + System.out.println(items_1); + } + + @Test + public void replayWithSeed() { + // tag::replay-seed[] + Multi upstream = Multi.createFrom().range(0, 10); + Iterable seed = Arrays.asList(-10, -5, -1); + + Multi replay = Multi.createBy().replaying().ofSeedAndMulti(seed, upstream); + + List items_1 = replay.collect().asList().await().indefinitely(); + List items_2 = replay.collect().asList().await().indefinitely(); + // end::replay-seed[] + + assertThat(items_1) + .isEqualTo(items_2) + .hasSize(13); + System.out.println(items_1); + } + + @Test + public void errors() { + // tag::replay-errors[] + Multi upstream = Multi.createBy().concatenating().streams( + Multi.createFrom().range(0, 10), + Multi.createFrom().failure(() -> new IOException("boom")) + ); + + Multi replay = Multi.createBy().replaying().upTo(3).ofMulti(upstream); + + replay.subscribe().with( + n -> System.out.println(" -> " + n), + failure -> System.out.println("Failed: " + failure.getMessage()), + () -> System.out.println("Completed")); + // end::replay-errors[] + } +} diff --git a/content/docs/smallrye-mutiny/2/_examples/java/guides/operators/RunSubscriptionOnTest.java b/content/docs/smallrye-mutiny/2/_examples/java/guides/operators/RunSubscriptionOnTest.java new file mode 100644 index 000000000..fbe4d38bf --- /dev/null +++ b/content/docs/smallrye-mutiny/2/_examples/java/guides/operators/RunSubscriptionOnTest.java @@ -0,0 +1,67 @@ +package guides.operators; + +import guides.extension.SystemOut; +import guides.extension.SystemOutCaptureExtension; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.infrastructure.Infrastructure; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Stream; + +import static org.awaitility.Awaitility.await; +import static org.hamcrest.CoreMatchers.is; + +@SuppressWarnings("Convert2MethodRef") +@ExtendWith(SystemOutCaptureExtension.class) +public class RunSubscriptionOnTest { + + @Test + public void testRunSubscriptionOn(SystemOut out) { + Executor executor = Infrastructure.getDefaultExecutor(); + AtomicBoolean completed = new AtomicBoolean(); + //tag::runSubscriptionOn[] + Multi.createFrom().items(() -> { + // called on a thread from the executor + return retrieveItemsFromSource(); + }) + .onItem().transform(this::applySomeOperation) + .runSubscriptionOn(executor) + .subscribe().with( + item -> System.out.println("Item: " + item), + Throwable::printStackTrace, + () -> completed.set(true) + ); + //end::runSubscriptionOn[] + + await().untilAtomic(completed, is(true)); + } + + @Test + public void testEmitOn(SystemOut out) { + Executor executor = Infrastructure.getDefaultExecutor(); + AtomicBoolean completed = new AtomicBoolean(); + //tag::emitOn[] + Multi.createFrom().items(this::retrieveItemsFromSource) + .emitOn(executor) + .onItem().transform(this::applySomeOperation) + .subscribe().with( + item -> System.out.println("Item: " + item), + Throwable::printStackTrace, + () -> completed.set(true) + ); + //end::emitOn[] + + await().untilAtomic(completed, is(true)); + } + + public String applySomeOperation(String s) { + return s.toUpperCase(); + } + + public Stream retrieveItemsFromSource() { + return Stream.of("a", "b", "c"); + } +} diff --git a/content/docs/smallrye-mutiny/2/_examples/java/guides/operators/SelectAndSkipTest.java b/content/docs/smallrye-mutiny/2/_examples/java/guides/operators/SelectAndSkipTest.java new file mode 100644 index 000000000..a8ffa4508 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/_examples/java/guides/operators/SelectAndSkipTest.java @@ -0,0 +1,65 @@ +package guides.operators; + +import io.smallrye.mutiny.Multi; +import org.junit.jupiter.api.Test; + +import java.time.Duration; + +import static org.assertj.core.api.Assertions.assertThat; + +public class SelectAndSkipTest { + + @Test + public void testSelect() { + Multi multi = Multi.createFrom().items(1, 2, 3, 4, 5, 6, 7, 8, 9); + + // tag::take-first[] + Multi firstThreeItems = multi.select().first(3); + // end::take-first[] + + // tag::take-last[] + Multi lastThreeItems = multi.select().last(3); + // end::take-last[] + + // tag::take-while[] + Multi takeWhile = multi.select().first(i -> i < 4); + // end::take-while[] + + // tag::take-for[] + Multi takeForDuration = multi.select().first(Duration.ofSeconds(1)); + // end::take-for[] + + assertThat(firstThreeItems.collect().asList().await().indefinitely()).containsExactly(1, 2, 3); + assertThat(takeWhile.collect().asList().await().indefinitely()).containsExactly(1, 2, 3); + assertThat(lastThreeItems.collect().asList().await().indefinitely()).containsExactly(7, 8, 9); + assertThat(takeForDuration.collect().asList().await().indefinitely()).containsExactly(1, 2, 3, 4, 5, 6, 7, 8, 9); + + } + + @Test + public void testSkip() { + Multi multi = Multi.createFrom().items(1, 2, 3, 4, 5, 6, 7, 8, 9); + + // tag::skip-first[] + Multi skipThreeItems = multi.skip().first(3); + // end::skip-first[] + + // tag::skip-last[] + Multi skipLastThreeItems = multi.skip().last(3); + // end::skip-last[] + + // tag::skip-while[] + Multi skipWhile = multi.skip().first(i -> i < 4); + // end::skip-while[] + + // tag::skip-for[] + Multi skipForDuration = multi.skip().first(Duration.ofSeconds(1)); + // end::skip-for[] + + assertThat(skipThreeItems.collect().asList().await().indefinitely()).containsExactly(4, 5, 6, 7, 8, 9); + assertThat(skipLastThreeItems.collect().asList().await().indefinitely()).containsExactly(1, 2, 3, 4 ,5, 6); + assertThat(skipWhile.collect().asList().await().indefinitely()).containsExactly(4, 5, 6, 7, 8, 9); + assertThat(skipForDuration.collect().asList().await().indefinitely()).isEmpty(); + + } +} diff --git a/content/docs/smallrye-mutiny/2/_examples/java/guides/operators/SplitTest.java b/content/docs/smallrye-mutiny/2/_examples/java/guides/operators/SplitTest.java new file mode 100644 index 000000000..bf2855bb9 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/_examples/java/guides/operators/SplitTest.java @@ -0,0 +1,45 @@ +package guides.operators; + +import io.smallrye.mutiny.Multi; +import org.junit.jupiter.api.Test; + +public class SplitTest { + + // tag::enum[] + enum Signals { + INPUT, + OUTPUT, + OTHER + } + // end::enum[] + + @Test + public void splitDemo() { + // tag::splits[] + Multi multi = Multi.createFrom().items( + "!a", "?b", "!c", "!d", "123", "?e" + ); + + var splitter = multi.split(Signals.class, s -> { + if (s.startsWith("?")) { + return Signals.INPUT; + } else if (s.startsWith("!")) { + return Signals.OUTPUT; + } else { + return Signals.OTHER; + } + }); + + splitter.get(Signals.INPUT) + .onItem().transform(s -> s.substring(1)) + .subscribe().with(signal -> System.out.println("input - " + signal)); + + splitter.get(Signals.OUTPUT) + .onItem().transform(s -> s.substring(1)) + .subscribe().with(signal -> System.out.println("output - " + signal)); + + splitter.get(Signals.OTHER) + .subscribe().with(signal -> System.out.println("other - " + signal)); + // end::splits[] + } +} diff --git a/content/docs/smallrye-mutiny/2/_examples/java/guides/operators/UniJoinTest.java b/content/docs/smallrye-mutiny/2/_examples/java/guides/operators/UniJoinTest.java new file mode 100644 index 000000000..a02897084 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/_examples/java/guides/operators/UniJoinTest.java @@ -0,0 +1,54 @@ +package guides.operators; + +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.groups.UniJoin; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.List; +import java.util.function.Supplier; + +class UniJoinTest { + + @Test + void joinAll() { + // tag::join-all[] + Uni a = Uni.createFrom().item(1); + Uni b = Uni.createFrom().item(2); + Uni c = Uni.createFrom().item(3); + + Uni> res = Uni.join().all(a, b, c).andCollectFailures(); + // end::join-all[] + + // tag::join-all-ff[] + res = Uni.join().all(a, b, c).andFailFast(); + // end::join-all-ff[] + } + + void joinFirst(Uni a, Uni b, Uni c) { + + // tag::join-first[] + Uni res = Uni.join().first(a, b, c).toTerminate(); + // end::join-first[] + + // tag::join-first-withitem[] + res = Uni.join().first(a, b, c).withItem(); + // end::join-first-withitem[] + + Supplier> supplier = () -> Uni.createFrom().item(63); + boolean someCondition = false; + + // tag::builder[] + UniJoin.Builder builder = Uni.join().builder(); + + while (someCondition) { + Uni uni = supplier.get(); + builder.add(uni); + } + + Uni> all = builder.joinAll().andFailFast(); + + Uni first = builder.joinFirst().withItem(); + // end::builder[] + } +} diff --git a/content/docs/smallrye-mutiny/2/_examples/java/guides/operators/UnicastProcessorTest.java b/content/docs/smallrye-mutiny/2/_examples/java/guides/operators/UnicastProcessorTest.java new file mode 100644 index 000000000..237b59345 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/_examples/java/guides/operators/UnicastProcessorTest.java @@ -0,0 +1,34 @@ +package guides.operators; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.helpers.test.AssertSubscriber; +import io.smallrye.mutiny.operators.multi.processors.UnicastProcessor; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class UnicastProcessorTest { + + @Test + public void test() { + // tag::code[] + UnicastProcessor processor = UnicastProcessor.create(); + Multi multi = processor + .onItem().transform(String::toUpperCase) + .onFailure().recoverWithItem("d'oh"); + + // Create a source of items that does not follow the request protocol + new Thread(() -> { + for (int i = 0; i < 1000; i++) { + processor.onNext(Integer.toString(i)); + } + processor.onComplete(); + }).start(); + + // end::code[] + AssertSubscriber subscriber = AssertSubscriber.create(Long.MAX_VALUE); + multi.subscribe().withSubscriber(subscriber) + .awaitCompletion() + .run(() -> assertThat(subscriber.getItems()).hasSize(1000)); + } +} diff --git a/content/docs/smallrye-mutiny/2/_examples/java/guides/reference/PitfallsTest.java b/content/docs/smallrye-mutiny/2/_examples/java/guides/reference/PitfallsTest.java new file mode 100644 index 000000000..1db116a84 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/_examples/java/guides/reference/PitfallsTest.java @@ -0,0 +1,64 @@ +package guides.reference; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import org.junit.jupiter.api.Test; + +import java.util.List; + +public class PitfallsTest { + + private Uni fetch(String id) { + return Uni.createFrom().item("Yolo"); + } + + @Test + public void noMagicScheduling() { + // tag::noMagicJoin[] + Uni> data = Uni.join().all( + fetch("abc"), + fetch("def"), + fetch("123")).andFailFast(); + // end::noMagicJoin[] + } + + @Test + public void inMemoryData() { + // tag::inMemoryData[] + Uni uni = Uni.createFrom().item(123); + Multi multi = Multi.createFrom().items(1, 2, 3, 4, 5); + // end::inMemoryData[] + } + + private List obtainValues(String key) { + return List.of(); + } + + // tag::suspiciousPublisher[] + public Multi fetchData(String key) { + List strings = obtainValues(key); + return Multi.createFrom().iterable(strings); + } + // end::suspiciousPublisher[] + + private Multi streamData(String key) { + return Multi.createFrom().items(""); + } + + private Multi extractData(String line) { + return Multi.createFrom().items(""); + } + + private Multi flatmap() { + // tag::flatmap-ism[] + Multi stream = streamData("abc"); + return stream.onItem().transformToMultiAndMerge(line -> { + if (line.trim().length() > 10) { + return extractData(line); + } else { + return Multi.createFrom().item("[N/A]"); + } + }); + // end::flatmap-ism[] + } +} diff --git a/content/docs/smallrye-mutiny/2/_examples/java/tutorials/CreatingMultiTest.java b/content/docs/smallrye-mutiny/2/_examples/java/tutorials/CreatingMultiTest.java new file mode 100644 index 000000000..b2c5e621a --- /dev/null +++ b/content/docs/smallrye-mutiny/2/_examples/java/tutorials/CreatingMultiTest.java @@ -0,0 +1,134 @@ +package tutorials; + +import guides.extension.SystemOut; +import guides.extension.SystemOutCaptureExtension; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.helpers.BlockingIterable; +import io.smallrye.mutiny.subscription.Cancellable; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.time.Duration; +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.IntStream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.awaitility.Awaitility.await; + +@SuppressWarnings({"Convert2MethodRef"}) +@ExtendWith(SystemOutCaptureExtension.class) +public class CreatingMultiTest { + + @Test + void pipeline(SystemOut out) { + // tag::pipeline[] + Multi.createFrom().items(1, 2, 3, 4, 5) + .onItem().transform(i -> i * 2) + .select().first(3) + .onFailure().recoverWithItem(0) + .subscribe().with(System.out::println); + // end::pipeline[] + assertThat(out.get()).contains("2", "4", "6"); + } + + @Test + void subscription(SystemOut out) { + Multi multi = Multi.createFrom().item(1); + // tag::subscription[] + Cancellable cancellable = multi + .subscribe().with( + item -> System.out.println(item), + failure -> System.out.println("Failed with " + failure), + () -> System.out.println("Completed")); + // end::subscription[] + assertThat(cancellable).isNotNull(); + assertThat(out.get()).contains("1", "Completed").doesNotContain("Failed"); + } + + @Test + public void creation() { + { + // tag::simple[] + Multi multiFromItems = Multi.createFrom().items(1, 2, 3, 4); + Multi multiFromIterable = Multi.createFrom().iterable(Arrays.asList(1, 2, 3, 4, 5)); + // end::simple[] + assertThat(multiFromItems.collect().asList().await().indefinitely()).containsExactly(1, 2, 3, 4); + assertThat(multiFromIterable.collect().asList().await().indefinitely()).containsExactly(1, 2, 3, 4, 5); + } + + { + // tag::supplier[] + AtomicInteger counter = new AtomicInteger(); + Multi multi = Multi.createFrom().items(() -> + IntStream.range(counter.getAndIncrement(), counter.get() * 2).boxed()); + // end::supplier[] + assertThat(multi.collect().asList().await().indefinitely()).containsExactly(0, 1); + assertThat(multi.collect().asList().await().indefinitely()).containsExactly(1, 2, 3); + assertThat(multi.collect().asList().await().indefinitely()).containsExactly(2, 3, 4, 5); + } + + { + // tag::failed[] + // Pass an exception directly: + Multi failed1 = Multi.createFrom().failure(new Exception("boom")); + + // Pass a supplier called for every subscriber: + Multi failed2 = Multi.createFrom().failure(() -> new Exception("boom")); + // end::failed[] + + assertThatThrownBy(() -> failed1.toUni().await().indefinitely()) + .hasMessageContaining("boom"); + assertThatThrownBy(() -> failed2.toUni().await().indefinitely()) + .hasMessageContaining("boom"); + + } + + { + // tag::empty[] + Multi multi = Multi.createFrom().empty(); + // end::empty[] + assertThat(multi.toUni().await().indefinitely()).isNull(); + } + + { + // tag::emitter[] + Multi multi = Multi.createFrom().emitter(em -> { + em.emit(1); + em.emit(2); + em.emit(3); + em.complete(); + }); + // end::emitter[] + assertThat(multi.collect().asList().await().indefinitely()).containsExactly(1, 2, 3); + } + + { + // tag::ticks[] + Multi ticks = Multi.createFrom().ticks().every(Duration.ofMillis(100)); + // end::ticks[] + BlockingIterable longs = ticks + .select().first(3) + .subscribe().asIterable(); + await().until(() -> longs.stream().count() == 3); + } + + { + // tag::generator[] + Multi sequence = Multi.createFrom().generator(() -> 1, (n, emitter) -> { + int next = n + (n / 2) + 1; + if (n < 50) { + emitter.emit(next); + + } else { + emitter.complete(); + } + return next; + }); + // end::generator[] + assertThat(sequence.collect().asList().await().indefinitely()).containsExactly(2, 4, 7, 11, 17, 26, 40, 61); + } + } + +} diff --git a/content/docs/smallrye-mutiny/2/_examples/java/tutorials/CreatingUniTest.java b/content/docs/smallrye-mutiny/2/_examples/java/tutorials/CreatingUniTest.java new file mode 100644 index 000000000..157e621ee --- /dev/null +++ b/content/docs/smallrye-mutiny/2/_examples/java/tutorials/CreatingUniTest.java @@ -0,0 +1,111 @@ +package tutorials; + +import guides.extension.SystemOut; +import guides.extension.SystemOutCaptureExtension; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.subscription.Cancellable; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.awaitility.Awaitility.await; + +@SuppressWarnings({ "ConstantConditions", "Convert2MethodRef" }) +@ExtendWith(SystemOutCaptureExtension.class) +public class CreatingUniTest { + + @Test + void pipeline(SystemOut out) { + // tag::pipeline[] + Uni.createFrom().item(1) + .onItem().transform(i -> "hello-" + i) + .onItem().delayIt().by(Duration.ofMillis(100)) + .subscribe().with(System.out::println); + // end::pipeline[] + await().untilAsserted(() -> + assertThat(out.get()).contains("hello-1") + ); + } + + @Test + void subscription(SystemOut out) { + Uni uni = Uni.createFrom().item(1); + // tag::subscription[] + Cancellable cancellable = uni + .subscribe().with( + item -> System.out.println(item), + failure -> System.out.println("Failed with " + failure)); + // end::subscription[] + assertThat(cancellable).isNotNull(); + assertThat(out.get()).contains("1").doesNotContain("Failed"); + } + + @Test + public void creation() { + { + // tag::simple[] + Uni uni = Uni.createFrom().item(1); + // end::simple[] + assertThat(uni.await().indefinitely()).isEqualTo(1); + } + + { + // tag::supplier[] + AtomicInteger counter = new AtomicInteger(); + Uni uni = Uni.createFrom().item(() -> counter.getAndIncrement()); + // end::supplier[] + assertThat(uni.await().indefinitely()).isEqualTo(0); + assertThat(uni.await().indefinitely()).isEqualTo(1); + assertThat(uni.await().indefinitely()).isEqualTo(2); + } + + { + // tag::failed[] + // Pass an exception directly: + Uni failed1 = Uni.createFrom().failure(new Exception("boom")); + + // Pass a supplier called for every subscriber: + Uni failed2 = Uni.createFrom().failure(() -> new Exception("boom")); + // end::failed[] + + assertThatThrownBy(() -> failed1.await().indefinitely()) + .hasMessageContaining("boom"); + assertThatThrownBy(() -> failed2.await().indefinitely()) + .hasMessageContaining("boom"); + + } + + { + // tag::null[] + Uni uni = Uni.createFrom().nullItem(); + // end::null[] + assertThat(uni.await().indefinitely()).isNull(); + } + + { + String result = "hello"; + // tag::emitter[] + Uni uni = Uni.createFrom().emitter(em -> { + // When the result is available, emit it + em.complete(result); + }); + // end::emitter[] + assertThat(uni.await().indefinitely()).isEqualTo("hello"); + } + + { + CompletionStage stage = CompletableFuture.completedFuture("hello"); + // tag::cs[] + Uni uni = Uni.createFrom().completionStage(stage); + // end::cs[] + assertThat(uni.await().indefinitely()).isEqualTo("hello"); + } + } + +} diff --git a/content/docs/smallrye-mutiny/2/_examples/java/tutorials/HandlingFailuresTest.java b/content/docs/smallrye-mutiny/2/_examples/java/tutorials/HandlingFailuresTest.java new file mode 100644 index 000000000..15d8e3f1d --- /dev/null +++ b/content/docs/smallrye-mutiny/2/_examples/java/tutorials/HandlingFailuresTest.java @@ -0,0 +1,105 @@ +package tutorials; + +import guides.extension.SystemOut; +import guides.extension.SystemOutCaptureExtension; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@SuppressWarnings("Convert2MethodRef") +@ExtendWith(SystemOutCaptureExtension.class) +public class HandlingFailuresTest { + + @Test + public void testInvoke(SystemOut out) { + Uni uni = Uni.createFrom().failure(new Exception("boom")); + Multi multi = Multi.createFrom().failure(new Exception("boom")); + // tag::invoke[] + Uni u = uni + .onFailure().invoke(failure -> log(failure)); + + Multi m = multi + .onFailure().invoke(failure -> log(failure)); + // end::invoke[] + assertThatThrownBy(() -> u.await().indefinitely()).hasMessageContaining("boom"); + assertThatThrownBy(() -> m.collect().asList() + .await().indefinitely()).hasMessageContaining("boom"); + assertThat(out.get()).contains("boom"); + } + + @Test + public void testTransform(SystemOut out) { + Uni uni = Uni.createFrom().failure(new Exception("boom")); + // tag::transform[] + Uni u = uni + .onFailure().transform(failure -> + new ServiceUnavailableException(failure)); + // end::transform[] + assertThatThrownBy(() -> u.await().indefinitely()) + .hasCauseExactlyInstanceOf(ServiceUnavailableException.class); + } + + @Test + public void testRecoverWithItem() { + Uni uni = Uni.createFrom().failure(new Exception("boom")); + // tag::recover-item[] + Uni u1 = uni + .onFailure().recoverWithItem("hello"); + + Uni u2 = uni + .onFailure().recoverWithItem(f -> getFallback(f)); + // end::recover-item[] + assertThat(u1.await().indefinitely()).isEqualTo("hello"); + assertThat(u2.await().indefinitely()).isEqualTo("hey"); + } + + @Test + public void testCompletionOnFailure() { + Multi multi = Multi.createFrom().failure(new Exception("boom")); + // tag::recover-completion[] + Multi m = multi + .onFailure().recoverWithCompletion(); + // end::recover-completion[] + assertThat(m.collect().asList().await().indefinitely()).isEmpty(); + } + + @Test + public void testSwitch() { + Uni uni = Uni.createFrom().failure(new Exception("boom")); + Multi multi = Multi.createFrom().failure(new Exception("boom")); + // tag::recover-switch[] + Uni u = uni + .onFailure().recoverWithUni(f -> getFallbackUni(f)); + + Multi m = multi + .onFailure().recoverWithMulti(f -> getFallbackMulti(f)); + // end::recover-switch[] + assertThat(u.await().indefinitely()).isEqualTo("hello"); + assertThat(m.collect().asList().await().indefinitely()).containsExactly("hey"); + } + + private Multi getFallbackMulti(Throwable f) { + return Multi.createFrom().item("hey"); + } + + private Uni getFallbackUni(Throwable f) { + return Uni.createFrom().item("hello"); + } + + private String getFallback(Throwable f) { + return "hey"; + } + + private void log(Throwable failure) { + System.out.println(failure.getMessage()); + } + + private static class ServiceUnavailableException extends Throwable { + public ServiceUnavailableException(Throwable ignored) { + } + } +} diff --git a/content/docs/smallrye-mutiny/2/_examples/java/tutorials/ObserveTest.java b/content/docs/smallrye-mutiny/2/_examples/java/tutorials/ObserveTest.java new file mode 100644 index 000000000..c32774f81 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/_examples/java/tutorials/ObserveTest.java @@ -0,0 +1,65 @@ +package tutorials; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import org.junit.jupiter.api.Test; + +import java.time.Duration; + +public class ObserveTest { + + @Test + public void test() { + Uni uni = Uni.createFrom().item("hello"); + Multi multi = Multi.createFrom().items("a", "b", "c"); + // tag::invoke[] + Uni u = uni.onItem() + .invoke(i -> System.out.println("Received item: " + i)); + + Multi m = multi.onItem() + .invoke(i -> System.out.println("Received item: " + i)); + // end::invoke[] + + // tag::call[] + multi + .onItem().call(i -> + Uni.createFrom().voidItem() + .onItem().delayIt().by(Duration.ofSeconds(1) + ) + ); + // end::call[] + + MyResource resource = new MyResource(); + // tag::close[] + multi + .onCompletion().call(() -> resource.close()); + // end::close[] + } + + @Test + public void all() { + Multi multi = Multi.createFrom().items("a", "b", "c"); + // tag::invoke-all[] + multi + .onSubscription() + .invoke(() -> System.out.println("⬇️ Subscribed")) + .onItem() + .invoke(i -> System.out.println("⬇️ Received item: " + i)) + .onFailure() + .invoke(f -> System.out.println("⬇️ Failed with " + f)) + .onCompletion() + .invoke(() -> System.out.println("⬇️ Completed")) + .onCancellation() + .invoke(() -> System.out.println("⬆️ Cancelled")) + .onRequest() + .invoke(l -> System.out.println("⬆️ Requested: " + l)); + // end::invoke-all[] + + } + + static class MyResource { + Uni close() { + return Uni.createFrom().voidItem(); + } + } +} diff --git a/content/docs/smallrye-mutiny/2/_examples/java/tutorials/RetryTest.java b/content/docs/smallrye-mutiny/2/_examples/java/tutorials/RetryTest.java new file mode 100644 index 000000000..be31f63cc --- /dev/null +++ b/content/docs/smallrye-mutiny/2/_examples/java/tutorials/RetryTest.java @@ -0,0 +1,61 @@ +package tutorials; + +import guides.extension.SystemOutCaptureExtension; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.time.Duration; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@SuppressWarnings("Convert2MethodRef") +@ExtendWith(SystemOutCaptureExtension.class) +public class RetryTest { + + @Test + public void testRetryAtMost() { + Uni uni = Uni.createFrom().failure(new Exception("boom")); + Multi multi = Multi.createFrom().failure(new Exception("boom")); + // tag::retry-at-most[] + Uni u = uni + .onFailure().retry().atMost(3); + Multi m = multi + .onFailure().retry().atMost(3); + // end::retry-at-most[] + assertThatThrownBy(() -> u.await().indefinitely()).hasMessageContaining("boom"); + assertThatThrownBy(() -> m.collect().asList() + .await().indefinitely()).hasMessageContaining("boom"); + } + + @Test + public void testRetryWithBackoff() { + Uni uni = Uni.createFrom().failure(new Exception("boom")); + // tag::retry-backoff[] + Uni u = uni + .onFailure().retry() + .withBackOff(Duration.ofMillis(100), Duration.ofSeconds(1)) + .atMost(3); + // end::retry-backoff[] + assertThatThrownBy(() -> u.await().indefinitely()) + .getCause() // Expected exception is wrapped in a java.util.concurrent.CompletionException + .hasMessageContaining("boom") + .hasSuppressedException(new IllegalStateException("Retries exhausted: 3/3")); + } + + @Test + public void testRetryUntil() { + Uni uni = Uni.createFrom().failure(new Exception("boom")); + // tag::retry-until[] + Uni u = uni + .onFailure().retry() + .until(f -> shouldWeRetry(f)); + // end::retry-until[] + assertThatThrownBy(() -> u.await().indefinitely()).hasMessageContaining("boom"); + } + + private boolean shouldWeRetry(Throwable ignored) { + return false; + } +} diff --git a/content/docs/smallrye-mutiny/2/_examples/java/tutorials/TransformItemsAsyncTest.java b/content/docs/smallrye-mutiny/2/_examples/java/tutorials/TransformItemsAsyncTest.java new file mode 100644 index 000000000..d94777db9 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/_examples/java/tutorials/TransformItemsAsyncTest.java @@ -0,0 +1,116 @@ +package tutorials; + +import guides.extension.SystemOut; +import guides.extension.SystemOutCaptureExtension; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import static org.assertj.core.api.Assertions.assertThat; + +@SuppressWarnings("Convert2MethodRef") +@ExtendWith(SystemOutCaptureExtension.class) +public class TransformItemsAsyncTest { + + interface MyAsyncService { + // tag::service[] + Uni invokeRemoteGreetingService(String name); + // end::service[] + } + + static class MyAsyncServiceImpl implements MyAsyncService { + + @Override + public Uni invokeRemoteGreetingService(String name) { + return Uni.createFrom().item(name); + } + } + + MyAsyncServiceImpl service = new MyAsyncServiceImpl(); + + + private Uni invokeRemoteGreetingService(String name) { + return service.invokeRemoteGreetingService(name); + } + + @Test + public void testCall() { + + Uni uni = Uni.createFrom().item("mutiny"); + // tag::call[] + Uni result = uni + .onItem().transformToUni(name -> invokeRemoteGreetingService(name)); + // end::call[] + + assertThat(result.await().indefinitely()).isEqualTo("mutiny"); + } + + @Test + public void testChain(SystemOut out) { + // tag::chain[] + Uni uni = Uni.createFrom().item("Cameron"); + uni + .onItem().transformToUni(name -> invokeRemoteGreetingService(name)) + .subscribe().with( + item -> System.out.println(item), // Print "Hello Cameron", + fail -> fail.printStackTrace()); // Print the failure stack trace + // end::chain[] + assertThat(out.get()).contains("Cameron"); + } + + + @Test + public void testUniToMulti(SystemOut out) { + Uni uni = Uni.createFrom().item("Cameron"); + // tag::uni-to-multi[] + Multi result = uni + .onItem().transformToMulti(item -> Multi.createFrom().items(item, item)); + // end::uni-to-multi[] + assertThat(result.collect().asList().await().indefinitely()).containsExactly("Cameron", "Cameron"); + + // tag::uni-to-multi-2[] + uni + .onItem().transformToMulti(item -> Multi.createFrom().items(item, item)) + .subscribe().with( + item -> System.out.println(item)); // Called twice + // end::uni-to-multi-2[] + assertThat(out.get()).contains("Cameron"); + } + + @Test + public void testMergeAndConcatUni() { + Multi multi = Multi.createFrom().items("a", "b"); + + // tag::merge-concat[] + Multi merged = multi + .onItem().transformToUniAndMerge(name -> invokeRemoteGreetingService(name)); + + Multi concat = multi + .onItem().transformToUniAndConcatenate(name -> invokeRemoteGreetingService(name)); + // end::merge-concat[] + + assertThat(merged.collect().asList().await().indefinitely()).containsExactly("a", "b"); + assertThat(concat.collect().asList().await().indefinitely()).containsExactly("a", "b"); + } + + @Test + public void testMergeAndConcatMulti() { + Multi multi = Multi.createFrom().items("a", "b"); + + // tag::merge-concat-multi[] + Multi merged = multi + .onItem().transformToMultiAndMerge(item -> someMulti(item)); + + Multi concat = multi + .onItem().transformToMultiAndConcatenate(item -> someMulti(item)); + // end::merge-concat-multi[] + + assertThat(merged.collect().asList().await().indefinitely()).containsExactly("a", "b"); + assertThat(concat.collect().asList().await().indefinitely()).containsExactly("a", "b"); + } + + private Multi someMulti(String item) { + return Multi.createFrom().item(item); + } +} diff --git a/content/docs/smallrye-mutiny/2/_examples/java/tutorials/TransformItemsTest.java b/content/docs/smallrye-mutiny/2/_examples/java/tutorials/TransformItemsTest.java new file mode 100644 index 000000000..e975e56a8 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/_examples/java/tutorials/TransformItemsTest.java @@ -0,0 +1,61 @@ +package tutorials; + +import guides.extension.SystemOut; +import guides.extension.SystemOutCaptureExtension; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import static org.assertj.core.api.Assertions.assertThat; + +@SuppressWarnings("Convert2MethodRef") +@ExtendWith(SystemOutCaptureExtension.class) +public class TransformItemsTest { + + @Test + public void testUniTransform(SystemOut out) { + // tag::uni-transform[] + Uni someUni = Uni.createFrom().item("hello"); + someUni + .onItem().transform(i -> i.toUpperCase()) + .subscribe().with( + item -> System.out.println(item)); // Print HELLO + // end::uni-transform[] + assertThat(out.get()).contains("HELLO"); + } + + @Test + public void testMultiTransform() { + Multi multi = Multi.createFrom().items("a", "b", "c"); + // tag::multi-transform[] + Multi m = multi.onItem().transform(i -> i.toUpperCase()); + // end::multi-transform[] + assertThat(m.collect().asList().await().indefinitely()).containsExactly("A", "B", "C"); + } + + @Test + public void testMultiTransformWithSubscription(SystemOut out) { + // tag::multi-transform-2[] + Multi someMulti = Multi.createFrom().items("a", "b", "c"); + someMulti + .onItem().transform(i -> i.toUpperCase()) + .subscribe().with( + item -> System.out.println(item)); // Print A B C + // end::multi-transform-2[] + assertThat(out.get()).contains("A", "B", "C"); + } + + @Test + public void testChain() { + Uni uni = Uni.createFrom().item("hello"); + // tag::chain[] + Uni u = uni + .onItem().transform(i -> i.toUpperCase()) + .onItem().transform(i -> i + "!"); + // end::chain[] + + assertThat(u.await().indefinitely()).isEqualTo("HELLO!"); + + } +} diff --git a/content/docs/smallrye-mutiny/2/_examples/kotlin/guides/Coroutines.kt b/content/docs/smallrye-mutiny/2/_examples/kotlin/guides/Coroutines.kt new file mode 100644 index 000000000..db0d3b64f --- /dev/null +++ b/content/docs/smallrye-mutiny/2/_examples/kotlin/guides/Coroutines.kt @@ -0,0 +1,60 @@ +import io.smallrye.mutiny.Multi +import io.smallrye.mutiny.Uni +// tag::importStatements[] +import io.smallrye.mutiny.coroutines.asFlow +import io.smallrye.mutiny.coroutines.asMulti +import io.smallrye.mutiny.coroutines.asUni +import io.smallrye.mutiny.coroutines.awaitSuspending +// end::importStatements[] +import io.smallrye.mutiny.coroutines.uni +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf + +suspend fun uniAwaitSuspending() { + // tag::uniAwaitSuspending[] + val uni: Uni = Uni.createFrom().item("Mutiny ❤ Kotlin") + try { + // Available within suspend function and CoroutineScope + val item: String = uni.awaitSuspending() + } catch (failure: Throwable) { + // onFailure event happened + } + // end::uniAwaitSuspending[] +} + +@ExperimentalCoroutinesApi +suspend fun deferredAsUni() { + // tag::deferredAsUni[] + val deferred: Deferred = GlobalScope.async { "Kotlin ❤ Mutiny" } + val uni: Uni = deferred.asUni() + // end::deferredAsUni[] +} + +@ExperimentalCoroutinesApi +suspend fun multiAsFlow() { + // tag::multiAsFlow[] + val multi: Multi = Multi.createFrom().items("Mutiny", "❤", "Kotlin") + val flow: Flow = multi.asFlow() + // end::multiAsFlow[] +} + +suspend fun flowAsMulti() { + // tag::flowAsMulti[] + val flow: Flow = flowOf("Kotlin", "❤", "Mutiny") + val multi: Multi = flow.asMulti() + // end::flowAsMulti[] +} + +suspend fun uniBuilder() { + // tag::uniBuilder[] + // import io.smallrye.mutiny.coroutines.uni + coroutineScope { + val uni: Uni = uni { "λ 🚧" } + } + // end::uniBuilder[] +} diff --git a/content/docs/smallrye-mutiny/2/_examples/kotlin/guides/UniExt.kt b/content/docs/smallrye-mutiny/2/_examples/kotlin/guides/UniExt.kt new file mode 100644 index 000000000..a8f96faa5 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/_examples/kotlin/guides/UniExt.kt @@ -0,0 +1,19 @@ +import io.smallrye.mutiny.Uni +import io.smallrye.mutiny.replaceWithUnit +import io.smallrye.mutiny.uni + +fun uniReplaceWithUnit() { + val uni: Uni = Uni.createFrom().item("") + // tag::uniReplaceWithUnit[] + // import io.smallrye.mutiny.replaceWithUnit + val unitUni : Uni = uni.replaceWithUnit() + assert(unitUni.await().indefinitely() === Unit) + // end::uniReplaceWithUnit[] +} + +fun regularUniBuilder() { + // tag::uniBuilder[] + // import io.smallrye.mutiny.uni + val uni: Uni = uni { "λ 🚧" } + // end::uniBuilder[] +} diff --git a/content/docs/smallrye-mutiny/2/_nav.yml b/content/docs/smallrye-mutiny/2/_nav.yml new file mode 100644 index 000000000..6fbb09d73 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/_nav.yml @@ -0,0 +1,68 @@ +title: SmallRye Mutiny +nav: +- page: index +- title: Tutorials + items: + - page: tutorials/getting-mutiny + - page: tutorials/hello-mutiny + - page: tutorials/creating-uni-pipelines + - page: tutorials/creating-multi-pipelines + - page: tutorials/observing-events + - page: tutorials/transforming-items + - page: tutorials/transforming-items-asynchronously + - page: tutorials/handling-failures + - page: tutorials/retrying + - page: tutorials/mutiny-workshop +- title: Guides + items: + - page: guides/imperative-to-reactive + - page: guides/reactive-to-imperative + - page: guides/unchecked-exceptions + - page: guides/filtering-items + - page: guides/collecting-items + - page: guides/take-skip-items + - page: guides/eliminate-duplicates-and-repetitions + - page: guides/handling-null + - page: guides/handling-timeouts + - page: guides/delaying-events + - page: guides/pagination + - page: guides/polling + - page: guides/emission-threads + - page: guides/emit-on-vs-run-subscription-on + - page: guides/completion-stage + - page: guides/rx + - page: guides/shortcut-methods + - page: guides/merging-and-concatenating-streams + - page: guides/combining-items + - page: guides/joining-unis + - page: guides/converters + - page: guides/testing + - page: guides/spies + - page: guides/custom-operators + - page: guides/dropped-exceptions + - page: guides/framework-integration + - page: guides/hot-streams + - page: guides/integrate-a-non-reactive-source + - page: guides/kotlin + - page: guides/logging + - page: guides/context-passing + - page: guides/replaying-multis + - page: guides/controlling-demand + - page: guides/multi-split + - page: guides/branching + - page: guides/broadcasting-to-multiple-subscribers +- title: Reference + items: + - page: reference/migrating-to-mutiny-2 + - page: reference/why-is-asynchronous-important + - page: reference/what-is-reactive-programming + - page: reference/what-makes-mutiny-different + - page: reference/uni-and-multi + - page: reference/going-reactive-a-few-pitfalls + - page: reference/publications + - title: Tags index + page: tags-index +- title: API + url: https://javadoc.io/doc/io.smallrye.reactive/mutiny/latest/index.html +- title: Discussions/Questions + url: https://github.com/smallrye/smallrye-mutiny/discussions diff --git a/content/docs/smallrye-mutiny/2/assets/CF_logo_horizontal_single_reverse.svg b/content/docs/smallrye-mutiny/2/assets/CF_logo_horizontal_single_reverse.svg new file mode 100644 index 000000000..2b8907471 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/assets/CF_logo_horizontal_single_reverse.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/2/assets/logo-white.png b/content/docs/smallrye-mutiny/2/assets/logo-white.png new file mode 100644 index 000000000..4472249bb Binary files /dev/null and b/content/docs/smallrye-mutiny/2/assets/logo-white.png differ diff --git a/content/docs/smallrye-mutiny/2/assets/logo-white.svg b/content/docs/smallrye-mutiny/2/assets/logo-white.svg new file mode 100644 index 000000000..fa27cb24f --- /dev/null +++ b/content/docs/smallrye-mutiny/2/assets/logo-white.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/2/assets/logo.png b/content/docs/smallrye-mutiny/2/assets/logo.png new file mode 100644 index 000000000..f8e4ca32a Binary files /dev/null and b/content/docs/smallrye-mutiny/2/assets/logo.png differ diff --git a/content/docs/smallrye-mutiny/2/guides/branching.adoc b/content/docs/smallrye-mutiny/2/guides/branching.adoc new file mode 100644 index 000000000..9391061fb --- /dev/null +++ b/content/docs/smallrye-mutiny/2/guides/branching.adoc @@ -0,0 +1,57 @@ += How to do branching in a reactive pipeline? +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,intermediate + +Mutiny and similar reactive programming libraries do not have _branching_ operators similar to `if / else` and `switch/case` statements in Java. + +This does not mean that we can't express _branching_ in a reactive pipeline, and the most classic way is to use a transformation to a `Uni` (also called `flatMap` in functional programming). + +== Expressing branches as Uni operations + +Suppose that we have a pipeline where a `Uni` is created from a random value, and suppose that we want to have a different processing pipeline depending on whether the value is odd or even. +Let's have these 2 `Uni`-returning methods to model different behaviors: + +[source,java] +---- +include::{examples-dir}/java/guides/BranchingTest.java[tags=branches,indent=0] +---- + +We can use the `transformToUni` operator to plug either method depending on the random number: + +[source,java] +---- +include::{examples-dir}/java/guides/BranchingTest.java[tags=pipeline,indent=0] +---- + +Having such a mapping function is a common pattern: it has conditional logic and each branch returns a `Uni` that represents the "sub-pipeline" of what each branch shall do. + +Note that such constructs are primarily relevant when asynchronous I/O are involved and that such asynchronous I/O operations are typically `Uni`-returning methods such as those found in the https://smallrye.io/smallrye-mutiny-vertx-bindings/[Mutiny Vert.x bindings]. + +[TIP] +==== + +There are other ways to express the "result" of a branch. +You could wrap results in a custom type or a container like `java.util.Optional`. + +You could also return a failed `Uni`, and later react by continuing with another `Uni`, another value, or retrying (which would model a loop!). + +==== + +== Branching in a Multi + +The case of `Multi` is even more interesting because a `null`-completed `Uni` is discarded from the stream by any of the `transformToUni++{++...}` methods: + +[source,java] +---- +include::{examples-dir}/java/guides/BranchingTest.java[tags=multi-pipeline,indent=0] +---- + +where `drop()` is as follows: + +[source,java] +---- +include::{examples-dir}/java/guides/BranchingTest.java[tags=drop,indent=0] +---- + +Any negative value is discarded in this `Multi` pipeline, while the positive even and odd numbers get forwarded to the subscriber. diff --git a/content/docs/smallrye-mutiny/2/guides/broadcasting-to-multiple-subscribers.adoc b/content/docs/smallrye-mutiny/2/guides/broadcasting-to-multiple-subscribers.adoc new file mode 100644 index 000000000..c0b359329 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/guides/broadcasting-to-multiple-subscribers.adoc @@ -0,0 +1,185 @@ += Broadcasting to multiple subscribers (like server-sent events, websockets, etc) +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,beginner + +It is a common requirement in event-driven applications to have multiple subscribers receiving events from a single events source: + +image::images/diagrams/mermaid-f74f7917dcf7.svg[mermaid diagram] + +A good example would be a periodic events stream where the events get pushed every second to multiple https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events[server-sent event] subscribers. + +Let us see how this can be done using Mutiny and the https://quarkus.io/[Quarkus framework]. + +[NOTE] +==== + +We use Quarkus to illustrate a classic real-world usage for the broadcast operator, but the same kind of construct +could be done with another framework or the https://vertx.io/[Vert.x toolkit]. + +==== + +== Use-case: dynamic subscribers + +We expose a HTTP endpoint for server-sent events, and each second we receive the current list of subscribers. +The endpoint is exposed on path `/hello/ticks/++{++id}` where `id` is an identifier for a subscriber. + +We could subscribe and follow the stream using https://httpie.io/[HTTPie] for subscriber `1`: + +[source,text] +---- +$ http --stream :8080/hello/ticks/1 +HTTP/1.1 200 OK +Content-Type: text/event-stream +X-SSE-Content-Type: text/plain +transfer-encoding: chunked + +data:1 + +data:1 + +data:1 + +data:1,2 + +data:1,2 +---- + +The first 3 ticks have just one subscriber (`data:1`), but then once another subscriber arrives at path `/hello/ticks/2` we get ticks with identifiers `1,2`. + +When the connection for subscriber `1` closes, we see the impact on subscriber `2`: + +[source,text] +---- +http --stream :8080/hello/ticks/2 +HTTP/1.1 200 OK +Content-Type: text/event-stream +X-SSE-Content-Type: text/plain +transfer-encoding: chunked + +data:1,2 + +data:1,2 + +data:2 + +data:2 + +data:2 +---- + +== Why is broadcasting required? + +Mutiny offers a publisher for periodic event streams: + +[source,java] +---- +var ticks = Multi.createFrom().ticks().every(Duration.ofSeconds(1)); +---- + +You might wonder why we couldn't simply build our Quarkus endpoints as in: + +[source,java] +---- +@Path("/hello") +public class GreetingResource { + + @GET + @Path("ticks/{id}") + @RestStreamElementType(MediaType.TEXT_PLAIN) + public Multi ticks(String id) { + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + // (rest of the pipeline...) + ; + } +} +---- + +The problem here is that instead of having a single events source to emit ticks, you will have as many as there are subscribers: + +image::images/diagrams/mermaid-cbd13fb6d19b.svg[mermaid diagram] + +You might be able to articulate some logic around such construct, but you will end up with many scheduled operations on the Quarkus thread pool. + +Even worse: if your source is not periodic ticks emitted by Mutiny but some event source (e.g., a Kafka topic, a message broker queue, etc) then you might add correctness issues (e.g., not all subscribers get all messages). + +== Broadcasting, listening to subscriptions and cancellations + +It is fortunately quite easy to express such construct by sharing a common `Multi`, and broadcasting events to each subscriber. + +Let's start with the HTTP endpoint: + +[source,java] +---- +private ConcurrentHashSet identifiers = new ConcurrentHashSet<>(); + +private Multi ticks = (...); + +@GET +@Path("ticks/{id}") +@RestStreamElementType(MediaType.TEXT_PLAIN) +public Multi ticks(String id) { + Log.info("New client with id " + id); + identifiers.add(id); + return ticks.onCancellation().invoke(() -> { + Log.info("Removing client with id " + id); + identifiers.remove(id); + }); +} +---- + +The `ticks` method registers a new client in `identifiers`, and removes it upon cancellation. +The returned `Multi` pipeline piggy-backs on top of the _shared_ `Multi`, which is built as follows: + +[source,java] +---- +private Multi ticks = Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .onItem().transform(tick -> { + Log.info("tick"); + return identifiers.stream().collect(Collectors.joining(",")); + }) + .onSubscription().invoke(() -> Log.info("Starting to emit ticks")) + .onCancellation().invoke(() -> Log.info("No more ticks")) + .broadcast() + .withCancellationAfterLastSubscriberDeparture() + .toAtLeast(1); +---- + +Here are a few observations. + +[arabic] +. For each periodic tick event, we assemble the current subscribers as a string of the form `"1,2,3"` with `.onItem().transform(...)`. +. We log an event when the periodic event stream starts (see `onSubscription().invoke(...)`). +. We log an event when the periodic event stream stops (see `onCancellation().invoke(...)`). +. We broadcast events to all subscribers, but: +[arabic] +.. there must be at least one subscriber before the stream starts, and +.. the stream is cancelled when the last subscriber departs. + +This construction is quite interesting because we don't emit ticks when there are no subscribers, and we stop it when there are none. + +If you play with such an example then you will see logs similar to these: + +[source,text] +---- +INFO [org.acm.GreetingResource] (vert.x-eventloop-thread-0) New client with id 1 +INFO [org.acm.GreetingResource] (vert.x-eventloop-thread-0) Starting to emit ticks +INFO [org.acm.GreetingResource] (executor-thread-1) tick +INFO [org.acm.GreetingResource] (executor-thread-1) tick +INFO [org.acm.GreetingResource] (vert.x-eventloop-thread-0) Removing client with id 1 +INFO [org.acm.GreetingResource] (vert.x-eventloop-thread-0) No more ticks +INFO [org.acm.GreetingResource] (vert.x-eventloop-thread-1) New client with id 1 +INFO [org.acm.GreetingResource] (vert.x-eventloop-thread-1) Starting to emit ticks +INFO [org.acm.GreetingResource] (executor-thread-1) tick +INFO [org.acm.GreetingResource] (executor-thread-1) tick +INFO [org.acm.GreetingResource] (executor-thread-1) tick +INFO [org.acm.GreetingResource] (vert.x-eventloop-thread-0) New client with id 2 +INFO [org.acm.GreetingResource] (executor-thread-1) tick +INFO [org.acm.GreetingResource] (executor-thread-1) tick +INFO [org.acm.GreetingResource] (vert.x-eventloop-thread-1) Removing client with id 1 +INFO [org.acm.GreetingResource] (executor-thread-1) tick +INFO [org.acm.GreetingResource] (executor-thread-1) tick +INFO [org.acm.GreetingResource] (executor-thread-1) tick +INFO [org.acm.GreetingResource] (vert.x-eventloop-thread-0) Removing client with id 2 +INFO [org.acm.GreetingResource] (vert.x-eventloop-thread-0) No more ticks +---- diff --git a/content/docs/smallrye-mutiny/2/guides/collecting-items.adoc b/content/docs/smallrye-mutiny/2/guides/collecting-items.adoc new file mode 100644 index 000000000..83772a291 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/guides/collecting-items.adoc @@ -0,0 +1,91 @@ += Collecting items from Multi +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,beginner + +There are cases where you want to accumulate the items from a `Multi` and produce a `Uni` as a final result. +This is also called a _reduction_ in functional programming. + +The `Uni` fires its item when the `Multi` completes. +Mutiny provides multiple operators to deal with that scenario. +They are available from the `collect()` group. +For example, you can store the items in a list, emit the list on completion, or use a Java `Collector` to customize the aggregation. + +[CAUTION] +==== + +Don't collect items from infinite streams or you will likely end with an out-of-memory failure! + +==== + +== Collecting items into a list + +One of the most common approaches to collect items is to store them in a list (`Uni++<++List++<++T++>>++`) +It emits the final list when the `Multi` completes. + +image::images/diagrams/mermaid-69aa56febc93.svg[mermaid diagram] + +How to achieve this with Mutiny? + +[source,java] +---- +include::{examples-dir}/java/guides/operators/CollectingItemsTest.java[tags=list,indent=0] +---- + +It's important to note that the returned type is a `Uni`. +It emits the list when the multi completes. + +== Collecting items into a map + +You can also collect the items into a `Map`. +In this case, you need to provide a function to compute the key for each item: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/CollectingItemsTest.java[tags=map,indent=0] +---- + +If the key mapper function returns the same key for multiple items, the last one with that key is stored in the final `Map`. +You can collect items in a _multimap_ to handle items with the same keys. + +== Collecting items into a multimap + +A multimap is a `Map++<++K, List++<++T++>>++.` +In the case of a conflicting key, it stores all the items in a list associated with that key. + +[source,java] +---- +include::{examples-dir}/java/guides/operators/CollectingItemsTest.java[tags=multimap,indent=0] +---- + +== Using a custom accumulator + +You can also use a custom _accumulator_ function: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/CollectingItemsTest.java[tags=accumulator,indent=0] +---- + +The `in` method receives two arguments: + +[arabic] +. a supplier providing the new instance of your collection/container +. the accumulator function + +You can also use a Java `Collector`. +For example, in the next example, count the number of items, and produce the final count as item: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/CollectingItemsTest.java[tags=collector,indent=0] +---- + +== Getting the first and last items + +While they are not strictly speaking collecting items, `collect().first()` and `collect().last()` allow retrieving the first and last item from a `Multi`: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/CollectingItemsTest.java[tags=first,indent=0] +---- diff --git a/content/docs/smallrye-mutiny/2/guides/combining-items.adoc b/content/docs/smallrye-mutiny/2/guides/combining-items.adoc new file mode 100644 index 000000000..05cf2c2b8 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/guides/combining-items.adoc @@ -0,0 +1,107 @@ += Combining items from streams +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,intermediate + +Combining items from various streams is an essential pattern in Reactive Programming. + +It associates the emitted items from multiple streams and emits an _aggregate_. +The downstream receives this _aggregate_ and can handle it smoothly. + +There are plenty of use cases, such as executing two tasks concurrently and waiting for both completions, getting the last items from different streams to build an always up-to-date view, and so on. + +== Combining Unis + +Imagine that you have two asynchronous operations to perform like 2 HTTP requests. +You want to send these requests and be notified when both have completed with their responses ready to be consumed. + +Of course, you could send the first request, wait for the response, and then send the second request. +If both requests are independent, we can do something better: send both concurrently and await for both completions! + +image::images/diagrams/mermaid-c54f60b30f42.svg[mermaid diagram] + +How can you achieve this with Mutiny? + +First, each request is a `Uni`, so we have: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/CombiningItemsTest.java[tags=invocations,indent=0] +---- + +Then, we want to combine both _responses_: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/CombiningItemsTest.java[tags=combination,indent=0] +---- + +This code creates a new `Uni` produced by combining `uniA` and `uniB`. +The responses are aggregated inside a `Tuple`: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/CombiningItemsTest.java[tags=subscription,indent=0] +---- + +The `tuple` aggregates the responses in the same order as the `Uni` sequence. + +If one of the `Uni` fails, so does the combination and you receive the failure: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/CombiningItemsTest.java[tags=invocations,indent=0] +include::{examples-dir}/java/guides/operators/CombiningItemsTest.java[tags=combination,indent=0] +include::{examples-dir}/java/guides/operators/CombiningItemsTest.java[tags=subscription,indent=0] +---- + +Using tuples is convenient but only works if you have less than 10 `Uni` objects. +If you want another structure or deal with 10 `Uni` objects or more then use `combineWith`: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/CombiningItemsTest.java[tags=combined-with,indent=0] +---- + +== Combining Multis + +Combining `Multis` consists of associating items from different stream per _index_: + +image::images/diagrams/mermaid-3d4459e3c6a6.svg[mermaid diagram] + +It associates the first items from the combined streams, then the second items: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/CombiningItemsTest.java[tags=combine-multi,indent=0] +---- + +As for `Uni`, you can aggregate the item into tuples (up to 9 items) or combine with a combinator function: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/CombiningItemsTest.java[tags=combine-multi-with,indent=0] +---- + +If one of the streams fails, the combined stream propagates the failure and stops the emission. +The combined stream completes as soon as one of the observed stream sends the completion event. + +[NOTE] +==== + +If one of the observed streams never emits any item then the combined stream will not emit anything. + +==== + +== Combining the latest items of Multis + +It can be useful to combine multiple `Multi` streams and receive the _latest_ items from each stream on every emission: + +image::images/diagrams/mermaid-f0001b5cde9f.svg[mermaid diagram] + +This is achieved using `latest()`: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/CombiningItemsTest.java[tags=combine-last,indent=0] +---- diff --git a/content/docs/smallrye-mutiny/2/guides/completion-stage.adoc b/content/docs/smallrye-mutiny/2/guides/completion-stage.adoc new file mode 100644 index 000000000..9ad73ee5a --- /dev/null +++ b/content/docs/smallrye-mutiny/2/guides/completion-stage.adoc @@ -0,0 +1,86 @@ += How to deal with CompletionStage? +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,intermediate + +`CompletionStage` and `CompletableFuture` are classes provided by Java to represent asynchronous actions. + +== Differences between Uni and CompletionStage + +While `CompletionStage` and `CompletableFuture` are close to `Uni` in terms of use case, there are some fundamental differences. + +`CompletionStage` are _eager_. +When a method returns a `CompletionStage,` the operation has already been triggered. +The outcome is used to complete the returned `CompletionStage`. +On the other side, `Unis` are lazy. +The operation is only triggered once there is a subscription. + +`CompletionStage` _caches_ the outcome. +So, once received, you can retrieve the result. +Every retrieval will get the same result. +With `Uni`, every subscription has the opportunity to re-trigger the operation and gets a different result. + +[TIP] +==== + +You can also _cache_ the outcome with `Uni.memoize().indefinitely()`. + +==== + +== From Uni to CompletionStage + +You can create a `CompletionStage` from `Uni` using `uni.subscribeAsCompletionStage()`. + +[source,java] +---- +include::{examples-dir}/java/guides/CompletionStageTest.java[tags=uni-subscribe-cs,indent=0] +---- + +It's important to understand that retrieving a `CompletionStage` subscribes to the `Uni`. +If you do this operation twice, it subscribes to the `Uni` twice and re-trigger the operation. + +[source,java] +---- +include::{examples-dir}/java/guides/CompletionStageTest.java[tags=uni-subscribe-cs-twice,indent=0] +---- + +== Creating a Uni from a CompletionStage + +To create a `Uni` from a `CompletionStage`, use `Uni.createFrom().completionStage(...)`. + +[source,java] +---- +include::{examples-dir}/java/guides/CompletionStageTest.java[tags=create-uni,indent=0] +---- + +As you can see, there are two versions. +The first one receives the `CompletionStage` directly, while the second one gets a supplier. +In the case of multiple subscriptions on the produced `Uni`, the supplier is called multiple times (once per subscription), and so can change the return `CompletionStage`. +It also delays the creation of the `CompletionStage` until there is a subscription, which only triggers the operation at that time. +If you pass the instance directly, it will always use the same one (even for multiple subscriptions) and triggers the operation even if there is no subscription. +For these reasons, it is generally better to use the variant accepting a supplier. + +Note that if the completion stage produces a `null` value, the resulting `Uni` emits `null` as item. +If the completion stages complete exceptionally, the failure is emitted by the resulting `Uni`. + +== Creating a Multi from a CompletionStage + +To create a `Multi` from a `CompletionStage`, use `Multi.createFrom().completionStage(...)`. +It produces: + +* a multi emitting an item and completing - if the value produced by the completion stage is not `null`, +* an empty multi if the value produced by the completion stage is `null`, +* a failed multi is completion stage is completed exceptionally. + +[source,java] +---- +include::{examples-dir}/java/guides/CompletionStageTest.java[tags=create-multi,indent=0] +---- + +For the same reason as for `Uni`, there are two versions: + +[arabic] +. one accepting a `CompletionStage` directly +. one accepting a `Supplier++<++CompletionStage++>++`, called at subscription-time, for every subscription. + +It is recommended to use the second version. diff --git a/content/docs/smallrye-mutiny/2/guides/context-passing.adoc b/content/docs/smallrye-mutiny/2/guides/context-passing.adoc new file mode 100644 index 000000000..4dcb0b0a1 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/guides/context-passing.adoc @@ -0,0 +1,90 @@ += Context passing +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,advanced + +Mutiny reactive pipelines let data flow from publishers to subscribers. + +In the vast majority of cases a publisher shall have _all_ required data, and operators shall perform processing based on item values. +For instance a network request shall be made with all request data known in advance, and response processing shall only depend on the response payload. + +That being said there are cases were this is not sufficient, and some data has to be carried along with items. +For instance one intermediary operator in a pipeline may have to make another networked request from which we need to extract some correlation identifier which will be used by another operator down the pipeline. +In such cases one will be tempted to forward tuples consisting of some item value plus some "extra" data. + +For such cases Mutiny offers a _subscriber-provided context_, so all operators involved in a subscription can share some form of _implicit data_. + +== What's in a context? + +A context is a simple key / value, in-memory storage. +Data can be queried, added and deleted from a context, as shown in the following snippet: + +[source,java] +---- +include::{examples-dir}/java/guides/ContextPassingTest.java[tags=contextManipulation,indent=0] +---- + +`Context` objects are thread-safe, and can be created from sequences of key / value pairs (as shown above), from a Java `Map`, or they can be created empty. + +Note that an empty-created context defers its internal storage allocation until the first call to `put`. +You can see `Context` as a glorified `ConcurrentHashMap` delegate, although this is an implementation detail and Mutiny might explore various internal storage strategies in the future. + +[TIP] +==== + +Contexts shall be primarily used to share transient data used for networked I/O processing such as correlation identifiers, tokens, etc. + +They should not be used as general-purpose data structures that are frequently updated and that hold large amounts of data. + +==== + +== How to access a context? + +Given a `Uni` or a `Multi`, a context can be accessed using the `withContext` operator, as in: + +[source,java] +---- +include::{examples-dir}/java/guides/ContextPassingTest.java[tags=contextSampleUsage,indent=0] +---- + +This operator builds a sub-pipeline using 2 parameters: the current `Uni` or `Multi` and the context. + +[IMPORTANT] +==== + +The function passed to `withContext` is called at subscription time. + +This means that the context has not had a chance to be updated by upstream operators yet, so be careful with what you do in the body of that function. + +==== + +There is another way to access the context by using the `attachContext` method: + +[source,java] +---- +include::{examples-dir}/java/guides/ContextPassingTest.java[tags=contextAttachedSampleUsage,indent=0] +---- + +This method materializes the context in the regular pipeline items using the wrapper `ItemWithContext` class. +The `get` method provides the item while the `context` method provides the context. + +== How to access a context at the pipeline source? + +The `Uni` and `Multi` _builder_ methods like `Multi.createFrom()` provide publishers, not operators, so they don't have the `withContext` method. + +The first option is to use the `Uni.createFrom().context(...)` or `Multi.createFrom().context(...)` general purpose method to materialize the context: + +[source,java] +---- +include::{examples-dir}/java/guides/ContextPassingTest.java[tags=builderUsage,indent=0] +---- + +The `context` method takes a function that accepts a `Context` and returns a pipeline. +This is very similar to the `deferred` builder. + +If you use an `emitter` builder then for both `Uni` and `Multi` cases the emitter object offers a `context` method to access the context: + +[source,java] +---- +include::{examples-dir}/java/guides/ContextPassingTest.java[tags=emitterUsage,indent=0] +---- diff --git a/content/docs/smallrye-mutiny/2/guides/controlling-demand.adoc b/content/docs/smallrye-mutiny/2/guides/controlling-demand.adoc new file mode 100644 index 000000000..a5f0b0c77 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/guides/controlling-demand.adoc @@ -0,0 +1,55 @@ += Controlling the demand +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,advanced + +== Pacing the demand + +A subscription is used for 2 purposes: cancelling a request and demanding batches of items. + +The `Multi.paceDemand()` operator can be used to automatically issue requests at certain points in time. + +The following example issues requests of 25 items every 100ms: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/ControlDemandTest.java[tags=pacing,indent=0] +---- + +`FixedDemandPacer` is a simple _pacer_ with a fixed demand and a fixed delay. + +You can create more elaborated pacers by implementing the `DemandPacer` interface. +To do so you provide an initial request and a function to evaluate the next request which is evaluated based on the previous request and the number of items emitted since the last request: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/ControlDemandTest.java[tags=custom-pacer,indent=0] +---- + +The previous example is a custom pacer that doubles the demand and increases the delay for each new request. + +== Capping the demand requests + +The `capDemandsTo` and `capDemandUsing` operators can be used to cap the demand from downstream subscribers. + +The `capDemandTo` operator defines a maximum demand that can flow: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/ControlDemandTest.java[tags=capConstant,indent=0] +---- + +Here we cap requests to 50 items, so it takes 2 requests to get all 100 items of the upstream range. +The first request of 75 items is capped to a request of 50 items, leaving an outstanding demand of 25 items. +The second request of 25 items is added to the outstanding demand, resulting in a request of 50 items and completing the stream. + +You can also define a custom function that provides a capping value based on a custom formula, or based on earlier demand observations: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/ControlDemandTest.java[tags=capFunction,indent=0] +---- + +Here we have a function that requests 75% of the downstream requests. + +Note that the function must return a value `n` that satisfies `(0 ++<++ n ++<++= requested)` where `requested` is the downstream demand. diff --git a/content/docs/smallrye-mutiny/2/guides/converters.adoc b/content/docs/smallrye-mutiny/2/guides/converters.adoc new file mode 100644 index 000000000..313692321 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/guides/converters.adoc @@ -0,0 +1,253 @@ += Using other reactive programming libraries +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,advanced + +You may need to integrate libraries exposing an API using other reactive programming libraries such as RX Java or Reactor. +Mutiny has a built-in conversion mechanism to ease that integration. + +== Picking the right dependency + +You need to add another dependency to access the converters. +Each artifact contains the converters for a specific reactive library. +Pick the right one and add it to your project: + +[.tabs] +==== + +Reactor:: ++ +-- + +[source,xml,subs="attributes+"] +---- + + + io.smallrye.reactive + mutiny-reactor + {versions-mutiny} + +---- + +-- + +RxJava 3:: ++ +-- + +[source,xml,subs="attributes+"] +---- + + + io.smallrye.reactive + mutiny-rxjava3 + {versions-mutiny} + +---- + +-- + +==== + +== Integration with Project Reactor + +https://projectreactor.io/[Project Reactor] is a popular reactive programming library. +It offers two types: `Mono` and `Flux,` both implementing Reactive Stream `Publisher`. + +To use the Reactor `++<++-++>++` Mutiny converter, add the following imports to your class: + +[source,java] +---- +import io.smallrye.mutiny.converters.multi.MultiReactorConverters; +import io.smallrye.mutiny.converters.uni.UniReactorConverters; +---- + +=== Converting a Flux or a Mono into a Multi + +Both `Flux` and `Mono` implement `Publisher`. +As a result, we can use the Reactive Streams interoperability to convert instances from `Flux++<++T++>++` and `Mono++<++T++>++` to `Multi++<++T++>++`: + +[source,java] +---- +include::{examples-dir}/java/guides/integration/ReactorTest.java[tags=reactor-multi-create,indent=0] +---- + +[WARNING] +==== + +Reactor still uses the legacy Reactive Streams APIs instead of `java.util.concurrent.Flow`, so you need to perform an adaptation. + +We recommend using the https://smallrye.io/smallrye-mutiny-zero/[Mutiny Zero Flow Adapters library] as in these examples (Maven coordinates `io.smallrye.reactive:mutiny-zero-flow-adapters`). + +==== + +=== Converting a Flux or a Mono into a Uni + +As you can create `Uni` from a `Publisher`, the same approach can be used to create `Uni` instances: + +[source,java] +---- +include::{examples-dir}/java/guides/integration/ReactorTest.java[tags=reactor-uni-create,indent=0] +---- + +When a `Flux` or `Mono` sends the _completion_ event without having emitted any item, the resulting `Uni` emits `null`. + +When converting a `Flux` to `Uni`, the resulting `Uni` emits the first item. +After that emission, it cancels the subscription to the `Flux`. + +=== Converting a Multi into a Flux or Mono + +Converting a `Multi` into a `Flux` or a `Mono` uses the Reactive Streams interoperability: + +[source,java] +---- +include::{examples-dir}/java/guides/integration/ReactorTest.java[tags=reactor-create-multi,indent=0] +---- + +=== Converting a Uni into a Flux or Mono + +Converting a `Uni` into a `Flux` or a `Mono` requires a converter, as `Uni` does not implement Reactive Streams. + +[source,java] +---- +include::{examples-dir}/java/guides/integration/ReactorTest.java[tags=reactor-create-uni,indent=0] +---- + +If the `Uni` emits `null`, it sends the _completion_ event. + +=== Using converter instead of Reactive Streams + +While Reactive Streams interoperability is convenient, Mutiny also provides converters to create `Flux` and `Mono` from `Uni` and `Multi`: + +[source,java] +---- +include::{examples-dir}/java/guides/integration/ReactorTest.java[tags=uni-export,indent=0] +include::{examples-dir}/java/guides/integration/ReactorTest.java[tags=multi-export,indent=0] +---- + +== Integration with RX Java 3 + +RxJava is another popular reactive programming library. +It offers 5 types: `Completable` (no item), `Single` (one item), `Maybe` (0 or 1 item), `Observable` (multiple items), `Flowable` (multiple items, implements Reactive Stream `Publisher`). + +To use the RxJava `++<++-++>++` Mutiny converters, add the following imports to your class: + +[source,java] +---- +import io.smallrye.mutiny.converters.multi.MultiRx3Converters; +import io.smallrye.mutiny.converters.uni.UniRx3Converters; +---- + +=== Converting an Observable or a Flowable into a Multi + +Both `Observable` and `Flowable` are item streams. +However, `Observable` does not implement `Publisher` and so does not have back-pressure support. + +To create `Multi` from an `Observable,` you need a specific converter: + +[source,java] +---- +include::{examples-dir}/java/guides/integration/RxJavaTest.java[tags=rx-multi-create-observable,indent=0] +---- + +Converting a `Flowable` is easier, as it's a `Publisher`: + +[source,java] +---- +include::{examples-dir}/java/guides/integration/RxJavaTest.java[tags=rx-multi-create-flowable,indent=0] +---- + +[WARNING] +==== + +Like Reactor, RxJava still uses the legacy Reactive Streams APIs instead of `java.util.concurrent.Flow`, so you need to perform an adaptation. + +==== + +=== Converting a Completable, Single or Maybe into a Multi + +To create a `Multi` from a `Completable,` `Single` or `Maybe` you need specific converters, as none of these types implement Reactive Streams. + +[source,java] +---- +include::{examples-dir}/java/guides/integration/RxJavaTest.java[tags=rx-multi-create-single,indent=0] +---- + +* Creating a `Multi` from a `Completable` always produces a `Multi++<++Void++>++` that only emits the _completion_ or _failure_ event. +* Creating a `Multi` from a `Single` produces a `Multi`. That `Multi` emits the item and then completes it. +* Creating a `Multi` from a `Maybe` produces a `Multi`. That `Multi` emits the item (if any) and then completes it. +If the `Maybe` is empty, then the created `Multi` emits the _completion_ event. + +When a `Completable,` `Single,` or `Maybe` emits a failure, then the resulting `Multi` emits that failure. + +=== Converting an Observable or a Flowable into a Uni + +To create a `Uni` from an `Observable,` you need to use a specific converter: + +[source,java] +---- +include::{examples-dir}/java/guides/integration/RxJavaTest.java[tags=rx-uni-create-observable,indent=0] +---- + +The creation from a `Flowable` can be done using the Reactive Streams interoperability: + +[source,java] +---- +include::{examples-dir}/java/guides/integration/RxJavaTest.java[tags=rx-uni-create-flowable,indent=0] +---- + +In both cases, it cancels the subscription to the `Flowable` or `Observable` after receiving the first item. +If the `Flowable` or `Observable` completes without items, the `Uni` emits a `null` item. + +=== Converting a Completable, Single or Maybe into a Uni + +To create a `Uni` from a `Completable,` `Single,` or `Maybe`, you need to use a specific converter: + +[source,java] +---- +include::{examples-dir}/java/guides/integration/RxJavaTest.java[tags=rx-uni-create-single,indent=0] +---- + +Converting a `Completable` to a `Uni` always produces a `Uni++<++Void++>++,` that emits either `null` once the `Completable` completes or the failure if it fails. +The `Maybe` to `Uni` conversion emits a `null` item if the `Maybe` completes without an item. + +=== Converting a Multi into a RX Java objects + +The conversion from a `Multi` to the various RX Java objects is done using converters: + +[source,java] +---- +include::{examples-dir}/java/guides/integration/RxJavaTest.java[tags=create-rx-from-multi,indent=0] +---- + +The creation of a `Completable` from a `Multi` discards all the items emitted by the `Multi`. +It only forwards the _completion_ or _failure_ event. + +Converting a `Multi` into a `Single` returns a `Single++<++Optional++<++T++>>++,` as the `Multi` may complete without items. +You can also produce a `Single++<++T++>++` and emit a _failure_ event if the `Multi` completes without items. +You can configure the thrown exception using `onEmptyThrow.` + +[TIP] +==== + +You can also create a `Flowable` from a `Multi` using: `Flowable.fromPublisher(multi)`. + +==== + +=== Converting a Uni into a RX Java type + +Similarly to the conversion from a `Multi` into an RX Type, converting a `Uni` requires a converter: + +[source,java] +---- +include::{examples-dir}/java/guides/integration/RxJavaTest.java[tags=create-rx-from-uni,indent=0] +---- + +The creation of a `Completable` from a `Uni` discards the item and sends the _completion_ signal after emission. + +Converting a `Uni` into a `Single` returns a `Single++<++Optional++<++T++>>++,` as the `Uni` may emit `null.` +You can also produce a `Single++<++T++>++` and emits a _failure_ event if the `Uni` sends `null.` +Configure the failure to forward using `failOnNull.` + +The creation of a `Maybe,` `Flowable,` or an `Observable` from a `Uni` produces an empty `Maybe,` `Flowable,` or `Observable` if the `Uni` emits `null.` +For `Flowable` and `Observable,` if the `Uni` emits a _non-null_ item, that item is emitted, followed immediately by the _completion_ signal. diff --git a/content/docs/smallrye-mutiny/2/guides/custom-operators.adoc b/content/docs/smallrye-mutiny/2/guides/custom-operators.adoc new file mode 100644 index 000000000..8c2baa638 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/guides/custom-operators.adoc @@ -0,0 +1,30 @@ += Can I have custom operators? +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,advanced + +Yes, but please write operators responsibly! + +Both `Uni` and `Multi` support custom operators using the `plug` operator. +Here is an example where we use a custom `Multi` operator that randomly drops items: + +[source,java] +---- +include::{examples-dir}/java/guides/PlugTest.java[tags=plug,indent=0] +---- + +with the operator defined as follows: + +[source,java] +---- +include::{examples-dir}/java/guides/PlugTest.java[tags=custom-operator,indent=0] +---- + +[CAUTION] +==== + +Custom operators are an advanced feature: when possible please use the existing operators and use helpers such as `stage` to write readable code. + +In the case of custom `Multi` operators it is wise to test them against the _Reactive Streams TCK_. + +==== diff --git a/content/docs/smallrye-mutiny/2/guides/delaying-events.adoc b/content/docs/smallrye-mutiny/2/guides/delaying-events.adoc new file mode 100644 index 000000000..c62577753 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/guides/delaying-events.adoc @@ -0,0 +1,68 @@ += How to delay events? +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,intermediate + +== Delaying Uni's item + +When you have a `Uni`, you can delay the item emission using `onItem().delayIt().by(...)`: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/DelayTest.java[tags=delay-by,indent=0] +---- + +You pass a duration. +When the item is received, it _waits for_ that duration before propagating it to the downstream consumer. + +You can also delay the item's emission based on another _companion_ `Uni`: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/DelayTest.java[tags=delay-until,indent=0] +---- + +The item is propagated downstream when the `Uni` returned by the function emits an item (possibly `null`). +If the function emits a failure (or throws an exception), this failure is propagated downstream. + +== Throttling a Multi + +Multi does not have a _delayIt_ operator because applying the same delay to all items is rarely what you want to do. +However, there are several ways to apply a delay in a `Multi`. + +First, you can use the `onItem().call()`, which delays the emission until the `Uni` produced the `call` emits an item. +For example, the following snippet delays all the items by 10 ms: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/DelayTest.java[tags=delay-multi,indent=0] +---- + +In general, you don't want to apply the same delay to all the items. +You can combine `call` with a random delay as follows: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/DelayTest.java[tags=delay-multi-random,indent=0] +---- + +Finally, you may want to throttle the items. +For example, you can introduce a (minimum) one-second delay between each item. +To achieve this, combine `Multi.createFrom().ticks()` and the multi to throttled: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/DelayTest.java[tags=throttling-multi,indent=0] +---- + +[TIP] +==== + +The `onOverflow().drop()` is used to avoid the _ticks_ to fail if the other stream (`multi`) is too slow. + +==== + +== Delaying other types of events + +We have looked at how to delay items, but you may need to delay other events, such as subscription or failure. +For these, use the `call` approach, and return a `Uni` that delay the event's propagation. diff --git a/content/docs/smallrye-mutiny/2/guides/dropped-exceptions.adoc b/content/docs/smallrye-mutiny/2/guides/dropped-exceptions.adoc new file mode 100644 index 000000000..10d0a1524 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/guides/dropped-exceptions.adoc @@ -0,0 +1,26 @@ += How to deal with dropped exceptions? +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,advanced + +There are a few corner cases where Mutiny cannot propagate an exception to a `Uni` or a `Multi` subscriber. + +Consider the following example: + +[source,java] +---- +include::{examples-dir}/java/guides/infrastructure/DroppedExceptionTest.java[tags=code,indent=0] +---- + +The `onCancellation().call(...)` method is called when the `Uni` subscription is cancelled. +The returned `Uni` is failed with a `IOException`, but since the subscription itself has been cancelled then there is no way to catch the exception. + +By default Mutiny reports such dropped exceptions to the standard error stream along with the corresponding stack trace. +You can change how these exceptions are handled using `Infrastructure.setDroppedExceptionHandler`. + +The following logs dropped exceptions to a logger: + +[source,java] +---- +include::{examples-dir}/java/guides/infrastructure/DroppedExceptionTest.java[tags=override-handler,indent=0] +---- diff --git a/content/docs/smallrye-mutiny/2/guides/eliminate-duplicates-and-repetitions.adoc b/content/docs/smallrye-mutiny/2/guides/eliminate-duplicates-and-repetitions.adoc new file mode 100644 index 000000000..4781db7ef --- /dev/null +++ b/content/docs/smallrye-mutiny/2/guides/eliminate-duplicates-and-repetitions.adoc @@ -0,0 +1,60 @@ += Eliminate duplicates and repetitions +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,beginner + +When observing a `Multi`, you may see duplicated items or repetitions. +The `multi.select()` and `multi.skip()` groups provide methods to only select distinct items or drop repetitions. + +== Selecting distinct + +The `.select().distinct()` operator removes all the duplicates. +As a result, the downstream only contains distinct items: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/RepetitionsTest.java[tags=distinct,indent=0] +---- + +If you have a stream emitting the `++{++1, 1, 2, 3, 4, 5, 5, 6, 1, 4, 4}` items, then applying `.select().distinct()` on such a stream produces: `++{++1, 2, 3, 4, 5, 6}`. + +[IMPORTANT] +==== + +The operator keeps a reference on all the emitted items, and so, it could lead to memory issues if the stream contains too many distinct items. + +==== + +[TIP] +==== + +By default, `select().distinct()` uses the `hashCode` method from the item's class. +You can pass a custom comparator for more advanced checks. + +==== + +If you have a stream emitting items of type `T`, where duplicates can be identified through an attribute of `T` of type `K`, +then an `extractor` of type `Function++<++T, K++>++` can be defined. Applying `.select().distinct(extractor)` on such a stream will +eliminate duplicates but have a lesser memory overhead as only the references to the extracted keys need to be kept, not the whole object. +A typical usage of this might be for a stream of records where uniqueness is determined by a UUID assigned to every record. + +== Skipping repetitions + +The `.skip().repetitions()` operator removes subsequent repetitions of an item: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/RepetitionsTest.java[tags=repetition,indent=0] +---- + +If you have a stream emitting the `++{++1, 1, 2, 3, 4, 5, 5, 6, 1, 4, 4}` items, then applying `.skip().repetitions()` on such a stream produces: `++{++1, 2, 3, 4, 5, 6, 1, 4}`. + +Unlike `.select().distinct()`, you can use this operator on large or infinite streams. + +[TIP] +==== + +By default, `skip().repetitions()` uses the `equals` method from the item's class. +You can pass a custom comparator for more advanced checks. + +==== diff --git a/content/docs/smallrye-mutiny/2/guides/emission-threads.adoc b/content/docs/smallrye-mutiny/2/guides/emission-threads.adoc new file mode 100644 index 000000000..4e3144069 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/guides/emission-threads.adoc @@ -0,0 +1,36 @@ += How to change the emission thread? +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,intermediate + +Except indicated otherwise, Mutiny invokes the next _stage_ using the thread emitting the event from upstream. +So, in the following code, the _transform_ stage is invoked from the thread emitting the event. + +[source,java] +---- +include::{examples-dir}/java/guides/operators/EmitOnTest.java[tags=example,indent=0] +---- + +You can switch to another thread using the `emitOn` operator. +The `emitOn` operator lets you switch the thread used to dispatch (upstream -++>++ downstream) events, so items, failure and completion events. +Just pass the _executor_ you want to use. + +[source,java] +---- +include::{examples-dir}/java/guides/operators/EmitOnTest.java[tags=code,indent=0] +---- + +[NOTE] +==== + +You cannot pass a specific thread, but you can implement a simple `Executor` dispatching on that specific thread, or use a _single threaded executor_. + +==== + +[WARNING] +==== + +Be careful as this operator can lead to concurrency problems with non thread-safe objects such as CDI request-scoped beans. +It might also break reactive-streams semantics with items being emitted concurrently. + +==== diff --git a/content/docs/smallrye-mutiny/2/guides/emit-on-vs-run-subscription-on.adoc b/content/docs/smallrye-mutiny/2/guides/emit-on-vs-run-subscription-on.adoc new file mode 100644 index 000000000..7588f9234 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/guides/emit-on-vs-run-subscription-on.adoc @@ -0,0 +1,43 @@ += What is the difference between emitOn and runSubscriptionOn? +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,intermediate + +The `emitOn` and `runSubscriptionOn` are 2 operators influencing on which threads the event are dispatched. +However, they target different types of events and different directions. + +== The case of emitOn + +`emitOn` takes events coming from upstream (items, completion, failure) and replays them downstream on a thread from the given executor. +Consequently, it affects where the subsequent operators execute (until another `emitOn` is used): + +[source,java] +---- +include::{examples-dir}/java/guides/operators/RunSubscriptionOnTest.java[tags=emitOn,indent=0] +---- + +The previous code produces the following sequence: + +image::images/diagrams/mermaid-18f9a50c0627.svg[mermaid diagram] + +[WARNING] +==== + +Be careful as this operator can lead to concurrency problems with non thread-safe objects such as CDI request-scoped beans. +It might also break reactive-streams semantics with items being emitted concurrently. + +==== + +== The case of runSubscriptionOn + +`runSubscriptionOn` applies to the subscription process. +It requests the upstream to run its subscription (call of the `subscribe` method on its own upstream) on a thread from the given executor: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/RunSubscriptionOnTest.java[tags=runSubscriptionOn,indent=0] +---- + +So, if we consider the previous code snippet, it produces the following sequence: + +image::images/diagrams/mermaid-76df06b35153.svg[mermaid diagram] diff --git a/content/docs/smallrye-mutiny/2/guides/filtering-items.adoc b/content/docs/smallrye-mutiny/2/guides/filtering-items.adoc new file mode 100644 index 000000000..e621770d9 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/guides/filtering-items.adoc @@ -0,0 +1,33 @@ += Filtering items from Multi +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,beginner + +When observing a `Multi`, you may not want to forward all the received items to the downstream. + +Use the `multi.select()` group to select items. + +[source,java] +---- +include::{examples-dir}/java/guides/operators/FilterTest.java[tags=filter,indent=0] +---- + +To _select_ items passing a given predicate, use `multi.select().where(predicate)`: + +`where` accepts a predicate called for each item. +If the predicate returns `true`, the item propagated downstream. +Otherwise, it drops the item. + +The predicate passed to `where` is synchronous. +The `when` method provides an asynchronous version: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/FilterTest.java[tags=test,indent=0] +---- + +`when` accepts a function called for each item. + +Unlike `where` where the predicate returns a boolean synchronously, the function returns a `Uni++<++Boolean++>++`. +It forwards the item downstream if the `uni` produced by the function emits `true`. +Otherwise, it drops the item. diff --git a/content/docs/smallrye-mutiny/2/guides/framework-integration.adoc b/content/docs/smallrye-mutiny/2/guides/framework-integration.adoc new file mode 100644 index 000000000..7dfbb8eb8 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/guides/framework-integration.adoc @@ -0,0 +1,32 @@ += How can I integrate Mutiny with my framework? +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,advanced + +Sometimes, Mutiny needs to execute tasks on other threads, such as monitoring time or delaying actions. +Most operators relying on such capacity let you pass either a `ScheduledExecutorService` or an `ExecutorService`. + +By default, Mutiny uses the a _cached_ thread pool as default executor, that creates new threads as needed, but reuse previously constructed threads when they are available. +A `ScheduledExecutorService` is also created but delegates the execution of the delayed/scheduled tasks to the default executor. + +In the case you want to integrate Mutiny with a thread pool managed by a platform, you can configure it using `Infrastructure.setDefaultExecutor()` method: + +[source,java] +---- +include::{examples-dir}/java/guides/infrastructure/InfrastructureTest.java[tags=infra,indent=0] +---- + +You can configure the default executor using the `Infrastructure.setDefaultExecutor` method: + +[source,java] +---- +include::{examples-dir}/java/guides/infrastructure/InfrastructureTest.java[tags=set-infra,indent=0] +---- + +[TIP] +==== + +If you are using Quarkus, the default executor is already configured to use the Quarkus worker thread pool. +Logging is also configured correctly. + +==== diff --git a/content/docs/smallrye-mutiny/2/guides/handling-null.adoc b/content/docs/smallrye-mutiny/2/guides/handling-null.adoc new file mode 100644 index 000000000..fa3abd3dc --- /dev/null +++ b/content/docs/smallrye-mutiny/2/guides/handling-null.adoc @@ -0,0 +1,40 @@ += How to handle null? +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,beginner + +The `Uni` type can emit `null` as item. + +While there are mixed feelings about `null`, it's part of the Java language and so handled in the `Uni` type. + +[IMPORTANT] +==== + +`Multi` does not support `null` items as it would break the compatibility with the _Reactive Streams_ protocol. + +==== + +Emitting `null` is convenient when returning `Uni++<++Void++>++`. +However, the downstream must expect `null` as item. + +Thus, `Uni` provides specific methods to handle `null` item. +`uni.onItem().ifNull()` lets you decide what you want to do when the received item is `null`: + +[source,java] +---- +include::{examples-dir}/java/guides/UniNullTest.java[tags=code,indent=0] +---- + +A symmetric group of methods is also available with `ifNotNull` which let you handle the case where the item is _not null_: + +[source,java] +---- +include::{examples-dir}/java/guides/UniNullTest.java[tags=code-not-null,indent=0] +---- + +[IMPORTANT] +==== + +While supported, emitting `null` should be avoided except for `Uni++<++Void++>++`. + +==== diff --git a/content/docs/smallrye-mutiny/2/guides/handling-timeouts.adoc b/content/docs/smallrye-mutiny/2/guides/handling-timeouts.adoc new file mode 100644 index 000000000..a687b0a37 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/guides/handling-timeouts.adoc @@ -0,0 +1,53 @@ += How to handle timeouts? +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,intermediate + +Unis are often used to represent asynchronous operations, like making an HTTP call. +So, it's not rare to need to add a timeout or a deadline on this kind of operation. +If we don't get a response (receive an item in the Mutiny lingo) before that deadline, we consider that the operation failed. + +We can then recover from this failure by using a fallback value, retrying, or any other failure handling strategy. + +To configure a timeout use `Uni.ifNoItem().after(Duration)`: + +[source,java] +---- +include::{examples-dir}/java/guides/UniTimeoutTest.java[tags=code,indent=0] +---- + +When the deadline is reached, you can do various actions. +First you can simply fail: + +[source,java] +---- +include::{examples-dir}/java/guides/UniTimeoutTest.java[tags=fail,indent=0] +---- + +A `TimeoutException` is propagated in this case. +So you can handle it specifically in the downstream: + +[source,java] +---- +include::{examples-dir}/java/guides/UniTimeoutTest.java[tags=fail-recover,indent=0] +---- + +You can also pass a custom exception: + +[source,java] +---- +include::{examples-dir}/java/guides/UniTimeoutTest.java[tags=fail-with,indent=0] +---- + +Failing and recovering might be inconvenient. +So, you can pass a fallback item or `Uni` directly: + +[source,java] +---- +include::{examples-dir}/java/guides/UniTimeoutTest.java[tags=fallback,indent=0] +---- + +[source,java] +---- +include::{examples-dir}/java/guides/UniTimeoutTest.java[tags=fallback-uni,indent=0] +---- diff --git a/content/docs/smallrye-mutiny/2/guides/hot-streams.adoc b/content/docs/smallrye-mutiny/2/guides/hot-streams.adoc new file mode 100644 index 000000000..87c93ce06 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/guides/hot-streams.adoc @@ -0,0 +1,26 @@ += Hot streams +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,advanced + +In a _cold_ stream, the stream is created when one subscriber subscribes to the stream. +So, if no one subscribes, the actual stream is not created, saving resources (that would be wasted because nobody is interested in the items). + +In a _hot_ stream, the stream exists before subscribers subscribe. +The stream emits items even if no subscribers observe the stream. +If there are no subscribers, the items are just dropped. +Subscribers only get items emitted after their subscription, meaning that any previous items would not be received. + +To create a hot stream, you can use `io.smallrye.mutiny.operators.multi.processors.BroadcastProcessor` that: + +* drops items if no subscribers are present, +* forwards items to the set of observing subscribers. + +[source,java] +---- +include::{examples-dir}/java/guides/operators/BroadcastProcessorTest.java[tags=code,indent=0] +---- + +Note that the `BroadcastProcessor` subscribes to the _hot_ source aggressively and without back-pressure. +However, the `BroadcastProcessor` enforces the back-pressure protocol per subscriber. +If a subscriber is not ready to handle an item emitted by the _hot_ source, an `io.smallrye.mutiny.subscription.BackPressureFailure` is forwarded to this subscriber. diff --git a/content/docs/smallrye-mutiny/2/guides/imperative-to-reactive.adoc b/content/docs/smallrye-mutiny/2/guides/imperative-to-reactive.adoc new file mode 100644 index 000000000..535317c49 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/guides/imperative-to-reactive.adoc @@ -0,0 +1,71 @@ += From imperative to reactive +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,advanced + +If you use Mutiny, there is a good chance you may want to avoid blocking the caller thread. + +In a _pure_ reactive application, the application logic is executed on one of the few I/O threads, and blocking one of these would have dramatic consequences. +So, here is the big question: _how do you deal with blocking code?_ + +Let's imagine you have blocking code (e.g., connecting to a database using JDBC, reading a file from the file system...), and you want to integrate that into your reactive pipelines while avoiding blocking. +You would need to isolate such blocking parts of your code and run these parts on worker threads. + +Mutiny provides two operators to customize the threads used to handle events: + +* `runSubscriptionOn` - to configure the thread used to execute the code happening at subscription-time +* `emitOn` - to configure the thread used to dispatch events downstream + +== Running blocking code on subscription + +It is very usual to deal with the blocking call during the subscription. +In this case, the `runSubscription` operator is what you need: + +[source,java] +---- +include::{examples-dir}/java/guides/integration/ImperativeToReactiveTest.java[tags=uni-runSubscriptionOn,indent=0] +---- + +The code above creates a Uni that will supply the item using a blocking call, here the `invokeRemoteServiceUsingBlockingIO` method. +To avoid blocking the subscriber thread, it uses `runSubscriptionOn` which switches the thread and call `invokeRemoteServiceUsingBlockingIO` on another thread. +Here we pass the default worker thread pool, but you can use your own executor. + +[TIP] +==== + +What's that default worker pool? + +In the previous snippet, you may wonder about `Infrastructure.getDefaultWorkerPool()`. +Mutiny allows the underlying platform to provide a default worker pool. +`Infrastructure.getDefaultWorkerPool()` provides access to this pool. + +==== + +If the underlying platform does not provide a pool, a default one is used. + +Note that `runSubscriptionOn` does not subscribe to the Uni. +It specifies the executor to use when a subscription happens. + +While the snippet above uses `Uni`, you can also use `runSubscriptionOn` on a `Multi`. + +== Executing blocking calls on event + +Using `runSubscriptionOn` works when the blocking operation happens at subscription time. +But, when dealing with `Multi` and need to execute blocking operations for each item, you need to use `emitOn`. + +While `runSubscriptionOn` runs the subscription on the given executor, `emitOn` configures the executor used to propagate downstream the items, failure and completion events: + +[source,java] +---- +include::{examples-dir}/java/guides/integration/ImperativeToReactiveTest.java[tags=multi-emitOn,indent=0] +---- + +`emitOn` is also available on `Uni`. + +[WARNING] +==== + +Be careful as this operator can lead to concurrency problems with non thread-safe objects such as CDI request-scoped beans. +It might also break reactive-streams semantics with items being emitted concurrently. + +==== diff --git a/content/docs/smallrye-mutiny/2/guides/integrate-a-non-reactive-source.adoc b/content/docs/smallrye-mutiny/2/guides/integrate-a-non-reactive-source.adoc new file mode 100644 index 000000000..0b0a7982c --- /dev/null +++ b/content/docs/smallrye-mutiny/2/guides/integrate-a-non-reactive-source.adoc @@ -0,0 +1,20 @@ += How can I create a Multi from a non-reactive source? +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,advanced + +The `UnicastProcessor` is an implementation of `Multi` that lets you enqueue items in a queue. + +The items are then dispatched to the subscriber using the request protocol. +While this pattern is against the idea of back-pressure, it lets you connect sources of data that do not support back-pressure with your subscriber. + +In the following example, the `UnicastProcessor` is used by a thread emitting items. +These items are enqueued in the processor and replayed when the subscriber is connected, following the request protocol. + +[source,java] +---- +include::{examples-dir}/java/guides/operators/UnicastProcessorTest.java[tags=code,indent=0] +---- + +By default, the `UnicastProcessor` uses an unbounded queue. +You can also pass a fixed size queue that would reject the items once full. diff --git a/content/docs/smallrye-mutiny/2/guides/joining-unis.adoc b/content/docs/smallrye-mutiny/2/guides/joining-unis.adoc new file mode 100644 index 000000000..eaaa1a204 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/guides/joining-unis.adoc @@ -0,0 +1,67 @@ += Joining several unis +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,intermediate + +A `Uni` represents an operation that either emits a value or a failure. +Examples of operations that fit into a `Uni` include: HTTP client requests, database `insert` queries, sending messages to a broker, etc. + +It is common to trigger several _concurrent_ operations, then _join_ on the results. +For instance you can make HTTP requests to 3 different HTTP APIs, then collect all HTTP responses. +Or you can just take the response from the one who was the fastest. + +`Uni` offers the `join` group to assemble all results from a list of `Uni`, pick the first one that terminates, or pick the first one that terminates with a value. + +== Joining multiple unis + +Given multiple `Uni`, you can join them all and obtain a `Uni` that emits a list of values: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/UniJoinTest.java[tags=join-all,indent=0] +---- + +The assembled values are in the same order as the list of unis. +The last call to `.andCollectFailures()` specifies that if one or several `Uni` fail, then the failures are assembled in a `CompositeException`. + +Sometimes you just want to _fail fast_ if any of the `Uni` fails, and not wait for all unis to terminate: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/UniJoinTest.java[tags=join-all-ff,indent=0] +---- + +When any `Uni` fails, then the failure is directly forwarded as a failure of `res`. + +== Joining on the first Uni + +In some cases you do not want to have all the results but just that of the first `Uni` to respond. +There are actually 2 different cases, depending on whether you want the result of the first `Uni` that emits a value, or just the result of the first `Uni` to terminate. + +If you want to get the first `Uni` that terminates: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/UniJoinTest.java[tags=join-first,indent=0] +---- + +If you want to have the first `Uni` that emits a value (and forget the first failures), then: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/UniJoinTest.java[tags=join-first-withitem,indent=0] +---- + +When all unis fail then `res` fails with a `CompositeException` that reports all failures. + +== Using a builder object + +There are situations where it can be more convenient to gather the unis to join in an iterative fashion. +For this purpose you can use a builder object, as in: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/UniJoinTest.java[tags=builder,indent=0] +---- + +The builder offers `joinAll()` and `joinFirst()` methods. diff --git a/content/docs/smallrye-mutiny/2/guides/kotlin.adoc b/content/docs/smallrye-mutiny/2/guides/kotlin.adoc new file mode 100644 index 000000000..796415510 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/guides/kotlin.adoc @@ -0,0 +1,135 @@ += Kotlin integration +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,intermediate + +The module `mutiny-kotlin` provides an integration with Kotlin for use with coroutines and convenient language features. + +There are extension methods available for converting between Mutiny and Kotlin (coroutine) types. +For implementation details please have also a look to these methods' documentation. + +== Dependency coordinates + +The coroutine extension functions are shipped in the package `io.smallrye.mutiny.coroutines`. + +[source,kotlin] +---- +include::{examples-dir}/kotlin/guides/Coroutines.kt[tags=importStatements,indent=0] +---- + +You need to add the following dependency to your project: + +[.tabs] +==== + +Maven:: ++ +-- + +[source,xml,subs="attributes+"] +---- + + io.smallrye.reactive + mutiny-kotlin + {versions-mutiny} + +---- + +-- + +Gradle (Kotlin):: ++ +-- + +[source,kotlin,subs="attributes+"] +---- +implementation("io.smallrye.reactive:mutiny-kotlin:{versions-mutiny}") +---- + +-- + +Gradle (Groovy):: ++ +-- + +[source,groovy,subs="attributes+"] +---- +implementation "io.smallrye.reactive:mutiny-kotlin:{versions-mutiny}" +---- + +-- + +==== + +== Awaiting a Uni in coroutines + +Within a coroutine or suspend function you can easily await Uni events in a suspended way: + +[source,kotlin] +---- +include::{examples-dir}/kotlin/guides/Coroutines.kt[tags=uniAwaitSuspending,indent=0] +---- + +== Processing a Multi as Flow + +The coroutine `Flow` type matches `Multi` semantically, even though it isn't a feature complete reactive streams implementation. +You can process a `Multi` as `Flow` as follows: + +[source,kotlin] +---- +include::{examples-dir}/kotlin/guides/Coroutines.kt[tags=multiAsFlow,indent=0] +---- + +[NOTE] +==== + +There's no flow control availabe for Kotlin's `Flow`. Published items are buffered for consumption using a coroutine `Channel`. +The buffer size and overflow strategy of that `Channel` can be configured using optional arguments: +`Multi.asFlow(bufferCapacity = Channel.UNLIMITED, bufferOverflowStrategy = BufferOverflow.SUSPEND)`, +for more details please consult the method documentation. + +==== + +== Providing a Deferred value as Uni + +The other way around is also possible, let a Deferred become a Uni: + +[source,kotlin] +---- +include::{examples-dir}/kotlin/guides/Coroutines.kt[tags=deferredAsUni,indent=0] +---- + +== Creating a Multi from a Flow + +Finally, creating a Multi from a Flow is also possible: + +[source,kotlin] +---- +include::{examples-dir}/kotlin/guides/Coroutines.kt[tags=flowAsMulti,indent=0] +---- + +== Language convenience + +=== Unit instead of Void (null) value + +Kotlin has a special value type `Unit` similar to Java's `Void`. +While regular `Uni++<++Void++>++` holds a `null` item, you can get a `Unit` by using the extension function `replaceWithUnit()`: + +[source,kotlin] +---- +include::{examples-dir}/kotlin/guides/UniExt.kt[tags=uniReplaceWithUnit,indent=0] +---- + +=== Uni builder + +Building a `Uni` from Kotlin code can easily be achieved using the following builders available as regular or coroutine variant: + +[source,kotlin] +---- +include::{examples-dir}/kotlin/guides/UniExt.kt[tags=uniBuilder,indent=0] +---- + +[source,kotlin] +---- +include::{examples-dir}/kotlin/guides/Coroutines.kt[tags=uniBuilder,indent=0] +---- diff --git a/content/docs/smallrye-mutiny/2/guides/logging.adoc b/content/docs/smallrye-mutiny/2/guides/logging.adoc new file mode 100644 index 000000000..4135995b1 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/guides/logging.adoc @@ -0,0 +1,66 @@ += Logging events +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,beginner + +Both `Uni` and `Multi` offer a `log` operator that can be used to trace events as they flow through operators. + +Mutiny does not make any assumption on _how_ logging is defined, and does not rely on any specific logging API. + +== Using a logging operator + +The `log` method comes in 2 forms: one that takes an identifier and one that derives the identifier from the upstream class: + +[source,java] +---- +include::{examples-dir}/java/guides/infrastructure/OperatorLoggingTest.java[tags=log,indent=0] +---- + +Here the `log` operator traces all events between the `onItem().transform(...)` operator and the subscriber, as in the following output: + +.... +11:01:48.709 [main] INFO Multi.MultiMapOp.0 - onSubscription() +11:01:48.711 [main] INFO Multi.MultiMapOp.0 - request(9223372036854775807) +11:01:48.711 [main] INFO Multi.MultiMapOp.0 - onItem(10) +>>> 10 +11:01:48.711 [main] INFO Multi.MultiMapOp.0 - onItem(20) +>>> 20 +11:01:48.711 [main] INFO Multi.MultiMapOp.0 - onItem(30) +>>> 30 +11:01:48.711 [main] INFO Multi.MultiMapOp.0 - onCompletion() +.... + +There are a few things to note here: + +[arabic] +. we are logging on a `Multi`, so the logging event is prefixed with `Multi` (and `Uni` in the case of a... `Uni`), and +. since we did not specify any identifier in the `log` method call, `MultiMapOp` has been derived from the preceding operator (non-qualified) class name, and +. since there can be multiple subscriptions an integer is appended to the identifier (`0`, `1`, `2`, ...). + +== Defining logging + +What happens when events are being logged is defined with the `Infrastructure` class. +Events are written by default to the standard console output in a format similar to: + +.... +[--> Multi.MultiMapOp.0 | onSubscription() +[--> Multi.MultiMapOp.0 | request(9223372036854775807) +[--> Multi.MultiMapOp.0 | onItem(10) +[--> Multi.MultiMapOp.0 | onItem(20) +[--> Multi.MultiMapOp.0 | onItem(30) +[--> Multi.MultiMapOp.0 | onCompletion() +.... + +The following is an example of configuring logging with http://www.slf4j.org++[++SLF4J++]++: + +[source,java] +---- +include::{examples-dir}/java/guides/infrastructure/OperatorLoggingTest.java[tags=set-logger,indent=0] +---- + +[TIP] +==== + +Note that this is only useful to do when embedding Mutiny in your own stack, some frameworks like https://quarkus.io[Quarkus] will already have defined the correct logging strategy. + +==== diff --git a/content/docs/smallrye-mutiny/2/guides/merging-and-concatenating-streams.adoc b/content/docs/smallrye-mutiny/2/guides/merging-and-concatenating-streams.adoc new file mode 100644 index 000000000..43d1277b3 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/guides/merging-and-concatenating-streams.adoc @@ -0,0 +1,87 @@ += Merging and Concatenating Streams +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,intermediate + +Merging or concatenating streams is a frequent operation which consists in taking multiple streams and creating a new `Multi` out of them. +Such an operation observes the items emitted by the different streams and produces a new `Multi` emitting the events. + +All the streams merged or concatenated this way should emit the same type of items. + +== The difference between merge and concatenate + +Understanding the difference between _merge_ and _concatenate_ is essential. + +When _merging_ streams, it observes the different upstreams and emits the items as they come. +If the streams emit their items concurrently, the items from the different streams are interleaved. + +image::images/diagrams/mermaid-74e1ac453bf9.svg[mermaid diagram] + +When using _merge_, failures are also propagated to the merged stream, and no more items are emitted after that failure. +The _completion_ event is only emitted by the merged stream when all the observed streams are completed. + +But if we want to keep the order of the observed stream, we need to _concatenate_. + +When _concatenating_, it waits for the first stream to complete before subscribing to the second one. Thus, it ensures that all the items from the first stream have been emitted before emitting the second stream items. It preserves an order corresponding to the source: + +image::images/diagrams/mermaid-c6f9029821c4.svg[mermaid diagram] + +When the first stream emits the completion event, it switches to the second stream, and so on. +When the last stream completes, the concatenated stream sends the completion event. +As for _merge_, if a stream fails then there won't be further events. + +== Merging Multis + +To create a new `Multi` from the _merge_ of multiple `Multi` streams use: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/MergeConcatTest.java[tags=merge,indent=0] +---- + +For example, we can merge multiple streams emitting periodical events: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/MergeConcatTest.java[tags=merge-ticks,indent=0] +---- + +and the output would be similar to: + +[source,text] +---- +Got item: Stream 1 - 0 +Got item: Stream 2 - 0 +Got item: Stream 3 - 0 +Got item: Stream 3 - 1 +Got item: Stream 1 - 1 +Got item: Stream 3 - 2 +Got item: Stream 2 - 1 +Got item: Stream 3 - 3 +Got item: Stream 1 - 2 +Got item: Stream 3 - 4 +Got item: Stream 3 - 5 +---- + +== Concatenating Multis + +To create a new `Multi` from the _concatenation_ of multiple `Multi` streams use: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/MergeConcatTest.java[tags=concat,indent=0] +---- + +Don't forget that the streams order matters in this case, as `(streamA, streamB)` does not provide the same result as `(streamB, streamA)`: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/MergeConcatTest.java[tags=concatenate-strings,indent=0] +---- + +[IMPORTANT] +==== + +If one of the concatenated streams is unbounded (infinite), the next streams in the list won't be consumed! + +==== diff --git a/content/docs/smallrye-mutiny/2/guides/multi-split.adoc b/content/docs/smallrye-mutiny/2/guides/multi-split.adoc new file mode 100644 index 000000000..d8427562a --- /dev/null +++ b/content/docs/smallrye-mutiny/2/guides/multi-split.adoc @@ -0,0 +1,51 @@ += Splitting a Multi into several Multi +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,intermediate + +It is possible to split a `Multi` into several `Multi` streams. + +== Using the split operator + +Suppose that we have a stream of strings that represent _signals_, and that we want a `Multi` for each kind of signal: + +* `?foo`, `?bar` are _input_ signals, +* `!foo`, `!bar` are _output_ signals, +* `foo`, `bar` are _other_ signals. + +To do that, we need a function that maps each item of the stream to its target stream. +The splitter API needs a Java enumeration to define keys, as in: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/SplitTest.java[tags=enum,indent=0] +---- + +Now we can use the `split` operator that provides a splitter object, and fetch individual `Multi` for each split stream using the `get` method: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/SplitTest.java[tags=splits,indent=0] +---- + +This prints the following console output: + +.... +output - a +input - b +output - c +output - d +other - 123 +input - e +.... + +== Notes on using splits + +* Items flow when all splits have a subscriber. +* The flow stops when either of the subscribers cancels, or when any subscriber has a no outstanding demand. +* The flow resumes when all splits have a subscriber again, and when all subscribers have outstanding demand. +* Only one subscriber can be active for a given split. Other subscription attempts will receive an error. +* When a subscriber cancels, then a new subscription attempt on its corresponding split can succeed. +* Subscribing to an already completed or errored split results in receiving the terminal signal (`onComplete()` or `onFailure(err)`). +* The upstream `Multi` gets subscribed to when the first split subscription happens, no matter which split it is. +* The first split subscription passes its context, if any, to the upstream `Multi`. It is expected that all split subscribers share the same context object, or the behavior of your code will most likely be incorrect. diff --git a/content/docs/smallrye-mutiny/2/guides/pagination.adoc b/content/docs/smallrye-mutiny/2/guides/pagination.adoc new file mode 100644 index 000000000..006afadb3 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/guides/pagination.adoc @@ -0,0 +1,45 @@ += How to use paginated APIs? +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,intermediate + +There are many REST / HTTP APIs using pagination, _i.e._ return only a subset of the results and you need to request the next _page_ to get the next batch. +Each batch contains a list of item(s). + +To use this kind of API and generate a continuous stream of items, you need to use the `Multi.createBy().repeating()` function. +However, we need to pass a cursor / state to advance and avoid requesting again and again the same page. +Fortunately, `repeating` provides methods to pass a shared state. +So by combining these methods and `disjoint` you can generate streams from these pages: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/PaginationTest.java[tags=code,indent=0] +---- + +First, you create a `Multi` containing the items emitted by the `CompletionStage` supplier and pass a state supplier to progress among the pages. + +Then, use `until` to call the paginated API until we have all the items. +At the point we have a stream of list of item such as `++[++"a", "b", "c"++]++, ++[++"d", "e"++]++, ++[]++`. +However, we want the following stream: `"a", "b", "c", "d", "e"`. +The `disjoint` method does exactly this. +It gets the items from the lists and passes them downstream: + +image::images/diagrams/mermaid-df98fc1dd069.svg[mermaid diagram] + +`Multi.createBy().repeating()` lets you choose the number of iterations using: + +* `atMost` - exact number of repetitions (or failure happens before reaching that number) +* `until` - the repetition is stopped if the item emitted by the `Uni` *passes* a test (predicate). +It does not propagate the item that did pass the check, and it stops the repetition. +The check verifies if the current item does not contain valid data. +* `whilst` - the repetition is stopped if the item emitted by the `Uni` *does not pass* a test (predicate). +It does propagate the item downstream even if the check does not pass. +However, it stops the repetition. +The test verifies if there is a _next_ batch to be retrieved. + +The following code illustrates the usage of `whilst`: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/PaginationTest.java[tags=code2,indent=0] +---- diff --git a/content/docs/smallrye-mutiny/2/guides/polling.adoc b/content/docs/smallrye-mutiny/2/guides/polling.adoc new file mode 100644 index 000000000..88aa7115e --- /dev/null +++ b/content/docs/smallrye-mutiny/2/guides/polling.adoc @@ -0,0 +1,21 @@ += How to use polling? +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,advanced + +There are many poll-based API around us. +Sometimes you need to use these APIs to generate a stream from the polled values. + +To do this, use the `repeat()` feature: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/PollableSourceTest.java[tags=code,indent=0] +---- + +You can also stop the repetition using the `repeat().until()` method which will continue the repetition until the given predicate returns `true`, and/or directly create a `Multi` using `Multi.createBy().repeating()`: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/PollableSourceTest.java[tags=code2,indent=0] +---- diff --git a/content/docs/smallrye-mutiny/2/guides/reactive-to-imperative.adoc b/content/docs/smallrye-mutiny/2/guides/reactive-to-imperative.adoc new file mode 100644 index 000000000..b1ede52c1 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/guides/reactive-to-imperative.adoc @@ -0,0 +1,54 @@ += From reactive to imperative +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,advanced + +There are use cases where you need the items in an imperative manner instead of asynchronous. +Typically, when you serve an HTTP request from a worker thread, you can block. + +Mutiny provides the ability to block until you get the items. + +== Awaiting on Uni's item + +When dealing with a `Uni,` you can block and await the item using: + +[source,java] +---- +include::{examples-dir}/java/guides/integration/ReactiveToImperativeTest.java[tags=await,indent=0] +---- + +This method blocks the caller thread until the observed `uni` emits the item. +Note that the returned item can be `null` if the `uni` emits `null.` +If the `uni` fails, it throws the exception, wrapped in the `CompletionException` for _checked_ exception. + +Blocking forever may not be a great idea. +You can use `uni.await().atMost(Duration)` to pass a deadline. +When the deadline is reached, a `TimeoutException` is thrown: + +[source,java] +---- +include::{examples-dir}/java/guides/integration/ReactiveToImperativeTest.java[tags=atMost,indent=0] +---- + +== Iterating over Multi's items + +When dealing with a `Multi,` you may want to iterate over the items using a simple "foreach." +You can achieve this using `multi.subscribe().asIterable()`: + +[source,java] +---- +include::{examples-dir}/java/guides/integration/ReactiveToImperativeTest.java[tags=iterable,indent=0] +---- + +The returned `iterable` is blocking. +It waits for the next items, and during that time, blocks the caller thread. + +The iteration ends once the last item is consumed. +If the `multi` emits a failure, an exception is thrown. + +Similar to `asIterable()`, the `asStream` method lets you retrieve a `java.util.stream.Stream`: + +[source,java] +---- +include::{examples-dir}/java/guides/integration/ReactiveToImperativeTest.java[tags=stream,indent=0] +---- diff --git a/content/docs/smallrye-mutiny/2/guides/replaying-multis.adoc b/content/docs/smallrye-mutiny/2/guides/replaying-multis.adoc new file mode 100644 index 000000000..d2cc64c0e --- /dev/null +++ b/content/docs/smallrye-mutiny/2/guides/replaying-multis.adoc @@ -0,0 +1,94 @@ += Replaying Multis +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,advanced + +A `Multi` is a _cold-source_: no processing happens until you subscribe. + +While the `broadcast` operator can be used so that multiple subscribers consume a `Multi` events _at the same time_, it does not support replaying items for _late subscribers_: when a subscriber joins after the `Multi` has completed (or failed), then it won't receive any item. + +This is where _replaying_ can be useful. + +== Replaying all events + +Replaying all events from an upstream `Multi` works as follows: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/ReplayTest.java[tags=replay-all,indent=0] +---- + +Both `item++_++1` and `item++_++2` trigger new subscriptions, and both lists contain the following elements: + +.... +[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] +.... + +Replaying works by turning `upstream` into a _hot-stream_, meaning that it gets requested `Long.MAX++_++VALUE` elements. +This is done when the first subscription happens. + +The replay operator stores the items in an internal _replay log_, and then each subscriber gets to replay them. + +[IMPORTANT] +==== + +Subscribers demand and cancellation requests are honored while replaying, but `upstream` cannot be cancelled. + +Be careful with unbounded streams as you can exhaust memory! + +In such cases or when you need to replay large amounts of data, you might opt to use some eventing middleware rather than Mutiny replays. + +==== + +== Replaying the last 'n' events + +You can limit the number of elements to replay by using the `upTo` method: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/ReplayTest.java[tags=replay-last,indent=0] +---- + +Each new subscriber gets to replay the last `n` elements from where the replay log is at subscription time. +For instance the first subscriber can observe all events, while a subscriber that joins 2 seconds later might not observe the earlier events. + +Since `Multi.createFrom().range(0, 10)` is an _immediate_ stream, both `item++_++1` and `item++_++2` lists contain the last items: + +.... +[7, 8, 9] +.... + +== Prepending with seed data + +In some cases you might want to prepend some _seed_ data that will be available for replay before the upstream starts emitting. + +You can do so using an `Iterable` to provide such seed data: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/ReplayTest.java[tags=replay-seed,indent=0] +---- + +In which case subscribers can observe the following events: + +.... +[-10, -5, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9] +.... + +== Replay of failures and completions + +Subscribers get to observe not just items but also the failure and completion events: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/ReplayTest.java[tags=replay-errors,indent=0] +---- + +Running this code yields the following output for any subscriber: + +.... +-> 7 +-> 8 +-> 9 +Failed: boom +.... diff --git a/content/docs/smallrye-mutiny/2/guides/rx.adoc b/content/docs/smallrye-mutiny/2/guides/rx.adoc new file mode 100644 index 000000000..b41ec4f0f --- /dev/null +++ b/content/docs/smallrye-mutiny/2/guides/rx.adoc @@ -0,0 +1,31 @@ += Using map, flatMap and concatMap +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,advanced + +If you are a seasoned reactive developer, you may miss the `map`, `flatMap`, `concatMap` methods. + +The Mutiny API is quite different from the _standard_ reactive eXtensions API. + +There are multiple reasons for this choice. +Typically, _flatMap_ is not necessarily well understood by every developer, leading to potentially catastrophic consequences. + +That being said, Mutiny provides the _map_, _flatMap_ and _concatMap_ methods, implementing the most common variant for each: + +[source,java] +---- +include::{examples-dir}/java/guides/RxTest.java[tags=rx,indent=0] +---- + +The Mutiny equivalents are: + +* `map -++>++ onItem().transform()` +* `flatMap -++>++ onItem().transformToUniAndMerge` and `onItem().transformToMultiAndMerge` +* `concatMap -++>++ onItem().transformToUniAndConcatenate` and `onItem().transformToMultiAndConcatenate` + +The following snippet demonstrates how to uses these methods: + +[source,java] +---- +include::{examples-dir}/java/guides/RxTest.java[tags=mutiny,indent=0] +---- diff --git a/content/docs/smallrye-mutiny/2/guides/shortcut-methods.adoc b/content/docs/smallrye-mutiny/2/guides/shortcut-methods.adoc new file mode 100644 index 000000000..219dcdcb1 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/guides/shortcut-methods.adoc @@ -0,0 +1,44 @@ += Shortcut methods +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,beginner + +The Mutiny API is decomposed around the idea of groups, each group handling a specific event. +However, to avoid verbosity, Mutiny also exposes _shortcuts_ for the most used methods. +Be aware that these shorts, while making the code shorter, may harm the readability and understandability. + +To _peek_ at items, you can use the `invoke` method: + +[source,java] +---- +include::{examples-dir}/java/guides/ShortcutsTest.java[tags=invoke,indent=0] +---- + +`invoke` is a shortcut for `onItem().invoke(...)`. + +Mutiny also provides the `call` method for executing an action returning a `Uni`. +This is useful to execute an asynchronous action without modifying incoming item: + +[source,java] +---- +include::{examples-dir}/java/guides/ShortcutsTest.java[tags=call,indent=0] +---- + +`call` is a shortcut for `onItem().call(...)`. + +The following table lists the available shortcuts available by the `Uni` class: + +[width="100%",cols="41%,59%",options="header",] +|=== +|Shortcut |Equivalent +|`uni.map(x -++>++ y)` |`uni.onItem().transform(x -++>++ y)` +|`uni.flatMap(x -++>++ uni2)` |`uni.onItem().transformToUni(x -++>++ uni2)` +|`uni.chain(x -++>++ uni2)` |`uni.onItem().transformToUni(x -++>++ uni2)` +|`uni.invoke(x -++>++ System.out.println(x))` |`uni.onItem().invoke(x -++>++ System.out.println(x))` +|`uni.call(x -++>++ uni2)` |`uni.onItem().call(x -++>++ uni2)` +|`uni.eventually(() -++>++ System.out.println("eventually"))` |`uni.onTermination().invoke(() -++>++ System.out.println("eventually"))` +|`uni.eventually(() -++>++ uni2)` |`uni.onTermination().call((ignoredItem, ignoredError, ignoredCancellation) -++>++ uni2)` +|`uni.replaceWith(x)` |`uni.onItem().transform(ignored -++>++ x)` +|`uni.replaceWith(uni2)` |`uni.onItem().transformToUni(ignored -++>++ uni2)` +|`uni.replaceIfNullWith(x)` |`uni.onItem().ifNull().continueWith(x)` +|=== diff --git a/content/docs/smallrye-mutiny/2/guides/spies.adoc b/content/docs/smallrye-mutiny/2/guides/spies.adoc new file mode 100644 index 000000000..2a87e08e8 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/guides/spies.adoc @@ -0,0 +1,71 @@ += Spying on events +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,advanced + +Spies are useful when you need to track which _events_ flow into a `Uni` or a `Multi`. +Spies can track events from groups such as `onItem()`, `onFailure()`, `onSubscribe()`, etc. + +The `io.smallrye.mutiny.helpers.spies.Spy` interface offers factory methods to spy on selected groups, or even on all groups. + +== Spying selected groups + +The following example spies on requests and completion group events: + +[source,java] +---- +include::{examples-dir}/java/guides/SpiesTest.java[tags=selected,indent=0] +---- + +The standard output stream shall display the following text: + +.... +1 +2 +3 +Number of requests: 9223372036854775807 +Completed? true +.... + +The number of requests corresponds to `Long.MAX++_++VALUE`, and a completion event was sent. + +[IMPORTANT] +==== + +It is important to note that spies observe and report events for all subscribers, not just one in particular. + +You should call the `.reset()` method on a given spy to resets its statistics such as the invocation count. + +==== + +== Spying all groups + +You can take advantage of a _global spy_ if you are interested in all event groups: + +[source,java] +---- +include::{examples-dir}/java/guides/SpiesTest.java[tags=global,indent=0] +---- + +Running the snippet above gives the following output: + +.... +1 +2 +3 +Number of requests: 9223372036854775807 +Cancelled? false +Failure? null +Items: [1, 2, 3] +.... + +[WARNING] +==== + +Tracking `onItem()` events on a `Multi` requires storing all items into a list, which can yield an out-of-memory +exception with large streams. + +In such cases consider using `Spy.onItem(multi, false)` to obtain a spy that does not store items, but that can +still report data such as the number of received events (see `spy.invocationCount()`). + +==== diff --git a/content/docs/smallrye-mutiny/2/guides/take-skip-items.adoc b/content/docs/smallrye-mutiny/2/guides/take-skip-items.adoc new file mode 100644 index 000000000..f466d1062 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/guides/take-skip-items.adoc @@ -0,0 +1,116 @@ += Take/Skip the first or last items +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,beginner + +Multi provides the ability to: + +* only forward items from the beginning of the observed multi, +* only forward the last items (and discard all the other ones), +* skip items from the beginning of the multi, +* skip the last items. + +These actions are available from the `multi.select()` and `multi.skip()` groups, allowing to, respectively, select and skip +items from upstream. + +== Selecting items + +The `multi.select().first` method forwards on the _n_ *first* items from the multi. +It forwards that amount of items and then sends the completion signal. +It also cancels the upstream subscription. + +[source,java] +---- +include::{examples-dir}/java/guides/operators/SelectAndSkipTest.java[tags=take-first,indent=0] +---- + +[NOTE] +==== + +The `select().first()` method selects only the first item. + +==== + +If the observed multi emits fewer items, it sends the completion event when the upstream completes. + +Similarly, The `multi.select().last` operator forwards on the _n_ *last* items from the multi. +It discards all the items emitted beforehand. + +[source,java] +---- +include::{examples-dir}/java/guides/operators/SelectAndSkipTest.java[tags=take-last,indent=0] +---- + +[NOTE] +==== + +The `select().last()` method selects only the last item. + +==== + +The `multi.select().first(Predicate)` operator forwards the items while the passed predicate returns `true`: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/SelectAndSkipTest.java[tags=take-while,indent=0] +---- + +It calls the predicate for each item. +Once the predicate returns `false`, it stops forwarding the items downstream. +It also sends the completion event and cancels the upstream subscription. + +Finally, `multi.select().first(Duration)` operator picks the first items emitted during a given period. +Once the passed duration expires, it sends the completion event and cancels the upstream subscription. +If the observed multi completes before the passed duration, it sends the completion event. + +[source,java] +---- +include::{examples-dir}/java/guides/operators/SelectAndSkipTest.java[tags=take-for,indent=0] +---- + +== Skipping items + +You can also skip items using `multi.skip()`. + +The `multi.skip().first(n)` method skips the _n_ *first* items from the multi. +It forwards all the remaining items and sends the completion event when the upstream multi completes. + +[source,java] +---- +include::{examples-dir}/java/guides/operators/SelectAndSkipTest.java[tags=skip-first,indent=0] +---- + +If the observed multi emits fewer items, it sends the completion event without emitting any items. + +[NOTE] +==== + +`skip().last()` drops only the very last item. + +==== + +Similarly, The `multi.skip().last(n)` operator skips on the _n_ *last* items from the multi: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/SelectAndSkipTest.java[tags=skip-last,indent=0] +---- + +The `multi.skip().first(Predicate)` operator skips the items while the passed predicate returns `true`: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/SelectAndSkipTest.java[tags=skip-while,indent=0] +---- + +It calls the predicate for each item. +Once the predicate returns `false`, it stops discarding the items and starts forwarding downstream. + +Finally, `multi.skip().first(Duration)` operator skips the first items for a given period. +Once the passed duration expires, it sends the items emitted after the deadline downstream. +If the observed multi completes before the passed duration, it sends the completion event. + +[source,java] +---- +include::{examples-dir}/java/guides/operators/SelectAndSkipTest.java[tags=skip-for,indent=0] +---- diff --git a/content/docs/smallrye-mutiny/2/guides/testing.adoc b/content/docs/smallrye-mutiny/2/guides/testing.adoc new file mode 100644 index 000000000..34299c43d --- /dev/null +++ b/content/docs/smallrye-mutiny/2/guides/testing.adoc @@ -0,0 +1,28 @@ += How can I write unit / integration tests? +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,beginner + +Mutiny provides subscribers for `Uni` and `Multi` offering helpful assertion methods. +You can use them to test pipelines. + +Here is an example to test a `Uni`: + +[source,java] +---- +include::{examples-dir}/java/guides/TestSubscribersTest.java[tags=uni,indent=0] +---- + +Testing a `Multi` pipeline is similar: + +[source,java] +---- +include::{examples-dir}/java/guides/TestSubscribersTest.java[tags=multi,indent=0] +---- + +The assertions do not just focus on _good_ outcomes, you can also test failures as in: + +[source,java] +---- +include::{examples-dir}/java/guides/TestSubscribersTest.java[tags=failing,indent=0] +---- diff --git a/content/docs/smallrye-mutiny/2/guides/unchecked-exceptions.adoc b/content/docs/smallrye-mutiny/2/guides/unchecked-exceptions.adoc new file mode 100644 index 000000000..8b0f4206d --- /dev/null +++ b/content/docs/smallrye-mutiny/2/guides/unchecked-exceptions.adoc @@ -0,0 +1,42 @@ += Dealing with checked exceptions +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,intermediate + +When implementing your reactive pipeline, you write lots of functions (`java.util.function.Function`), consumers (`java.util.function.Consumer`), suppliers (`java.util.function.Supplier`) and so on. + +By default, you cannot throw checked exceptions. + +When integrating libraries throwing checked exceptions (like `IOException`) it's not very convenient to add a `try/catch` block and wrap the thrown exception into a runtime exception: + +[source,java] +---- +include::{examples-dir}/java/guides/UncheckedTest.java[tags=rethrow,indent=0] +---- + +Mutiny provides utilities to avoid having to do this manually. + +If your operation throws a _checked exception_, you can use the https://javadoc.io/doc/io.smallrye.reactive/mutiny/latest/io/smallrye/mutiny/unchecked/Unchecked.html[`io.smallrye.mutiny.unchecked.Unchecked`] wrappers. + +For example, if your synchronous transformation uses a method throwing a checked exception, wrap it using `Unchecked.function`: + +[source,java] +---- +include::{examples-dir}/java/guides/UncheckedTest.java[tags=transform,indent=0] +---- + +You can also wrap consumers such as in: + +[source,java] +---- +include::{examples-dir}/java/guides/UncheckedTest.java[tags=invoke,indent=0] +---- + +[TIP] +==== + +You can add the following import statement to simplify the usage of the provided methods: + +`import static io.smallrye.mutiny.unchecked.Unchecked.++*++;` + +==== diff --git a/content/docs/smallrye-mutiny/2/images/diagrams/mermaid-0277dcc2b534.svg b/content/docs/smallrye-mutiny/2/images/diagrams/mermaid-0277dcc2b534.svg new file mode 100644 index 000000000..d390d8d76 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/images/diagrams/mermaid-0277dcc2b534.svg @@ -0,0 +1 @@ +SubscriberProcessorPublisherSubscriberProcessorPublishersubscribesubscribesubscriptionsubscriptionrequest(5)request(5)onItem("a")onItem("A")onItem("b")onItem("B")cancel()cancel() \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/2/images/diagrams/mermaid-18f9a50c0627.svg b/content/docs/smallrye-mutiny/2/images/diagrams/mermaid-18f9a50c0627.svg new file mode 100644 index 000000000..684d7dc80 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/images/diagrams/mermaid-18f9a50c0627.svg @@ -0,0 +1 @@ +SubscriberemitOn(executor)MultiSubscriberemitOn(executor)MultiOn caller threadOn executor threadonItem(1)1onItem(1)2 \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/2/images/diagrams/mermaid-3b8d4c58438a.svg b/content/docs/smallrye-mutiny/2/images/diagrams/mermaid-3b8d4c58438a.svg new file mode 100644 index 000000000..bfccc4178 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/images/diagrams/mermaid-3b8d4c58438a.svg @@ -0,0 +1 @@ +SubscriberMulti(item)TransformerUniSubscriberMulti(item)TransformerUnionItem(item)1subscribe(...)2onSubscribe(sub)3onItem(item)4onItem(item)5onItem(item)6onItem(item)7 \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/2/images/diagrams/mermaid-3d4459e3c6a6.svg b/content/docs/smallrye-mutiny/2/images/diagrams/mermaid-3d4459e3c6a6.svg new file mode 100644 index 000000000..2e8d7dc42 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/images/diagrams/mermaid-3d4459e3c6a6.svg @@ -0,0 +1 @@ +SubscriberCombined streamStream BStream ASubscriberCombined streamStream BStream AonItem(1)1onItem(2)2onItem(a)3onItem([1,a])4onItem(3)5onItem(b)6onItem([2,b])7onItem(c)8 \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/2/images/diagrams/mermaid-491ff27a51a5.svg b/content/docs/smallrye-mutiny/2/images/diagrams/mermaid-491ff27a51a5.svg new file mode 100644 index 000000000..767fb50f9 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/images/diagrams/mermaid-491ff27a51a5.svg @@ -0,0 +1 @@ +ObserverStreamObserverStreamObserverStreamObserverStreamonItem("a")onItem("a")onItem("b")onItem("b")onItem("c")onFailure(err)onCompletion() \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/2/images/diagrams/mermaid-69aa56febc93.svg b/content/docs/smallrye-mutiny/2/images/diagrams/mermaid-69aa56febc93.svg new file mode 100644 index 000000000..47b820123 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/images/diagrams/mermaid-69aa56febc93.svg @@ -0,0 +1 @@ +SubscriberCollect operatorMultiSubscriberCollect operatorMultionItem(1)1onItem(2)2onItem(3)3onItem([1, 2, 3])4 \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/2/images/diagrams/mermaid-74e1ac453bf9.svg b/content/docs/smallrye-mutiny/2/images/diagrams/mermaid-74e1ac453bf9.svg new file mode 100644 index 000000000..d0afbd3b0 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/images/diagrams/mermaid-74e1ac453bf9.svg @@ -0,0 +1 @@ +Merged streamStream BStream AMerged streamStream BStream Asubscribe1subscribe2onSubscribe(s)3onItem(1)4onSubscribe(s)5onItem(2)6onItem(a)7onItem(3)8onItem(b)9onItem(c)10 \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/2/images/diagrams/mermaid-76df06b35153.svg b/content/docs/smallrye-mutiny/2/images/diagrams/mermaid-76df06b35153.svg new file mode 100644 index 000000000..137defedd --- /dev/null +++ b/content/docs/smallrye-mutiny/2/images/diagrams/mermaid-76df06b35153.svg @@ -0,0 +1 @@ +SubscriberrunSubscriptionOn(executor)MultiSubscriberrunSubscriptionOn(executor)Multion Caller threadOn executor threadsubscribe1subscribe2 \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/2/images/diagrams/mermaid-bc092005fd3f.svg b/content/docs/smallrye-mutiny/2/images/diagrams/mermaid-bc092005fd3f.svg new file mode 100644 index 000000000..2f934d514 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/images/diagrams/mermaid-bc092005fd3f.svg @@ -0,0 +1 @@ +DownstreamReturned UnisonItem().call(...)MultiDownstreamReturned UnisonItem().call(...)Multiitem11item12result13result14item25item26result27result28item39item310result311result312 \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/2/images/diagrams/mermaid-c54f60b30f42.svg b/content/docs/smallrye-mutiny/2/images/diagrams/mermaid-c54f60b30f42.svg new file mode 100644 index 000000000..07645de13 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/images/diagrams/mermaid-c54f60b30f42.svg @@ -0,0 +1 @@ +SubscriberCombined streamStream BStream ASubscriberCombined streamStream BStream AonItem(1)1onItem(a)2onItem([1,a])3onItem(2)4onItem(b)5onItem([2,b])6 \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/2/images/diagrams/mermaid-c6f9029821c4.svg b/content/docs/smallrye-mutiny/2/images/diagrams/mermaid-c6f9029821c4.svg new file mode 100644 index 000000000..64a7d800c --- /dev/null +++ b/content/docs/smallrye-mutiny/2/images/diagrams/mermaid-c6f9029821c4.svg @@ -0,0 +1 @@ +Concatenated streamStream BStream AConcatenated streamStream BStream Asubscribe1onSubscribe(s)2onItem(1)3onItem(2)4onItem(3)5onCompletion()6subscribe7onSubscribe(s)8onItem(a)9onItem(b)10onItem(c)11 \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/2/images/diagrams/mermaid-cbd13fb6d19b.svg b/content/docs/smallrye-mutiny/2/images/diagrams/mermaid-cbd13fb6d19b.svg new file mode 100644 index 000000000..4d0a969d8 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/images/diagrams/mermaid-cbd13fb6d19b.svg @@ -0,0 +1 @@ +

a, b, c

a, b, c

a, b, c

a, b, c

a, b, c

a, b, c

Events source

Events source

Events source

Publisher

Publisher

Publisher

Subscriber 1

Subscriber 2

Subscriber 3

\ No newline at end of file diff --git a/content/docs/smallrye-mutiny/2/images/diagrams/mermaid-df98fc1dd069.svg b/content/docs/smallrye-mutiny/2/images/diagrams/mermaid-df98fc1dd069.svg new file mode 100644 index 000000000..876990fe8 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/images/diagrams/mermaid-df98fc1dd069.svg @@ -0,0 +1 @@ +SubscriberDisjoint operatorMultiSubscriberDisjoint operatorMultionItem([a,b,c])1onItem(a)2onItem(b)3onItem(c)4onItem([d,e])5onItem(d)6onItem(e)7 \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/2/images/diagrams/mermaid-e6dd95e32a3f.svg b/content/docs/smallrye-mutiny/2/images/diagrams/mermaid-e6dd95e32a3f.svg new file mode 100644 index 000000000..deda34e8f --- /dev/null +++ b/content/docs/smallrye-mutiny/2/images/diagrams/mermaid-e6dd95e32a3f.svg @@ -0,0 +1 @@ +SubscriberTransformerMultiSubscriberTransformerMultionItem(a1)1onItem(a2)2onItem(b1)3onItem(b2)4onItem(c1)5onItem(c2)6 \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/2/images/diagrams/mermaid-e88e1918cd26.svg b/content/docs/smallrye-mutiny/2/images/diagrams/mermaid-e88e1918cd26.svg new file mode 100644 index 000000000..8f33f491e --- /dev/null +++ b/content/docs/smallrye-mutiny/2/images/diagrams/mermaid-e88e1918cd26.svg @@ -0,0 +1 @@ +DownstreamonItem().invoke(...)MultiDownstreamonItem().invoke(...)Multicallback executionitem11item12item23item24item35item36 \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/2/images/diagrams/mermaid-f0001b5cde9f.svg b/content/docs/smallrye-mutiny/2/images/diagrams/mermaid-f0001b5cde9f.svg new file mode 100644 index 000000000..5818402e1 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/images/diagrams/mermaid-f0001b5cde9f.svg @@ -0,0 +1 @@ +SubscriberCombined streamStream BStream ASubscriberCombined streamStream BStream AonItem(1)1onItem(2)2onItem(a)3onItem([2,a])4onItem(3)5onItem([3,a])6onItem(b)7onItem([3,b])8onItem(c)9onItem([3,c])10 \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/2/images/diagrams/mermaid-f67adb6018b3.svg b/content/docs/smallrye-mutiny/2/images/diagrams/mermaid-f67adb6018b3.svg new file mode 100644 index 000000000..3fbcf4900 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/images/diagrams/mermaid-f67adb6018b3.svg @@ -0,0 +1 @@ +SubscriberUni(item)TransformerUniSubscriberUni(item)TransformerUnionItem(item)1subscribe(...)2onSubscribe(sub)3onItem(result)4onItem(result)5 \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/2/images/diagrams/mermaid-f71496c9eae5.svg b/content/docs/smallrye-mutiny/2/images/diagrams/mermaid-f71496c9eae5.svg new file mode 100644 index 000000000..fc83f5b8c --- /dev/null +++ b/content/docs/smallrye-mutiny/2/images/diagrams/mermaid-f71496c9eae5.svg @@ -0,0 +1 @@ +SubscriberProcessor 2Processor 1PublisherSubscriberProcessor 2Processor 1PublisheronItemonItemonItemonItemonItemonItemonCompletiononCompletiononCompletion \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/2/images/diagrams/mermaid-f74f7917dcf7.svg b/content/docs/smallrye-mutiny/2/images/diagrams/mermaid-f74f7917dcf7.svg new file mode 100644 index 000000000..b92c0941f --- /dev/null +++ b/content/docs/smallrye-mutiny/2/images/diagrams/mermaid-f74f7917dcf7.svg @@ -0,0 +1 @@ +

a, b, c

a, b, c

a, b, c

a, b, c

Events source

Publisher

Subscriber 1

Subscriber 2

Subscriber 3

\ No newline at end of file diff --git a/content/docs/smallrye-mutiny/2/index.adoc b/content/docs/smallrye-mutiny/2/index.adoc new file mode 100644 index 000000000..74b38c67d --- /dev/null +++ b/content/docs/smallrye-mutiny/2/index.adoc @@ -0,0 +1,88 @@ += Mutiny -- Intuitive Event-Driven Reactive Programming Library for Java +include::_attributes.adoc[] + +[source,java] +---- +Uni request = makeSomeNetworkRequest(params); + +request.ifNoItem().after(ofMillis(100)) + .failWith(() -> new TooSlowException("💥")) + .onFailure(IOException.class).recoverWithItem(fail -> "📦") + .subscribe().with( + item -> log("👍 " + item), + err -> log(err.getMessage()) + ); +---- + +xref:tutorials/getting-mutiny.adoc[Get Mutiny {versions-mutiny},role=button] + +[.cards] +-- + +**** + +[discrete] +== Event-Driven + +Mutiny places _events_ at the core of its design. + +With Mutiny, you observe _events_, _react_ to them, and create elegant and readable processing pipelines. + +A PhD in functional programming is not required. + +**** + +**** + +[discrete] +== Navigable + +Even with smart code completion, classes with hundreds of methods are confusing. + +Mutiny provides a navigable and explicit API driving you towards the operator you need. + +**** + +**** + +[discrete] +== Non-blocking I/O + +Mutiny is the perfect companion to tame the asynchronous nature of applications with non-blocking I/O. + +Compose operations in a declarative fashion, transform data, enforce progress, recover from failures and more. + +**** + +**** + +[discrete] +== Quarkus and Vert.x native + +Mutiny is integrated in https://quarkus.io[Quarkus] where every reactive API uses Mutiny, and https://vertx.io[Eclipse Vert.x] clients are made available using Mutiny bindings. + +Mutiny is however an independent library that can ultimately be used in any Java application. + +**** + +**** + +[discrete] +== Made for an asynchronous world + +Mutiny can be used in any asynchronous application such as event-driven microservices, message-based applications, network utilities, data stream processing, and of course... reactive applications! + +**** + +**** + +[discrete] +== Reactive Converters Built-In + +Mutiny is based on the https://www.reactive-streams.org/[Reactive Streams protocol] and https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/concurrent/Flow.html[Java Flow], and so it can be integrated with any other reactive programming library. + +In addition, it proposes _converters_ to interact with other popular libraries. + +**** + +-- diff --git a/content/docs/smallrye-mutiny/2/reference/distributed_systems_are_asynchronous.png b/content/docs/smallrye-mutiny/2/reference/distributed_systems_are_asynchronous.png new file mode 100644 index 000000000..0d00a8c9e Binary files /dev/null and b/content/docs/smallrye-mutiny/2/reference/distributed_systems_are_asynchronous.png differ diff --git a/content/docs/smallrye-mutiny/2/reference/going-reactive-a-few-pitfalls.adoc b/content/docs/smallrye-mutiny/2/reference/going-reactive-a-few-pitfalls.adoc new file mode 100644 index 000000000..ac6f0e7b2 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/reference/going-reactive-a-few-pitfalls.adoc @@ -0,0 +1,74 @@ += Going reactive: a few pitfalls +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: reference,beginner + +Don't get us wrong, reactive programming is a fantastic way to write resource-efficient code! + +That being said, reactive programming has a learning curve that should not be taken lightly, and in some cases it is safer to write imperative code that you fully comprehend over reactive code that you don't fully grok. + +We have assembled a few considerations that we think new users should know before they embark into writing complex reactive business logic. + +== Mutiny doesn't auto-magically make your code asynchronous + +This is a common source of confusion for new reactive programmers. +Mutiny itself *does not perform any scheduling work*, except for the xref:guides/emit-on-vs-run-subscription-on.adoc[`emitOn` and `runSubscriptionOn` operators]. + +Consider the following code where we _join_ results from multiple asynchronous operations, materialised by the `Uni++<++String++>++`-returning `fetch` method: + +[source,java] +---- +include::{examples-dir}/java/guides/reference/PitfallsTest.java[tags=noMagicJoin,indent=0] +---- + +You might think that the `join` operator schedules the calls to `fetch` to be run concurrently, and then collects the results into a list. +This is not how it works! + +The `join` operator does subscribe to each `Uni++<++String++>++` returned by each call to `fetch`. +When it receives a value, it puts it into a list, and when all values have been received, that list is emitted. +The threads involved here are the ones that emit values in `fetch`. +If `fetch` uses async I/O underneath then you should observe true concurrency, but if `fetch` just emits a value right when the subscription happens then you will merely observe a sequential execution of each call to `fetch`, in order. + +== When to prefer `Uni++<++List++<++T++>>++` over `Multi++<++T++>++` + +The reason why `Multi` exists is to model streams over back-pressured sources. +By conforming to the https://www.reactive-streams.org/[Reactive Streams protocol], a `Multi` respects the control flow requests from its subscribers, avoiding classic problems such as a fast producer and a slow consumer that can yield to memory exhaustion problems. + +That being said, not everything is a stream. +Take the example of relational databases: *databases don't stream!* (for the most parts) + +When you do a query such as `SELECT ++*++ FROM ABC WHERE INDEX ++<++ 123`, you get result rows. +While you might wrap the results in a `Multi++<++Row++>++` as a convenience, the network protocol of the database still sends you all `Row` values and is very unlikely to support any notion of back-pressure on a SQL query result. + +This is why `Uni++<++List++<++Row++>>++` is in this case a better representation of an asynchronous operation than `Multi++<++Row++>++`, because the underlying networked service protocol does not provide you with any back-pressured stream. + +== Creating `Uni` and `Multi` from in-memory data might be suspicious + +You will find lots of occurrences of creating `Uni` and `Multi` from in-memory data in this documentation, as in: + +[source,java] +---- +include::{examples-dir}/java/guides/reference/PitfallsTest.java[tags=inMemoryData,indent=0] +---- + +This is convenient and expected when creating tests and examples, but this should be a strong warning in production. +Indeed, if we have a method such as the following: + +[source,java] +---- +include::{examples-dir}/java/guides/reference/PitfallsTest.java[tags=suspiciousPublisher,indent=0] +---- + +then it is clear that there is nothing _"reactive"_ in this code _(sadly, you can find such idioms in some well-known "reactive" client libraries, but we digress)_. + +As a rule of thumb, if your *initial* publisher does not make any I/O operation and it already has the data available in memory, then it is suspicious: + +* if it is a `Uni`, then it does not really model an asynchronous I/O operation because the data is already here, and +* if it is a `Multi` then not only there is no asynchronous I/O operation involved, but there is no need for a back-pressure protocol either (see the previous section). + +What is not suspicious however is to create, say, a `Multi` to perform a transformation operation: + +[source,java] +---- +include::{examples-dir}/java/guides/reference/PitfallsTest.java[tags=flatmap-ism,indent=0] +---- diff --git a/content/docs/smallrye-mutiny/2/reference/migrating-to-mutiny-2.adoc b/content/docs/smallrye-mutiny/2/reference/migrating-to-mutiny-2.adoc new file mode 100644 index 000000000..c036345db --- /dev/null +++ b/content/docs/smallrye-mutiny/2/reference/migrating-to-mutiny-2.adoc @@ -0,0 +1,52 @@ += Migrating to Mutiny 2 +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: reference,beginner + +The upgrade is transparent for most code bases that _use_ Mutiny in applications (e.g., Quarkus applications). + +== Highlights + +* Mutiny 2 is a major release with source and binary incompatible changes to the Mutiny `0.x` and `1.x` series. +* The main highlight of Mutiny 2 is that it is now based on top of the `java.util.concurrent.Flow` APIs instead of the legacy _Reactive Streams APIs_. +* The `Flow` APIs have been part of the JDK since Java 9, and they are the modern _Reactive Streams APIs_. +* Mutiny remains a faithful implementation of the _Reactive Streams_ specification and passes the `Flow` variant of the _Reactive Streams TCK_. +* Deprecated APIs in Mutiny `1.x` have been removed, and experimental APIs have been promoted. + +== Impact of the switch from legacy Reactive Streams APIs to JDK Flow + +* The `Flow` types are isomorphic to the legacy _Reactive Streams API_ types. +* We recommend that you migrate to `Flow` in your own code bases. +* You should encourage third-party libraries to migrate to `Flow`. +* You can always use _adapters_ to go back and forth between `Flow` and legacy _Reactive Streams_ types. + +=== General guidelines + +* If your code _only uses_ `Uni` and `Multi` (i.e., not `org.reactivestreams.Publisher`), then you will be source-compatible with Mutiny 2. You should still recompile and check that your test suites pass. +* If you expose `Multi` as a `org.reactivestreams.Publisher` then you will either need an _adapter_ (see below) or migrate to `java.util.concurrent.Flow.Publisher`. +* If you interact with `org.reactivestreams.Publisher` publishers and you can't migrate them to `java.util.concurrent.Flow.Publisher` (e.g., because it is a third-party library), then you will need an _adapter_. Please encourage third-party libraries to migrate to `Flow`. + +=== Adapters between Flow and legacy Reactive Streams APIs + +* We recommend using the adapters from the https://smallrye.io/smallrye-mutiny-zero[Mutiny Zero project]. +** The Maven coordinates are `groupId: io.smallrye.reactive`, `artifactId: mutiny-zero-flow-adapters` +** Use `AdaptersToFlow` to convert from _Reactive Streams_ types to `Flow` types, and +** Use `AdaptersToReactiveStreams` to convert `Flow` types to _Reactive Streams_ types. +* The Mutiny Zero adapters have virtually zero overhead. + +== Other API changes + +=== Deprecated API removals + +* `Uni` and `Multi` `onSubscribe()` group is now `onSubscription()`. +* `AssertSubscriber.await()` has been replaced by event-specific methods (items, failure, completion, etc). +* The _RxJava 2_ integration module has been discarded (only RxJava 3 is now supported). + +=== Experimental API promotions + +* `Uni` and `Multi` subscription-bound contexts. +* `Uni.join()` publisher. +* `.ifNoItem()` timeout operators. +* `Uni` and `Multi` spies. +* `capDemandsUsing()` and `paceDemand()` request management operators. +* `Multi` `replay()` operator. diff --git a/content/docs/smallrye-mutiny/2/reference/publications.adoc b/content/docs/smallrye-mutiny/2/reference/publications.adoc new file mode 100644 index 000000000..05db24027 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/reference/publications.adoc @@ -0,0 +1,18 @@ += Publications +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: reference,advanced + +Julien Ponge, Arthur Navarro, Clément Escoffier, and Frédéric Le Mouël. 2021. +*https://doi.org/10.1145/3486605.3486788[Analysing the Performance and Costs of Reactive Programming Libraries in Java].* +_In Proceedings of the 8th ACM SIGPLAN International Workshop on Reactive and Event-Based Languages and Systems (REBLS ’21)_, October 18, 2021, Chicago, IL, USA. ACM, New York, NY, USA, 10 pages. +https://hal.inria.fr/hal-03409277/document[(PDF)] + +____ +Modern services running in cloud and edge environments need to be resource-efficient to increase deployment density and reduce operating costs. +Asynchronous I/O combined with asynchronous programming provides a solid technical foundation to reach these goals. +Reactive programming and reactive streams are gaining traction in the Java ecosystem. +However, reactive streams implementations tend to be complex to work with and maintain. +This paper discusses the performance of the three major reactive streams compliant libraries used in Java applications: RxJava, Project Reactor, and SmallRye Mutiny. +As we will show, advanced optimization techniques such as operator fusion do not yield better performance on realistic I/O-bound workloads, and they significantly increase development and maintenance costs. +____ diff --git a/content/docs/smallrye-mutiny/2/reference/uni-and-multi.adoc b/content/docs/smallrye-mutiny/2/reference/uni-and-multi.adoc new file mode 100644 index 000000000..bc8181723 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/reference/uni-and-multi.adoc @@ -0,0 +1,44 @@ += Uni and Multi +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: reference,beginner + +Mutiny defines two _reactive_ types: + +* `Multi` - represents streams of ++_++0..++*_++ items (potentially unbounded) +* `Uni` - represents streams receiving either an item or a failure + +[TIP] +==== + +The Mutiny name comes from the contraction of `Multi` and `Uni` names + +==== + +Both `Uni` and `Multi` are asynchronous types. +They receive and fire events at any time. + +You may wonder why we make the distinction between `Uni` and `Multi.` +Conceptually, a `Uni` is a `Multi,` right? + +In practice, you don't use `Unis` and `Multis` the same way. +The use cases and operations are different. + +* `Uni` does not need the complete ceremony presented above as the _request_ does not make sense. +* The `subscribe` event expresses the interest and triggers the computation, no need for an additional _request_. +* `Uni` can handle items having a `null` value (and has specific methods to handle this case). +* `Multi` does not allow it (because the Reactive Streams specification forbids it). +* Having a `Uni` implementing `Publisher` would be a bit like having `Optional` implementing `Iterable`. + +In other words, `Uni`: + +* can receive at most 1 `item` event, or a `failure` event +* cannot receive a `completion` event (`null` in the case of 0 items) +* cannot receive a `request` event + +The following snippet shows how you can use `Uni` and `Multi`: + +[source,java] +---- +include::{examples-dir}/java/guides/UniMultiComparisonTest.java[tags=code,indent=0] +---- diff --git a/content/docs/smallrye-mutiny/2/reference/what-is-reactive-programming.adoc b/content/docs/smallrye-mutiny/2/reference/what-is-reactive-programming.adoc new file mode 100644 index 000000000..9065de5f3 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/reference/what-is-reactive-programming.adoc @@ -0,0 +1,31 @@ += What is Reactive Programming? +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: reference,beginner + +Mutiny is a reactive programming library. +If you look on Wikipedia for reactive programming, you will find the following definition: + +____ +Reactive Programming combines functional programming, the observer pattern, and the iterable pattern. +____ + +While correct, we never found this definition very helpful. +It does not convey clearly what's reactive programming is all about. +So, let's make another definition, much more straightforward: + +____ +Reactive programming is about programming with data streams. +____ + +That's it. +Reactive programming is about streams and especially, observing them. +It pushes that idea to its limit: with reactive programming, everything is a data stream. + +With reactive programming, you observe streams and implement side effects when _something_ flows in the stream: + +image::images/diagrams/mermaid-491ff27a51a5.svg[mermaid diagram] + +It's asynchronous by nature as you don't know when the _data_ is going to be seen. +Yet, reactive programming goes beyond this. +It provides a toolbox to compose streams and process events. diff --git a/content/docs/smallrye-mutiny/2/reference/what-makes-mutiny-different.adoc b/content/docs/smallrye-mutiny/2/reference/what-makes-mutiny-different.adoc new file mode 100644 index 000000000..aec919089 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/reference/what-makes-mutiny-different.adoc @@ -0,0 +1,108 @@ += What makes Mutiny different? +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: reference,beginner + +There are other reactive programming libraries out there. +In the Java world, we can mention Project Reactor and Rx Java. + +So, what makes Mutiny different from these two well-known libraries? +*The API!* + +Asynchronous is hard to grasp for most developers, and for good reasons. +Thus, the API must not require advanced knowledge or add cognitive overload. +It should help you design your logic and still be intelligible when you return to the code 6 months later. + +To achieve this, Mutiny is built on three pillars: + +* *Event-Driven* - with Mutiny, you listen for _events_ and handle them, +* *API Navigability* - based on the event-driven nature, the API is built around the type of events and drive the navigation based on the kind of event you want to handle, +* *Simplicity* - Mutiny provides only two types (`Multi` and `Uni`), which can handle any kind of asynchronous interactions. + +== Events? + +When you use Mutiny, you design a pipeline in which the events flow. +Your code observes these events and react. + +Each processing stage is a new pipe you append to the pipeline. +This pipe may change the events, create new ones, drops, buffers, whatever you need. + +In general, events flow from upstream to downstream, from source to sinks. +Some events can _swim_ upstream from the sinks to the source. + +Events going from upstream to downstream are published by `Publishers` and consumed by (downstream) `Subscribers,` which may also produce events for their own downstream, as illustrated by the following diagram: + +image::images/diagrams/mermaid-f71496c9eae5.svg[mermaid diagram] + +Four types of events can flow in this direction: + +* *Subscribed* - indicates that the upstream has taken into account the subscription - more on this later, +* *Items* - events containing some (business) _value_, +* *Completion* - event indicating that the source won't emit any more items, +* *Failure* - event telling that something terrible happened upstream and that the source cannot continue to emit items. + +`Failure` and `Completion` are terminal events. +Once they are sent, no more items will flow. + +Three types of events flow in the opposite direction, _i.e._ from downstream to upstream: + +* *Subscription* - event sent by a _subscriber_ to indicate its interest for the events (such as items) emitted by upstream +* *Requests* - event sent by a _subscriber_ indicating how many items event it can handle - this is related to back-pressure +* *Cancellation* - event sent by a _subscriber_ to stop the reception of events. + +In a typical scenario: + +[arabic] +. A subscriber _subscribes_ to the upstream - the upstream receive the `subscription request`, and when initialized sends the `subscribed` event to the subscriber +. The subscriber gets the `subscribed` event with a _subscription_ used to emit the `requests` and `cancellation` events +. The subscriber sends a `request` event indicating how many items it can handle at this moment; it can request 1, _n_, or infinite. +. The publisher receiving the `request` event starts emitting at most _n_ item events to the subscriber +. The subscriber can decide at any time to request more events or to cancel the subscription + +image::images/diagrams/mermaid-0277dcc2b534.svg[mermaid diagram] + +The `request` event is the cornerstone of the back-pressure protocol. +A subscriber should not request more than what it can handle, and a publisher should not emit more items than the amount of request received. + +[NOTE] +==== + +Mutiny uses the https://www.reactive-streams.org/[Reactive Streams] protocol for back-pressure management and the https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/concurrent/Flow.html[Java Flow APIs]. + +==== + +[IMPORTANT] +==== + +Don't forget to subscribe! + +If no subscriber _subscribes_, no items will be emitted. +More importantly, nothing will ever happen. + +If your program does not do anything, check that it subscribes, it's a very common error. + +==== + +== An event-driven API? + +Mutiny is an event-driven API. + +For each type of event, there is an `on` associated method that lets you handle this specific event. +For example: + +[source,java] +---- +include::{examples-dir}/java/guides/EventsTest.java[tags=code,indent=0] +---- + +Of course, the methods presented in this snippet are not very interesting, although they are quite useful to trace what's going on. + +You can see a common pattern emerging: + +[source,java] +---- +.onEvent().invoke(event -> ...); +---- + +`invoke` is just one of the methods available. +Each _group_ proposes methods specific to the type of event. For example, `onFailure().recover`, `onCompletion().continueWith` and so on. diff --git a/content/docs/smallrye-mutiny/2/reference/why-is-asynchronous-important.adoc b/content/docs/smallrye-mutiny/2/reference/why-is-asynchronous-important.adoc new file mode 100644 index 000000000..b06a0d807 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/reference/why-is-asynchronous-important.adoc @@ -0,0 +1,45 @@ += Why is asynchronous important? +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: reference,beginner + +We are living in a distributed world. + +Most of the applications built nowadays are distributed systems. +The Cloud, IoT, microservices, mobile application, even simple CRUD applications are distributed applications. + +Still, developing distributed systems is hard! + +.Distributed systems are asynchronous +image::reference/distributed_systems_are_asynchronous.png[Distributed systems are asynchronous] + +Communications in distributed systems are inherently asynchronous and unreliable. +Anything can go wrong, anytime, and often with no prior notice. + +Network disruptions, unavailable services, software, or hardware failures are just a tiny subset of the wide variety of failures that can happen in distributed systems. + +_Correctly_ building distributed applications is a considerable challenge, as it requires re-assessing almost everything we know from traditional software development. + +Most classic applications use a synchronous development model. +Synchronous code is easy to reason about, more comfortable to write and read than asynchronous code, but it has some hidden cost. +This cost emerges when building I/O intensive applications, quite common in distributed applications. + +In general, these traditional applications assign one thread per request, and so they handle multiple concurrent requests with multiple threads. +When the request processing needs to interact over the network, it uses that _worker_ thread, which blocks the thread until the response has been received. +This response may never come, so you need to add watchdogs handling timeouts and other resilience patterns. +And, to handle more requests concurrently, you need to create more threads. + +Threads come at a cost. +Each thread requires memory, and the more threads you have, the more CPU cycles are used to handle the context switches. +Thus, this model ends up being costly, limits the deployment density, and on the Cloud means that you pay bigger bills. + +Fortunately, there is another way, and it relies on non-blocking I/O, an efficient way to handle I/O interactions that do not require additional threads. +While applications using non-blocking I/O are more efficient and better suited for the Cloud's distributed nature, they come with a considerable constraint: you must never block the I/O thread. + +Thus, you need to implement your business logic using an asynchronous development model. + +I/O is not the only reason why asynchronous is essential in Today's systems. +Most of the interactions in the real world are asynchronous and event-driven. +Representing these interactions using synchronous processes is not only wrong; it also introduces fragility in your application. + +Asynchronous is a significant shift. +Mutiny helps you to take the plunge. diff --git a/content/docs/smallrye-mutiny/2/tags-index.adoc b/content/docs/smallrye-mutiny/2/tags-index.adoc new file mode 100644 index 000000000..6f43a9375 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/tags-index.adoc @@ -0,0 +1,24 @@ += Index +include::_attributes.adoc[] + +== Document kinds + +[width="100%",cols="18%,82%",options="header",] +|=== +|Tag |Description +|`tutorial` |Tutorials to get started with Mutiny. +|`guides` |Topic-centric guides on how to use Mutiny. +|`reference` |Reference documents to understand core concepts in Mutiny. +|=== + +== Audience level + +[cols=",",options="header",] +|=== +|Tag |Description +|`beginner` |Reading materials if you are new to Mutiny +|`intermediate` |Intermediate operations beyond the basics +|`advanced` |Advanced operations beyond classic usage of Mutiny +|=== + +== Tags diff --git a/content/docs/smallrye-mutiny/2/tutorials/creating-multi-pipelines.adoc b/content/docs/smallrye-mutiny/2/tutorials/creating-multi-pipelines.adoc new file mode 100644 index 000000000..8d551c63f --- /dev/null +++ b/content/docs/smallrye-mutiny/2/tutorials/creating-multi-pipelines.adoc @@ -0,0 +1,152 @@ += Creating `Multi` pipelines +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: tutorial,beginner + +A `Multi` represents a _stream_ of data. +A stream can emit 0, 1, n, or an infinite number of items. + +You will rarely create instances of `Multi` yourself but instead use a reactive client that exposes a Mutiny API. +Still, just like `Uni` there exists a rich API for creating `Multi` objects. + +== The Multi type + +A `Multi++<++T++>++` is a data stream that: + +* emits `0..n` item events +* emits a failure event +* emits a completion event for bounded streams + +[WARNING] +==== + +Failures are terminal events: after having received a failure no further item will be emitted. + +==== + +`Multi++<++T++>++` provides many operators that create, transform, and orchestrate `Multi` sequences. +The operators can be used to define a processing pipeline. +The events flow in this pipeline, and each operator can process or transform the events. + +`Multis` are lazy by nature. +To trigger the computation, you must subscribe. + +The following snippet provides a simple example of pipeline using `Multi`: + +[source,java] +---- +include::{examples-dir}/java/tutorials/CreatingMultiTest.java[tags=pipeline,indent=0] +---- + +== Subscribing to a Multi + +Remember, if you don't subscribe, nothing is going to happen. +Also, the pipeline is materialized for each _subscription_. + +When subscribing to a `Multi,` you can pass an item callback (invoked when the item is emitted), or pass two callbacks, one receiving the item and one receiving the failure, or three callbacks to handle respectively the item, failure and completion events. + +[source,java] +---- +include::{examples-dir}/java/tutorials/CreatingMultiTest.java[tags=subscription,indent=0] +---- + +Note the returned `Cancellable`: this object allows canceling the stream if need be. + +== Creating Multi from items + +There are many ways to create `Multi` instances. +See `Multi.createFrom()` to see all the possibilities. + +For instance, you can create a `Multi` from known items or from an `Iterable`: + +[source,java] +---- +include::{examples-dir}/java/tutorials/CreatingMultiTest.java[tags=simple,indent=0] +---- + +Every subscriber receives the same set of items (`1`, `2`... `5`) just after the subscription. + +You can also use `Suppliers`: + +[source,java] +---- +include::{examples-dir}/java/tutorials/CreatingMultiTest.java[tags=supplier,indent=0] +---- + +The `Supplier` is called for every subscriber, so each of them will get different values. + +[TIP] +==== + +You can create ranges using `Multi.createFrom().range(start, end)`. + +==== + +== Creating failing Multis + +Streams can also fail. + +Failures are used to indicate to the downstream subscribers that the source encountered a terrible error and cannot continue emitting items. +Create failed `Multi` instances with: + +[source,java] +---- +include::{examples-dir}/java/tutorials/CreatingMultiTest.java[tags=failed,indent=0] +---- + +== Creating empty Multis + +Unlike `Uni,` `Multi` streams don't send `null` items (this is forbidden in _reactive streams_). + +Instead `Multi` streams send completion events indicating that there are no more items to consume. +Of course, the completion event can happen even if there are no items, creating an empty stream. + +You can create such a stream using: + +[source,java] +---- +include::{examples-dir}/java/tutorials/CreatingMultiTest.java[tags=empty,indent=0] +---- + +== Creating Multis using an emitter (_advanced_) + +You can create a `Multi` using an emitter. +This approach is useful when integrating callback-based APIs: + +[source,java] +---- +include::{examples-dir}/java/tutorials/CreatingMultiTest.java[tags=emitter,indent=0] +---- + +The emitter can also send a failure. +It can also get notified of cancellation to, for example, stop the work in progress. + +== Creating Multis from _ticks_ (_advanced_) + +You can create a stream that emit a _ticks_ periodically: + +[source,java] +---- +include::{examples-dir}/java/tutorials/CreatingMultiTest.java[tags=ticks,indent=0] +---- + +The downstream receives a `long,` which is a counter. +For the first tick, it's 0, then 1, then 2, and so on. + +== Creating Multis from a generator (_advanced_) + +You can create a stream from some _initial state_, and a _generator function_: + +[source,java] +---- +include::{examples-dir}/java/tutorials/CreatingMultiTest.java[tags=generator,indent=0] +---- + +The initial state is given through a supplier (here `() -++>++ 1`). +The generator function accepts 2 arguments: + +* the current state, +* an emitter that can emit a new item, emit a failure, or emit a completion. + +The generator function return value is the next _current state_. +Running the previous example gives the following number suite: `++{++2, 4, 7, 11, 17, 26, 40, 61}`. diff --git a/content/docs/smallrye-mutiny/2/tutorials/creating-uni-pipelines.adoc b/content/docs/smallrye-mutiny/2/tutorials/creating-uni-pipelines.adoc new file mode 100644 index 000000000..6ffa03df1 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/tutorials/creating-uni-pipelines.adoc @@ -0,0 +1,128 @@ += Creating `Uni` pipelines +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: tutorial,beginner + +A `Uni` represents a _stream_ that can only emit either an item or a failure event. + +You rarely create instances of `Uni` yourself, but, instead, use a reactive client exposing a Mutiny API that provides `Uni` objects. +That being said, it can be handy at times. + +== The Uni type + +A `Uni++<++T++>++` is a specialized stream that emits only an item or a failure. +Typically, `Uni++<++T++>++` are great to represent asynchronous actions such as a remote procedure call, an HTTP request, or an operation producing a single result. + +`Uni++<++T++>++` provides many operators that create, transform, and orchestrate `Uni` sequences. + +As said, `Uni++<++T++>++` emits either an item or a failure. +Note that the item can be `null,` and the `Uni` API has specific methods for this case. + +Typically, a `Uni++<++Void++>++` always emits `null` as item event or a failure if the represented operation fails. +You can consider the item event as a completion signal indicating the success of the operation. + +The offered operators can be used to define a processing pipeline. +The event, either the item or failure, flows in this pipeline, and each operator can process or transform the event. +`Unis` are lazy by nature. + +To trigger the computation, you must have a final subscriber indicating your interest. +The following snippet provides a simple example of pipeline using `Uni`: + +[source,java] +---- +include::{examples-dir}/java/tutorials/CreatingUniTest.java[tags=pipeline,indent=0] +---- + +== Subscribing to a Uni + +[IMPORTANT] +==== + +Remember: if you don't subscribe, nothing is going to happen. +What's more, the pipeline is materialized for each _subscription_. + +==== + +When subscribing to a `Uni`, you can pass an item callback (invoked when the item is emitted), or two callbacks (one receiving the item and one receiving the failure): + +[source,java] +---- +include::{examples-dir}/java/tutorials/CreatingUniTest.java[tags=subscription,indent=0] +---- + +Note the returned `Cancellable`: this object allows canceling the operation if need be. + +== Creating Unis from items + +There are many ways to create `Uni` instances. +Use `Uni.createFrom()` to see all the possibilities. + +You can, for instance, create a `Uni` from a known value: + +[source,java] +---- +include::{examples-dir}/java/tutorials/CreatingUniTest.java[tags=simple,indent=0] +---- + +Every subscriber receives the item `1` just after the subscription. + +You can also pass a `Supplier`: + +[source,java] +---- +include::{examples-dir}/java/tutorials/CreatingUniTest.java[tags=supplier,indent=0] +---- + +The `Supplier` is called for every subscriber. +So, each of them will get a different value. + +== Creating failing Unis + +Operations represented by `Unis` can also emit a failure event, indicating that the operation failed. + +You can create failed `Uni` instances with: + +[source,java] +---- +include::{examples-dir}/java/tutorials/CreatingUniTest.java[tags=failed,indent=0] +---- + +== Creating `Uni++<++Void++>++` + +When the represented operation to not produce a result, you still need a way to indicate the operation's completion. +For this, you need to emit a `null` item: + +[source,java] +---- +include::{examples-dir}/java/tutorials/CreatingUniTest.java[tags=null,indent=0] +---- + +== Creating Unis using an emitter (_advanced_) + +You can create a `Uni` using an emitter. +This approach is useful when integrating callback-based APIs: + +[source,java] +---- +include::{examples-dir}/java/tutorials/CreatingUniTest.java[tags=emitter,indent=0] +---- + +The emitter can also send a failure. +It can also get notified of cancellation to, for example, stop the work in progress. + +== Creating Unis from a CompletionStage (_advanced_) + +You can also `Uni` objects from `CompletionStage` / `CompletableFuture`. +This is useful when integrating with APIs that are based on these types: + +[source,java] +---- +include::{examples-dir}/java/tutorials/CreatingUniTest.java[tags=cs,indent=0] +---- + +[TIP] +==== + +You can also create a `CompletionStage` from a `Uni` using `uni.subscribe().asCompletionStage()` + +==== diff --git a/content/docs/smallrye-mutiny/2/tutorials/getting-mutiny.adoc b/content/docs/smallrye-mutiny/2/tutorials/getting-mutiny.adoc new file mode 100644 index 000000000..651586995 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/tutorials/getting-mutiny.adoc @@ -0,0 +1,142 @@ += Getting started with Mutiny +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: tutorial,beginner + +== Using Mutiny in a Java application + +Add the _dependency_ to your project using your preferred build tool: + +[.tabs] +==== + +Apache Maven:: ++ +-- + +[source,xml,subs="attributes+"] +---- + + io.smallrye.reactive + mutiny + {versions-mutiny} + +---- + +-- + +Gradle (Groovy):: ++ +-- + +[source,groovy,subs="attributes+"] +---- +implementation 'io.smallrye.reactive:mutiny:{versions-mutiny}' +---- + +-- + +Gradle (Kotlin):: ++ +-- + +[source,kotlin,subs="attributes+"] +---- +implementation("io.smallrye.reactive:mutiny:{versions-mutiny}") +---- + +-- + +JBang:: ++ +-- + +[source,java,subs="attributes+"] +---- +//DEPS io.smallrye.reactive:mutiny:{versions-mutiny} +---- + +-- + +==== + +== Using Mutiny with Quarkus + +Most of the https://quarkus.io[Quarkus] extensions with reactive capabilities already depend on Mutiny. + +You can also add the `quarkus-mutiny` dependency explicitly from the command-line: + +[source,bash] +---- +mvn quarkus:add-extension -Dextensions=mutiny +---- + +or by editing the `pom.xml` file and adding: + +[source,xml] +---- + + io.quarkus + quarkus-mutiny + +---- + +== Using Mutiny with Vert.x + +Most of the https://vertx.io[Eclipse Vert.x] stack modules are available through the https://smallrye.io/smallrye-mutiny-vertx-bindings/[SmallRye Mutiny Vert.x Bindings] project. + +Bindings for Vert.x modules are named by prepending `smallrye-mutiny-`. +As an example here's how to add a dependency to the `vertx-core` Mutiny bindings: + +[.tabs] +==== + +Apache Maven:: ++ +-- + +[source,xml,subs="attributes+"] +---- + + io.smallrye.reactive + smallrye-mutiny-vertx-core + {versions-vertx-bindings} + +---- + +-- + +Gradle (Groovy):: ++ +-- + +[source,groovy,subs="attributes+"] +---- +implementation 'io.smallrye.reactive:smallrye-mutiny-vertx-core:{versions-vertx-bindings}' +---- + +-- + +Gradle (Kotlin):: ++ +-- + +[source,kotlin,subs="attributes+"] +---- +implementation("io.smallrye.reactive:smallrye-mutiny-vertx-core:{versions-vertx-bindings}") +---- + +-- + +JBang:: ++ +-- + +[source,java,subs="attributes+"] +---- +//DEPS io.smallrye.reactive:smallrye-mutiny-vertx-core:{versions-vertx-bindings} +---- + +-- + +==== diff --git a/content/docs/smallrye-mutiny/2/tutorials/handling-failures.adoc b/content/docs/smallrye-mutiny/2/tutorials/handling-failures.adoc new file mode 100644 index 000000000..5dab71250 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/tutorials/handling-failures.adoc @@ -0,0 +1,96 @@ += Handling failures +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: tutorial,beginner + +Mutiny provides several operators to handle failures. + +Remember, failures are terminal events sent by the observed stream, indicating that something _bad_ happened. +After a failure, no more items are being received. + +When such an event is received, you can: + +* propagate the failure downstream (default), or +* transform the failure into another failure, or +* recover from it by switching to another stream, passing a fallback item, or completing, or +* retrying (covered in the next guide) + +If you don't handle the failure event, it is propagated downstream until a stage handles the failure or reaches the final subscriber. + +[IMPORTANT] +==== + +on `Multi`, a failure cancels the subscription, meaning you will not receive any more items. +The `retry` operator lets you re-subscribe and continue the reception. + +==== + +== Observing failures + +It can be useful to execute some custom action when a failure happens. +For example, you can log the failure: + +[source,java] +---- +include::{examples-dir}/java/tutorials/HandlingFailuresTest.java[tags=invoke,indent=0] +---- + +[TIP] +==== + +You can also execute an asynchronous action using `onFailure().call(Function++<++Throwable, Uni++<++?++>++)`. +The received failure will be propagated downstream when the `Uni` produced by the passed function emits its item. + +==== + +== Transforming failures + +Another useful action on failure is to transform the failure into a _more meaningful_ failure. + +Typically, you can wrap a low-level failure (like an `IOException`) into a business failure (`ServiceUnavailableException`): + +[source,java] +---- +include::{examples-dir}/java/tutorials/HandlingFailuresTest.java[tags=transform,indent=0] +---- + +== Recovering using fallback item(s) + +In general, upon failure, you want to recover. +The first approach is to recover by replacing the failure with an item: + +[source,java] +---- +include::{examples-dir}/java/tutorials/HandlingFailuresTest.java[tags=recover-item,indent=0] +---- + +The second approach receives a `Supplier` to compute the fallback item. +For the downstream, it didn't fail; it gets the fallback item instead. + +However, don't forget that failures are terminal! +So for `Multi`, the downstream receives the fallback item followed by the completion signal, as no more items can be produced. + +== Completing on failure + +When observing a `Multi` you can replace the failure with the completion signal: + +[source,java] +---- +include::{examples-dir}/java/tutorials/HandlingFailuresTest.java[tags=recover-completion,indent=0] +---- + +The downstream won't see the failure, just the completion event. + +== Switching to another stream + +On failure, you may want to switch to an alternate stream. +When the failure is received, it subscribes to this other stream and propagates the items from this stream instead: + +[source,java] +---- +include::{examples-dir}/java/tutorials/HandlingFailuresTest.java[tags=recover-switch,indent=0] +---- + +The `recoverWithUni` and `recoverWithMulti` methods replace the failed upstream with the returned stream. + +The fallback streams must produce the same type of event as the original upstream. diff --git a/content/docs/smallrye-mutiny/2/tutorials/hello-mutiny.adoc b/content/docs/smallrye-mutiny/2/tutorials/hello-mutiny.adoc new file mode 100644 index 000000000..80fab7b4e --- /dev/null +++ b/content/docs/smallrye-mutiny/2/tutorials/hello-mutiny.adoc @@ -0,0 +1,75 @@ += Hello Mutiny! +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: tutorial,beginner + +Once you made Mutiny available to your classpath, you can start writing code. +Let's start with this simple program: + +[source,java] +---- +include::{examples-dir}/java/FirstProgram.java[indent=0] +---- + +This program prints: + +.... +>> HELLO MUTINY +.... + +== Dissecting the pipeline + +What's interesting is how this message is _built_. +We described a processing pipeline taking an item, processing it and finally consuming it. + +First, we create a `Uni`, one of the two types with `Multi` that Mutiny provides. +A `Uni` is a stream emitting either a single item or a failure. + +Here, we create a `Uni` emitting the `"hello"` item. +This is the input of our pipeline. +Then we process this item: + +* we append `" mutiny"`, then +* we make it an uppercase string. + +This forms the processing part of our pipeline, and then we finally *subscribe* to the pipeline. + +This last part is essential. +If you don't have a final subscriber, nothing is going to happen. +Mutiny types are lazy, meaning that you need to express your interest. +If you don't, the computation won't even start. + +[IMPORTANT] +==== + +If your program doesn't do anything, verify that you didn't forget to subscribe! + +==== + +== Mutiny uses a builder API! + +Another important aspect is the pipeline construction. +Appending a new _stage_ to a pipeline returns a new `Uni.` + +The previous program is equivalent to: + +[source,java] +---- +include::{examples-dir}/java/FirstProgramTest.java[tags=uni,indent=0] +---- + +It is fundamental to understand that this program is not equivalent to: + +[source,java] +---- +include::{examples-dir}/java/FirstProgramTest.java[tags=uni2,indent=0] +---- + +This program just prints `"++>>++ hello"`, as it does not use the appended stages and the final subscriber consumes the first `Uni.` + +[WARNING] +==== + +Mutiny APIs are not fluent and each computation stage returns a new object. + +==== diff --git a/content/docs/smallrye-mutiny/2/tutorials/mutiny-workshop.adoc b/content/docs/smallrye-mutiny/2/tutorials/mutiny-workshop.adoc new file mode 100644 index 000000000..99fcf5a81 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/tutorials/mutiny-workshop.adoc @@ -0,0 +1,15 @@ += Go further with the Mutiny workshop! +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: tutorial,beginner + +One great option to teach yourself Mutiny is to go through the https://github.com/smallrye/smallrye-mutiny/tree/main/workshop-examples[Mutiny workshop examples]. + +These self-contained https://jbang.dev/[JBang] scripts cover the main parts of the Mutiny APIs. + +It's a fun and easy way to discover Mutiny! + +Check out https://github.com/smallrye/smallrye-mutiny/tree/main/workshop-examples to learn more. + +.Running a workshop sample +image::tutorials/running-workshop-sample.png[Running a workshop sample,width=400] diff --git a/content/docs/smallrye-mutiny/2/tutorials/observing-events.adoc b/content/docs/smallrye-mutiny/2/tutorials/observing-events.adoc new file mode 100644 index 000000000..3d9e1ca28 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/tutorials/observing-events.adoc @@ -0,0 +1,111 @@ += Observing events +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: tutorial,beginner + +Learn how to observe the events emitted by `Uni` and `Multi` instances. + +== Events + +`Uni` and `Multi` emit _events_. +Your code is going to observe and process these events. + +Most of the time, your code is only interested in item and failure events. +But there are other kinds of events such as cancellation, request, completion, and so on: + +[width="100%",cols=">12%,9%,<15%,<64%",options="header",] +|=== +|Event |Uni / Multi |Direction |Note +|*item* |Uni {plus} Multi |upstream -++>++ downstream |The upstream sent an item. +|*failure* |Uni {plus} Multi |upstream -++>++ downstream |The upstream failed. +|*completion* |Multi |upstream -++>++ downstream |The upstream completed. +|*subscribe* |Uni and Multi |downstream -++>++ upstream |A downstream subscriber is interested in the data. +|*subscription* |Uni and Multi |upstream -++>++ downstream |Event happening after a `subscribe` event to indicate that the upstream acknowledged the subscription. +|*cancellation* |Uni and Multi |downstream -++>++ upstream |A downstream subscriber does not want any more events. +|*overflow* |Multi |upstream -++>++ downstream |The upstream has emitted more than the downstream can handle. +|*request* |Multi |downstream -++>++ upstream |The downstream indicates its capacity to handle `n` items. +|=== + +It’s not rare that you need to look at these various events to understand better what’s going on or implement specific side effects. +For example, you may need to close a resource after a completion event or log a message on failure or cancellation. + +For each kind of event, there is an associated group providing the methods to handle that specific event: `onItem()`, `onFailure()`, `onCompletion()` and so on. +These groups provide two methods to _peek_ at the various events without impacting its distribution: `invoke(...)` and `call(...)`. +It does not transform the received event; it notifies you that something happened and let you react. +Once this _reaction_ completes, the event is propagated downstream or upstream depending on the direction of the event. + +== The `invoke` method + +The invoke method is synchronous and the passed callback does not return anything. +Mutiny invokes the configured callback when the observed stream dispatches the event: + +[source,java] +---- +include::{examples-dir}/java/tutorials/ObserveTest.java[tags=invoke,indent=0] +---- + +As said above, `invoke` is synchronous. +Mutiny invokes the callback and propagates the event downstream when the callback returns. +It blocks the dispatching. + +image::images/diagrams/mermaid-e88e1918cd26.svg[mermaid diagram] + +Of course, we highly recommend you not to block. + +The following snippets show how you can log the different types of events. + +[source,java] +---- +include::{examples-dir}/java/tutorials/ObserveTest.java[tags=invoke-all,indent=0] +---- + +The arrows from the previous code snippet indicate if the event comes from the upstream (source) or downstream (consumer) (see the table above for more details). +The `invoke` method does not change the event, except in one case. +If the callback throws an exception, the downstream does not get the actual event but get a failure event instead. + +When observing the failure event, if the callback throws an exception, Mutiny propagates a `CompositeException` aggregating the original failure and the callback failure. + +== The `call` method + +Unlike `invoke`, `call` is asynchronous, and the callback returns a `Uni++<++?++>++` object. + +`call` is often used when you need to implement asynchronous side-effects, such as closing resources. + +image::images/diagrams/mermaid-bc092005fd3f.svg[mermaid diagram] + +Mutiny does not dispatch the original event downstream until the Uni returned by the callback emits an item: + +[source,java] +---- +include::{examples-dir}/java/tutorials/ObserveTest.java[tags=call,indent=0] +---- + +As shown in the previous snippet, you can use this approach to delay items. +But, the primary use case is about completing asynchronous actions such as calling an asynchronous `close` method on a resource: + +[source,java] +---- +include::{examples-dir}/java/tutorials/ObserveTest.java[tags=close,indent=0] +---- + +Under the hood, Mutiny gets the `Uni` (by invoking the callback) and subscribes to it. +It observes the item or failure event from that Uni. +It discards the item value as only the emission matters in this case. + +If the callback throws an exception or the produced `Uni` produces a failure, Mutiny propagates that failure (or a `CompositeException`) downstream, replacing the original event. + +== Summary + +* The `invoke` and `call` methods are handy when you need to observe a `Uni` or a `Multi` without changing the transiting events. +* Use `invoke` for implementing synchronous side-effects or logging events. +* The asynchronous nature of `call` makes it perfect for implementing asynchronous side-effects, such as closing resources, flushing data, delay items, and so on. + +The following table highlights the key differences: + +[width="100%",cols=">20%,<33%,<47%",options="header",] +|=== +| |`invoke` |`call` +|*Nature* |synchronous |asynchronous +|*Return type* |`void` |`Uni++<++T++>++` +|*Main use cases* |logging, synchronous side-effects |I/O operations, closing resources, flushing data +|=== diff --git a/content/docs/smallrye-mutiny/2/tutorials/retrying.adoc b/content/docs/smallrye-mutiny/2/tutorials/retrying.adoc new file mode 100644 index 000000000..c2ec63ecc --- /dev/null +++ b/content/docs/smallrye-mutiny/2/tutorials/retrying.adoc @@ -0,0 +1,67 @@ += Retrying on failures +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: tutorial,beginner + +It is common to want to retry if something terrible happened. + +You can retry upon failure. +The https://quarkus.io/blog/uni-retry/[How does retry... retries] blog post provides a more detailed overview of the retry mechanism. + +[NOTE] +==== + +If despite multiple attempts, it still fails, the failure is propagated downstream. + +==== + +== Retry multiple times + +To retry on failure, use `onFailure().retry()`: + +[source,java] +---- +include::{examples-dir}/java/tutorials/RetryTest.java[tags=retry-at-most,indent=0] +---- + +You pass the number of retries as a parameter. + +[IMPORTANT] +==== + +While `.onFailure().retry().indefinitely()` is available, it may never terminate, so use it with caution. + +==== + +== Introducing delays + +By default, `retry` retries immediately. +When using remote services, it is often better to delay a bit the attempts. + +Mutiny provides a method to configure an exponential backoff: a growing delay between retries. +Configure the exponential backoff as follows: + +[source,java] +---- +include::{examples-dir}/java/tutorials/RetryTest.java[tags=retry-backoff,indent=0] +---- + +The backoff is configured with the initial and max delay. +Optionally, you can also configure a jitter to add a pinch of randomness to the delay. + +When using exponential backoff, you may not want to configure the max number of attempts (`atMost`), but a deadline. +To do so, use either `expireIn` or `expireAt`. + +== Deciding to retry + +As an alternative to `atMost`, you can also use `until`. +This method accepts a predicate called after every failure. +When used, a backoff should not be used. + +If the predicate returned `true,` it retries. +Otherwise, it stops retrying and propagates the last failure downstream: + +[source,java] +---- +include::{examples-dir}/java/tutorials/RetryTest.java[tags=retry-until,indent=0] +---- diff --git a/content/docs/smallrye-mutiny/2/tutorials/running-workshop-sample.png b/content/docs/smallrye-mutiny/2/tutorials/running-workshop-sample.png new file mode 100644 index 000000000..6eec15382 Binary files /dev/null and b/content/docs/smallrye-mutiny/2/tutorials/running-workshop-sample.png differ diff --git a/content/docs/smallrye-mutiny/2/tutorials/transforming-items-asynchronously.adoc b/content/docs/smallrye-mutiny/2/tutorials/transforming-items-asynchronously.adoc new file mode 100644 index 000000000..8a620c0c1 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/tutorials/transforming-items-asynchronously.adoc @@ -0,0 +1,110 @@ += Transforming items asynchronously +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: tutorial,beginner + +The previous examples illustrated how to transform each item from a stream into another item. +Yet, there are cases where we need to go beyond this, for example: + +* Transforming an item into a `Uni` -- any asynchronous processing such as calling a remote service, interacting with a database, etc +* Transforming an item into a `Multi` -- producing a multi-items stream based on the incoming item, filtering out items, etc + +Having the possibility to transform an item into a stream gives us many opportunities. +To implement such transformations, we use `onItem().transformToUni(Function++<++T, Uni++<++O++>>++)` and `onItem().transformToMulti(Function++<++T, Multi++<++O++>>++)` + +== Uni - Transforming an item into a Uni + +image::images/diagrams/mermaid-f67adb6018b3.svg[mermaid diagram] + +Imagine that you have a `Uni++<++String++>++`, and you want to call a remote service. + +Calling a remote service is an asynchronous action represented by a `Uni`, as in: + +[source,java] +---- +include::{examples-dir}/java/tutorials/TransformItemsAsyncTest.java[tags=service,indent=0] +---- + +To call this service, you need to transform the item received from the first `Uni` into the `Uni` returned by the service: + +[source,java] +---- +include::{examples-dir}/java/tutorials/TransformItemsAsyncTest.java[tags=call,indent=0] +---- + +This snippet _chains_ the first `Uni` with another one. +The returned `Uni` (`result`) emits the result from the remote service or a failure if anything wrong happened: + +[source,java] +---- +include::{examples-dir}/java/tutorials/TransformItemsAsyncTest.java[tags=chain,indent=0] +---- + +== Uni - Transforming an item into a Multi + +The previous example produced a single item. +You may want to transform the received item into a stream which is... a `Multi`! + +[source,java] +---- +include::{examples-dir}/java/tutorials/TransformItemsAsyncTest.java[tags=uni-to-multi,indent=0] +---- + +This code creates a stream of two elements, duplicating the received item. + +[source,java] +---- +include::{examples-dir}/java/tutorials/TransformItemsAsyncTest.java[tags=uni-to-multi-2,indent=0] +---- + +image::images/diagrams/mermaid-3b8d4c58438a.svg[mermaid diagram] + +The produced `Multi` objects can of course be more complicated than that and emit items in an asynchronous fashion. + +== Transforming items from Multi - the _merge_ vs _concatenate_ dilemma + +When transforming items emitted by an upstream `Multi,` we need to answer the following question: how are we going to _merge_ the produced items back? + +Let's take an example. +Imagine a `Multi` emitting the `Cameron` and `Donna` items (in order), and you want to call the `invokeRemoteGreetingService` from above. +It thus calls `invokeRemoteGreetingService("Cameron")` then `invokeRemoteGreetingService("Donna")`. + +The service does not have a constant response time (because of network latency or the load), which means that responses can be interleaved. +Indeed, you may receive `"Hello Donna"` before `"Hello Cameron"`. + +Now, how do you want to handle this case? +Do you need to preserve the order and ensure that the downstream subscriber will always get `"Hello Cameron"` first, or do you accept interleaved responses? + +When transforming items from `Multi` into streams, you need to decide in which order the items emitted by the produced stream are going to be received by the downstream subscriber. +Mutiny offers two possibilities: + +[arabic] +. *Merging* -- it does not preserve the order and emits the items from the produced streams as they come, or +. *Concatenating* -- it maintains and concatenates the streams produced for each item. + +== Multi - Transforming an item into a Uni + +To implement the scenario from the last section, you will use `onItem().transformToUniAndMerge` or `onItem().transformToUniAndConcatenate()` depending on your ordering choice: + +[source,java] +---- +include::{examples-dir}/java/tutorials/TransformItemsAsyncTest.java[tags=merge-concat,indent=0] +---- + +[IMPORTANT] +==== + +* When merging: items from the source `Multi` _may_ be processed *concurrently* depending on the concurrency level that has been set, if any. +* When concatenating: items from the source `Multi` are processed *in order*, waiting for each `Uni` to complete before moving on to the next item. + +==== + +== Multi - Transforming an item into a Multi + +`onItem().transformToMultiAndMerge` and `onItem().transformToMultiAndConcatenate` transform incoming items into `Multi` streams. +The produced `Multi` objects are either _merged_ or _concatenated_: + +[source,java] +---- +include::{examples-dir}/java/tutorials/TransformItemsAsyncTest.java[tags=merge-concat-multi,indent=0] +---- diff --git a/content/docs/smallrye-mutiny/2/tutorials/transforming-items.adoc b/content/docs/smallrye-mutiny/2/tutorials/transforming-items.adoc new file mode 100644 index 000000000..f13a92f76 --- /dev/null +++ b/content/docs/smallrye-mutiny/2/tutorials/transforming-items.adoc @@ -0,0 +1,53 @@ += Transforming items +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: tutorial,beginner + +Both `Unis` and `Multis` emit _items_. + +One of the most common operations you will do is transforming these items using a _synchronous_ 1-to-1 function. + +To achieve this, you use `onItem().transform(Function++<++T, U++>++)`. +It calls the passed function for each item and produces the result as an item which is propagated downstream. + +image::images/diagrams/mermaid-e6dd95e32a3f.svg[mermaid diagram] + +== Transforming items produced by a Uni + +Let's imagine you have a `Uni++<++String++>++,` and you want to capitalize the received `String`. +Implementing this transformation is done as follows: + +[source,java] +---- +include::{examples-dir}/java/tutorials/TransformItemsTest.java[tags=uni-transform,indent=0] +---- + +== Transforming items produced by a Multi + +The only difference for `Multi` is that the function is called for each item: + +[source,java] +---- +include::{examples-dir}/java/tutorials/TransformItemsTest.java[tags=multi-transform,indent=0] +---- + +The produced items are passed to the downstream subscriber: + +[source,java] +---- +include::{examples-dir}/java/tutorials/TransformItemsTest.java[tags=multi-transform-2,indent=0] +---- + +== What if the transformation failed? + +If the transformation throws an exception, that exception is caught and passed to the downstream subscriber as a _failure_ event. +It also means that the subscriber won't get further item after that failure. + +== Chaining multiple transformations + +You can chain multiple transformations: + +[source,java] +---- +include::{examples-dir}/java/tutorials/TransformItemsTest.java[tags=chain,indent=0] +---- diff --git a/content/docs/smallrye-mutiny/latest/_attributes.adoc b/content/docs/smallrye-mutiny/latest/_attributes.adoc new file mode 100644 index 000000000..bca0b16c5 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/_attributes.adoc @@ -0,0 +1,22 @@ +:project-version: 3.3.0 +// Attributes shared by every page. smallrye.io overrides the ones it needs when it publishes. +// Pages in subdirectories set :version-root: (../ per level) before including this file. +ifndef::version-root[:version-root:] +:idprefix: +:idseparator: - +:sectanchors: +:examples-dir: {version-root}_examples +:partialsdir: {version-root}_partials +:project-name: SmallRye Mutiny +:javadoc-version: latest +:versions-mutiny: 3.3.0 +:versions-vertx-bindings: 4.0.0-beta2 +:generated-dir: {version-root}_generated + +// Added by smallrye.io (tools/docs/Docs.java sync) +:javadoc-version: 3.3.0 +:relfileprefix: /docs/smallrye-mutiny/latest/ +:imagesdir: /docs/smallrye-mutiny/latest +:docs-project: smallrye-mutiny +:docs-version: latest +:docs-ref: 3.3.0 diff --git a/content/docs/smallrye-mutiny/latest/_examples/java/FirstProgram.java b/content/docs/smallrye-mutiny/latest/_examples/java/FirstProgram.java new file mode 100644 index 000000000..87d40de5d --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/_examples/java/FirstProgram.java @@ -0,0 +1,11 @@ +import io.smallrye.mutiny.Uni; + +public class FirstProgram { + + public static void main(String[] args) { + Uni.createFrom().item("hello") + .onItem().transform(item -> item + " mutiny") + .onItem().transform(String::toUpperCase) + .subscribe().with(item -> System.out.println(">> " + item)); + } +} \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/latest/_examples/java/FirstProgramTest.java b/content/docs/smallrye-mutiny/latest/_examples/java/FirstProgramTest.java new file mode 100644 index 000000000..d5c464230 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/_examples/java/FirstProgramTest.java @@ -0,0 +1,44 @@ +import guides.extension.SystemOut; +import guides.extension.SystemOutCaptureExtension; +import io.smallrye.mutiny.Uni; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(SystemOutCaptureExtension.class) +public class FirstProgramTest { + + @Test + public void testFirstProgram(SystemOut out) { + FirstProgram.main(new String[0]); + assertThat(out.get()).contains(">> HELLO MUTINY"); + } + + @Test + public void testUniBuilder(SystemOut out) { + // tag::uni[] + Uni uni1 = Uni.createFrom().item("hello"); + Uni uni2 = uni1.onItem().transform(item -> item + " mutiny"); + Uni uni3 = uni2.onItem().transform(String::toUpperCase); + + uni3.subscribe().with(item -> System.out.println(">> " + item)); + // end::uni[] + assertThat(out.get()).contains("HELLO MUTINY"); + assertThat(uni3.await().indefinitely()).isEqualTo("HELLO MUTINY"); + } + + @Test + public void testUniBuilder2(SystemOut out) { + // tag::uni2[] + Uni uni = Uni.createFrom().item("hello"); + + uni.onItem().transform(item -> item + " mutiny"); + uni.onItem().transform(String::toUpperCase); + + uni.subscribe().with(item -> System.out.println(">> " + item)); + // end::uni2[] + assertThat(out.get()).contains("hello"); + assertThat(uni.await().indefinitely()).isEqualTo("hello"); + } +} diff --git a/content/docs/smallrye-mutiny/latest/_examples/java/guides/BranchingTest.java b/content/docs/smallrye-mutiny/latest/_examples/java/guides/BranchingTest.java new file mode 100644 index 000000000..320469593 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/_examples/java/guides/BranchingTest.java @@ -0,0 +1,64 @@ +package guides; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import org.junit.jupiter.api.Test; + +import java.util.Random; + +public class BranchingTest { + + @Test + public void uni() { + // tag::pipeline[] + Random random = new Random(); + Uni.createFrom().item(() -> random.nextInt(100)) + .onItem().transformToUni(n -> { + if (n % 2 == 0) { + return evenOperation(n); + } else { + return oddOperation(n); + } + }) + .subscribe().with(System.out::println); + // end::pipeline[] + } + + // tag::branches[] + Uni evenOperation(int n) { + return Uni.createFrom().item("Even number: " + n) + .onItem().invoke(() -> System.out.println("(even branch)")); + } + + Uni oddOperation(int n) { + return Uni.createFrom().item("Odd number: " + n) + .onItem().invoke(() -> System.out.println("(odd branch)")); + } + // end::branches[] + + @Test + public void multi() { + // tag::multi-pipeline[] + Random random = new Random(); + Multi.createBy().repeating().supplier(random::nextInt).atMost(20) + .onItem().transformToUniAndMerge(n -> { + System.out.println("----"); + if (n < 0) { + return drop(); + } else if (n % 2 == 0) { + return evenOperation(n); + } else { + return oddOperation(n); + } + }) + .subscribe().with(str -> System.out.println("=> " + str)); + // end::multi-pipeline[] + } + + // tag::drop[] + Uni drop() { + return Uni.createFrom().nullItem() + .onItem().invoke(() -> System.out.println("(dropping negative value)")); + } + // end::drop[] +} diff --git a/content/docs/smallrye-mutiny/latest/_examples/java/guides/CompletionStageTest.java b/content/docs/smallrye-mutiny/latest/_examples/java/guides/CompletionStageTest.java new file mode 100644 index 000000000..27b0fd6ba --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/_examples/java/guides/CompletionStageTest.java @@ -0,0 +1,77 @@ +package guides; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.assertj.core.api.Assertions.assertThat; + +public class CompletionStageTest { + + @Test + public void testSubscribeAsCompletionStage() { + AtomicInteger counter = new AtomicInteger(); + Uni uni = Uni.createFrom().item(() -> "hello-" + counter.getAndIncrement()); + // tag::uni-subscribe-cs[] + CompletionStage cs = uni.subscribeAsCompletionStage(); + // end::uni-subscribe-cs[] + + assertThat(cs.toCompletableFuture().join()).isEqualTo("hello-0"); + + // tag::uni-subscribe-cs-twice[] + // Trigger the underlying operation twice: + CompletionStage cs1 = uni.subscribeAsCompletionStage(); + CompletionStage cs2 = uni.subscribeAsCompletionStage(); + // end::uni-subscribe-cs-twice[] + assertThat(cs1.toCompletableFuture().join()).isEqualTo("hello-1"); + assertThat(cs2.toCompletableFuture().join()).isEqualTo("hello-2"); + } + + + @Test + public void test() { + Executor executor = Runnable::run; + // tag::create-uni[] + Uni uni1 = Uni + // Create from a Completion Stage + .createFrom().completionStage( + CompletableFuture.supplyAsync(() -> "hello", executor) + ) + .onItem().transform(String::toUpperCase); + + Uni uni2 = Uni + // Create from a Completion Stage supplier (recommended) + .createFrom().completionStage( + () -> CompletableFuture.supplyAsync(() -> "hello", executor) + ) + .onItem().transform(String::toUpperCase); + // end::create-uni[] + + assertThat(uni1.await().indefinitely()).isEqualTo("HELLO"); + assertThat(uni2.await().indefinitely()).isEqualTo("HELLO"); + + // tag::create-multi[] + Multi multi1 = Multi + .createFrom().completionStage( + CompletableFuture.supplyAsync(() -> "hello", executor) + ) + .onItem().transform(String::toUpperCase); + + Multi multi2 = Multi + .createFrom().completionStage(() -> + CompletableFuture.supplyAsync(() -> "hello", executor) + ) + .onItem().transform(String::toUpperCase); + // end::create-multi[] + + assertThat(multi1.collect().asList().await().indefinitely()).containsExactly("HELLO"); + assertThat(multi2.collect().asList().await().indefinitely()).containsExactly("HELLO"); + + } +} diff --git a/content/docs/smallrye-mutiny/latest/_examples/java/guides/ContextPassingTest.java b/content/docs/smallrye-mutiny/latest/_examples/java/guides/ContextPassingTest.java new file mode 100644 index 000000000..00897d788 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/_examples/java/guides/ContextPassingTest.java @@ -0,0 +1,99 @@ +package guides; + +import io.smallrye.mutiny.Context; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class ContextPassingTest { + + void contextManipulation() { + + // tag::contextManipulation[] + // Create a context using key / value pairs + Context context = Context.of( + "X-CUSTOMER-ID", "1234", + "X-SPAN-ID", "foo-bar-baz" + ); + + // Get an entry + System.out.println( + context.get("X-SPAN-ID")); + + // Get an entry, use a supplier for a default value if the key is not present + System.out.println( + context.getOrElse("X-SPAN-ID", () -> "")); + + // Add an entry + context.put("foo", "bar"); + + // Remove an entry + context.delete("foo"); + // end::contextManipulation[] + } + + @Test + void sampleUsage() { + Multi pipeline = Multi.createFrom().range(1, 10); + String customerId = "1234"; + + // tag::contextSampleUsage[] + Context context = Context.of("X-CUSTOMER-ID", customerId); + + pipeline.withContext((multi, ctx) -> multi.onItem().transformToUniAndMerge(item -> makeRequest(item, ctx.get("X-CUSTOMER-ID")))) + .subscribe().with(context, item -> handleResponse(item), err -> handleFailure(err)); + // end::contextSampleUsage[] + } + + @Test + void sampleUsageAttachedContext() { + Multi pipeline = Multi.createFrom().range(1, 10); + String customerId = "1234"; + + // tag::contextAttachedSampleUsage[] + Context context = Context.of("X-CUSTOMER-ID", customerId); + + pipeline.attachContext() + .onItem().transformToUniAndMerge(item -> makeRequest(item.get(), item.context().get("X-CUSTOMER-ID"))) + .subscribe().with(context, item -> handleResponse(item), err -> handleFailure(err)); + // end::contextAttachedSampleUsage[] + } + + @Test + void builderUsage() { + Context context = Context.of("X-SPAN-ID", "1234"); + + // tag::builderUsage[] + Uni.createFrom().context(ctx -> makeRequest("db1", ctx.get("X-SPAN-ID"))) + .subscribe().with(context, item -> handleResponse(item), err -> handleFailure(err)); + // end::builderUsage[] + } + + @Test + void emitterUsage() { + Context context = Context.of("X-SPAN-ID", "1234"); + + // tag::emitterUsage[] + Multi.createFrom().emitter(emitter -> { + String customerId = emitter.context().get("X-SPAN-ID"); + for (int i = 0; i < 10; i++) { + emitter.emit("@" + i + " [" + customerId + "]"); + } + emitter.complete(); + }); + // end::emitterUsage[] + } + + private void handleFailure(Throwable err) { + Assertions.fail(err); + } + + private void handleResponse(String item) { + Assertions.assertTrue(item.endsWith("::1234")); + } + + private Uni makeRequest(Object item, Object o) { + return Uni.createFrom().item(item + "::" + o); + } +} diff --git a/content/docs/smallrye-mutiny/latest/_examples/java/guides/EventsTest.java b/content/docs/smallrye-mutiny/latest/_examples/java/guides/EventsTest.java new file mode 100644 index 000000000..85fd1d709 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/_examples/java/guides/EventsTest.java @@ -0,0 +1,60 @@ +package guides; + +import guides.extension.SystemOut; +import guides.extension.SystemOutCaptureExtension; +import io.smallrye.mutiny.Uni; +import org.junit.jupiter.api.Test; + +import io.smallrye.mutiny.Multi; +import org.junit.jupiter.api.extension.ExtendWith; + +import static org.awaitility.Awaitility.await; + +@ExtendWith(SystemOutCaptureExtension.class) +public class EventsTest { + + private void log(String s) { + System.out.println(s); + } + + @SuppressWarnings("Convert2MethodRef") + @Test + public void test(SystemOut out) { + // tag::code[] + Multi source = Multi.createFrom().items("a", "b", "c"); + source + .onItem() // Called for every item + .invoke(item -> log("Received item " + item)) + .onFailure() // Called on failure + .invoke(failure -> log("Failed with " + failure)) + .onCompletion() // Called when the stream completes + .invoke(() -> log("Completed")) + .onSubscription() // Called when the upstream is ready + .invoke(subscription -> log("We are subscribed!")) + .onCancellation() // Called when the downstream cancels + .invoke(() -> log("Cancelled :-(")) + .onRequest() // Called on downstream requests + .invoke(n -> log("Downstream requested " + n + " items")) + .subscribe() + .with(item -> log("Subscriber received " + item)); + // end::code[] + + // + Multi multi = Multi.createFrom().items("a", "b", "c"); + multi.invoke(item -> System.out.println("Received item " + item)); + // + + // + multi.call(item -> executeAnAsyncAction(item)); + // + + await().until(() -> out.get().contains("We are subscribed!")); + await().until(() -> out.get().contains("Downstream requested")); + await().until(() -> out.get().contains("Received item c")); + await().until(() -> out.get().contains("Completed")); + } + + private Uni executeAnAsyncAction(String item) { + return Uni.createFrom().nullItem(); + } +} diff --git a/content/docs/smallrye-mutiny/latest/_examples/java/guides/PlugTest.java b/content/docs/smallrye-mutiny/latest/_examples/java/guides/PlugTest.java new file mode 100644 index 000000000..c696891a3 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/_examples/java/guides/PlugTest.java @@ -0,0 +1,58 @@ +package guides; + +import guides.extension.SystemOut; +import guides.extension.SystemOutCaptureExtension; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.operators.multi.AbstractMultiOperator; +import io.smallrye.mutiny.operators.multi.MultiOperatorProcessor; +import io.smallrye.mutiny.subscription.MultiSubscriber; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.util.concurrent.ThreadLocalRandom; + +import static org.assertj.core.api.Assertions.assertThat; + +@SuppressWarnings("InnerClassMayBeStatic") +@ExtendWith(SystemOutCaptureExtension.class) +public class PlugTest { + + @Test + public void plug(SystemOut out) { + // tag::plug[] + Multi.createFrom() + .range(1, 101) + .plug(RandomDrop::new) + .subscribe().with(System.out::println); + // end::plug[] + + assertThat(out.get()).isNotBlank(); + } + + // tag::custom-operator[] + public class RandomDrop extends AbstractMultiOperator { + public RandomDrop(Multi upstream) { + super(upstream); + } + + @Override + public void subscribe(MultiSubscriber downstream) { + upstream.subscribe().withSubscriber(new DropProcessor(downstream)); + } + + private class DropProcessor extends MultiOperatorProcessor { + DropProcessor(MultiSubscriber downstream) { + super(downstream); + } + + @Override + public void onItem(T item) { + if (ThreadLocalRandom.current().nextBoolean()) { + super.onItem(item); + } + } + } + } + // end::custom-operator[] +} diff --git a/content/docs/smallrye-mutiny/latest/_examples/java/guides/RxTest.java b/content/docs/smallrye-mutiny/latest/_examples/java/guides/RxTest.java new file mode 100644 index 000000000..ba439fe5c --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/_examples/java/guides/RxTest.java @@ -0,0 +1,111 @@ +package guides; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class RxTest { + + @Test + public void rx() { + Multi multi = Multi.createFrom().range(1, 3); + Uni uni = Uni.createFrom().item(1); + // tag::rx[] + + int result = uni + .map(i -> i + 1) + .await().indefinitely(); + + int result2 = uni + .flatMap(i -> Uni.createFrom().item(i + 1)) + .await().indefinitely(); + + List list = multi + .map(i -> i + 1) + .collect().asList() + .await().indefinitely(); + + List list2 = multi + .flatMap(i -> Multi.createFrom().items(i, i)) + .collect().asList() + .await().indefinitely(); + + List list3 = multi + .concatMap(i -> Multi.createFrom().items(i, i)) + .collect().asList() + .await().indefinitely(); + + // end::rx[] + assertThat(result).isEqualTo(2); + assertThat(result2).isEqualTo(2); + assertThat(list).containsExactly(2, 3); + assertThat(list2).containsExactly(1, 1, 2, 2); + assertThat(list3).containsExactly(1, 1, 2, 2); + } + + @Test + public void mutiny() { + Multi multi = Multi.createFrom().range(1, 3); + Uni uni = Uni.createFrom().item(1); + // tag::mutiny[] + + int result = uni + .onItem().transform(i -> i + 1) + .await().indefinitely(); + + int result2 = uni + .onItem().transformToUni(i -> Uni.createFrom().item(i + 1)) + .await().indefinitely(); + + // Shortcut for .onItem().transformToUni + int result3 = uni + .chain(i -> Uni.createFrom().item(i + 1)) + .await().indefinitely(); + + List list = multi + .onItem().transform(i -> i + 1) + .collect().asList() + .await().indefinitely(); + + List list2 = multi + .onItem().transformToMultiAndMerge(i -> Multi.createFrom().items(i, i)) + .collect().asList() + .await().indefinitely(); + + // Equivalent to transformToMultiAndMerge but let you configure the flattening process, + // failure management, concurrency... + List list3 = multi + .onItem().transformToMulti(i -> Multi.createFrom().items(i, i)).merge() + .collect().asList() + .await().indefinitely(); + + List list4 = multi + .onItem().transformToMultiAndConcatenate(i -> Multi.createFrom().items(i, i)) + .collect().asList() + .await().indefinitely(); + + // Equivalent to transformToMultiAndConcatenate but let you configure the flattening process, + // failure management... + List list5 = multi + .onItem().transformToMulti(i -> Multi.createFrom().items(i, i)).concatenate() + .collect().asList() + .await().indefinitely(); + + + // end::mutiny[] + assertThat(result).isEqualTo(2); + assertThat(result2).isEqualTo(2); + assertThat(result3).isEqualTo(2); + assertThat(list).containsExactly(2, 3); + assertThat(list2).containsExactly(1, 1, 2, 2); + assertThat(list3).containsExactly(1, 1, 2, 2); + assertThat(list4).containsExactly(1, 1, 2, 2); + assertThat(list5).containsExactly(1, 1, 2, 2); + } + + +} diff --git a/content/docs/smallrye-mutiny/latest/_examples/java/guides/ShortcutsTest.java b/content/docs/smallrye-mutiny/latest/_examples/java/guides/ShortcutsTest.java new file mode 100644 index 000000000..1d08c9982 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/_examples/java/guides/ShortcutsTest.java @@ -0,0 +1,29 @@ +package guides; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import org.junit.jupiter.api.Test; + +public class ShortcutsTest { + + + @Test + public void test() { + + // tag::invoke[] + Multi multi = Multi.createFrom().items("a", "b", "c"); + multi.invoke(item -> System.out.println("Received item " + item)); + // end::invoke[] + + // tag::call[] + multi.call(item -> executeAnAsyncAction(item)); + // end::call[] + + multi.subscribe().with(x -> {}); + } + + private Uni executeAnAsyncAction(String item) { + return Uni.createFrom().nullItem(); + } + +} diff --git a/content/docs/smallrye-mutiny/latest/_examples/java/guides/SpiesTest.java b/content/docs/smallrye-mutiny/latest/_examples/java/guides/SpiesTest.java new file mode 100644 index 000000000..bc2c297dc --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/_examples/java/guides/SpiesTest.java @@ -0,0 +1,40 @@ +package guides; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.helpers.spies.MultiGlobalSpy; +import io.smallrye.mutiny.helpers.spies.MultiOnCompletionSpy; +import io.smallrye.mutiny.helpers.spies.MultiOnRequestSpy; +import io.smallrye.mutiny.helpers.spies.Spy; +import org.junit.jupiter.api.Test; + +public class SpiesTest { + + @Test + public void trackItems() { + // tag::selected[] + Multi multi = Multi.createFrom().items(1, 2, 3); + MultiOnRequestSpy requestSpy = Spy.onRequest(multi); + MultiOnCompletionSpy completionSpy = Spy.onCompletion(requestSpy); + + completionSpy.subscribe().with(System.out::println); + + System.out.println("Number of requests: " + requestSpy.requestedCount()); + System.out.println("Completed? " + completionSpy.invoked()); + // end::selected[] + } + + @Test + public void trackEverything() { + // tag::global[] + Multi multi = Multi.createFrom().items(1, 2, 3); + MultiGlobalSpy spy = Spy.globally(multi); + + spy.subscribe().with(System.out::println); + + System.out.println("Number of requests: " + spy.onRequestSpy().requestedCount()); + System.out.println("Cancelled? " + spy.onCancellationSpy().isCancelled()); + System.out.println("Failure? " + spy.onFailureSpy().lastFailure()); + System.out.println("Items: " + spy.onItemSpy().items()); + // end::global[] + } +} \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/latest/_examples/java/guides/TestSubscribersTest.java b/content/docs/smallrye-mutiny/latest/_examples/java/guides/TestSubscribersTest.java new file mode 100644 index 000000000..467bd8602 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/_examples/java/guides/TestSubscribersTest.java @@ -0,0 +1,131 @@ +package guides; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.helpers.test.AssertMulti; +import io.smallrye.mutiny.helpers.test.AssertSubscriber; +import io.smallrye.mutiny.helpers.test.UniAssertSubscriber; +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +public class TestSubscribersTest { + + @Test + void uni() { + // tag::uni[] + Uni uni = Uni.createFrom().item(63); + + UniAssertSubscriber subscriber = uni + .subscribe().withSubscriber(UniAssertSubscriber.create()); + + subscriber + .awaitItem() + .assertItem(63); + // end::uni[] + } + + @Test + void multi() { + // tag::multi[] + Multi multi = Multi.createFrom().range(1, 5) + .onItem().transform(n -> n * 10); + + AssertSubscriber subscriber = multi.subscribe().withSubscriber(AssertSubscriber.create(10)); + + subscriber + .awaitCompletion() + .assertItems(10, 20, 30, 40); + // end::multi[] + } + + @Test + void failing() { + // tag::failing[] + Multi multi = Multi.createFrom().failure(() -> new IOException("Boom")); + + AssertSubscriber subscriber = multi + .subscribe().withSubscriber(AssertSubscriber.create(10)); + + subscriber + .awaitFailure() + .assertFailedWith(IOException.class, "Boom"); + // end::failing[] + } + + @Test + void uniPredicate() { + // tag::uni-predicate[] + Uni uni = Uni.createFrom().item(63); + + UniAssertSubscriber subscriber = uni + .subscribe().withSubscriber(UniAssertSubscriber.create()); + + subscriber + .awaitItem() + .assertItem(i -> i > 0, "positive number") + .inspectItem(item -> { + // use any assertion library here + assert item == 63; + }); + // end::uni-predicate[] + } + + @Test + void multiPredicate() { + // tag::multi-predicate[] + Multi multi = Multi.createFrom().range(1, 5) + .onItem().transform(n -> n * 10); + + AssertSubscriber subscriber = multi + .subscribe().withSubscriber(AssertSubscriber.create(10)); + + subscriber + .awaitCompletion() + .assertLastItem(i -> i == 40, "last item is 40") + .assertItemCount(4) + .inspectItems(items -> { + // use any assertion library here + assert items.size() == 4; + }); + // end::multi-predicate[] + } + + @Test + void assertMulti() { + // tag::assert-multi[] + Multi multi = Multi.createFrom().range(1, 6); + + AssertMulti.create(multi) + .expectNext(1, 2, 3) + .expectNextMatches(i -> i > 3, "greater than 3") + .expectNext(5) + .expectComplete() + .verify(); + // end::assert-multi[] + } + + @Test + void assertMultiFailure() { + // tag::assert-multi-failure[] + Multi multi = Multi.createFrom().failure(new IOException("boom")); + + AssertMulti.create(multi) + .expectFailure(IOException.class, "boom") + .verify(); + // end::assert-multi-failure[] + } + + @Test + void assertMultiDemand() { + // tag::assert-multi-demand[] + Multi multi = Multi.createFrom().range(1, 4); + + AssertMulti.create(multi) + .thenRequest(10) + .expectNext(1, 2, 3) + .expectComplete() + .verify(); + // end::assert-multi-demand[] + } +} diff --git a/content/docs/smallrye-mutiny/latest/_examples/java/guides/UncheckedTest.java b/content/docs/smallrye-mutiny/latest/_examples/java/guides/UncheckedTest.java new file mode 100644 index 000000000..1d15e5bd4 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/_examples/java/guides/UncheckedTest.java @@ -0,0 +1,69 @@ +package guides; + +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.unchecked.Unchecked; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.io.UncheckedIOException; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@SuppressWarnings("Convert2MethodRef") +public class UncheckedTest { + + @Test + public void rethrow() { + Uni item = Uni.createFrom().item(1); + + // tag::rethrow[] + Uni uni = item.onItem().transform(i -> { + try { + return methodThrowingIoException(i); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + // end::rethrow[] + + assertThatThrownBy(() -> uni.await().indefinitely()) + .isInstanceOf(RuntimeException.class).hasCauseInstanceOf(IOException.class); + + } + + @Test + public void transformExample() { + Uni item = Uni.createFrom().item(1); + + // tag::transform[] + Uni uni = item.onItem().transform(Unchecked.function(i -> { + // Can throw checked exception + return methodThrowingIoException(i); + })); + // end::transform[] + + assertThatThrownBy(() -> uni.await().indefinitely()) + .isInstanceOf(RuntimeException.class).hasCauseInstanceOf(IOException.class); + + } + + @Test + public void invokeExample() { + Uni item = Uni.createFrom().item(1); + + // tag::invoke[] + Uni uni = item.onItem().invoke(Unchecked.consumer(i -> { + // Can throw checked exception + throw new IOException("boom"); + })); + // end::invoke[] + + assertThatThrownBy(() -> uni.await().indefinitely()) + .isInstanceOf(RuntimeException.class).hasCauseInstanceOf(IOException.class); + + } + + private int methodThrowingIoException(Integer ignored) throws IOException { + throw new IOException("boom"); + } +} diff --git a/content/docs/smallrye-mutiny/latest/_examples/java/guides/UniMultiComparisonTest.java b/content/docs/smallrye-mutiny/latest/_examples/java/guides/UniMultiComparisonTest.java new file mode 100644 index 000000000..d1df9017c --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/_examples/java/guides/UniMultiComparisonTest.java @@ -0,0 +1,63 @@ +package guides; + +import guides.extension.SystemOut; +import guides.extension.SystemOutCaptureExtension; +import org.junit.jupiter.api.Test; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import org.junit.jupiter.api.extension.ExtendWith; + +import static org.awaitility.Awaitility.await; + +@ExtendWith(SystemOutCaptureExtension.class) +public class UniMultiComparisonTest { + + @Test + public void comparison(SystemOut out) { + //tag::code[] + Multi.createFrom().items("a", "b", "c") + .onItem().transform(String::toUpperCase) + .subscribe().with( + item -> System.out.println("Received: " + item), + failure -> System.out.println("Failed with " + failure) + ); + + Uni.createFrom().item("a") + .onItem().transform(String::toUpperCase) + .subscribe().with( + item -> System.out.println("Received: " + item), + failure -> System.out.println("Failed with " + failure) + ); + //end::code[] + + await().until(() -> out.get().contains("Received: C")); + } + + @Test + public void conversion(SystemOut out) { + // + Multi.createFrom().items("a", "b", "c") + .onItem().transform(String::toUpperCase) + // Convert the multi to uni + // It only emits the first item ("a") + .toUni() + .subscribe().with( + item -> System.out.println("Received: " + item), + failure -> System.out.println("Failed with " + failure)); + + Uni.createFrom().item("a") + .onItem().transform(String::toUpperCase) + // Convert the uni to a multi, + // the completion event will be fired after "a": + .toMulti() + .subscribe().with( + item -> System.out.println("Received: " + item), + failure -> System.out.println("Failed with " + failure) + ); + + // + + await().until(() -> out.get().contains("Received: A")); + } +} diff --git a/content/docs/smallrye-mutiny/latest/_examples/java/guides/UniNullTest.java b/content/docs/smallrye-mutiny/latest/_examples/java/guides/UniNullTest.java new file mode 100644 index 000000000..53ddbeb02 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/_examples/java/guides/UniNullTest.java @@ -0,0 +1,48 @@ +package guides; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class UniNullTest { + + @Test + public void uni() { + Uni uni = Uni.createFrom().item(() -> null); + // tag::code[] + uni.onItem().ifNull().continueWith("hello"); + uni.onItem().ifNull().switchTo(() -> Uni.createFrom().item("hello")); + uni.onItem().ifNull().failWith(() -> new Exception("Boom!")); + // end::code[] + + assertThat(uni.onItem().ifNull().continueWith("hello").await().indefinitely()).isEqualTo("hello"); + } + + @Test + public void uniNotNull() { + Uni uni = Uni.createFrom().item(() -> null); + // tag::code-not-null[] + uni + .onItem().ifNotNull().transform(String::toUpperCase) + .onItem().ifNull().continueWith("yolo!"); + // end::code-not-null[] + + String r = uni + .onItem().ifNotNull().transform(String::toUpperCase) + .onItem().ifNull().continueWith("yolo!") + .await().indefinitely(); + assertThat(r).isEqualTo("yolo!"); + } + + @Test + public void accumulate() { + Multi multi = Multi.createFrom().range(1, 3); + // + Multi added = multi.onItem().scan(() -> 0, (item, acc) -> acc + item); + // + assertThat(added.subscribe().asIterable()).containsExactly(0, 1, 3); + + } +} diff --git a/content/docs/smallrye-mutiny/latest/_examples/java/guides/UniTimeoutTest.java b/content/docs/smallrye-mutiny/latest/_examples/java/guides/UniTimeoutTest.java new file mode 100644 index 000000000..3dcd602ca --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/_examples/java/guides/UniTimeoutTest.java @@ -0,0 +1,102 @@ +package guides; + +import io.smallrye.mutiny.TimeoutException; +import io.smallrye.mutiny.Uni; +import org.junit.jupiter.api.Test; + +import javax.naming.ServiceUnavailableException; +import java.time.Duration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@SuppressWarnings("Convert2MethodRef") +public class UniTimeoutTest { + + @Test + public void test() { + Uni uni = Uni.createFrom().nothing(); + // tag::code[] + Uni uniWithTimeout = uni + .ifNoItem().after(Duration.ofMillis(100)) + .recoverWithItem("some fallback item"); + // end::code[] + String item = uniWithTimeout + .await().indefinitely(); + assertThat(item).isEqualTo("some fallback item"); + } + + @Test + public void testFail() { + Uni uni = Uni.createFrom().nothing(); + // tag::fail[] + Uni uniWithTimeout = uni + .ifNoItem().after(Duration.ofMillis(100)).fail(); + // end::fail[] + assertThatThrownBy(() -> + uniWithTimeout + .await().indefinitely() + ).isInstanceOf(TimeoutException.class); + } + + @Test + public void testFailRecover() { + Uni uni = Uni.createFrom().nothing(); + // tag::fail-recover[] + Uni uniWithTimeout = uni + .ifNoItem().after(Duration.ofMillis(100)).fail() + .onFailure(TimeoutException.class).recoverWithItem("we got a timeout"); + // end::fail-recover[] + String item = uniWithTimeout + .await().indefinitely(); + assertThat(item).isEqualTo("we got a timeout"); + } + + @Test + public void testFailWithSpecificException() { + Uni uni = Uni.createFrom().nothing(); + // tag::fail-with[] + Uni uniWithTimeout = uni + .ifNoItem().after(Duration.ofMillis(100)).failWith(() -> new ServiceUnavailableException()); + // end::fail-with[] + assertThatThrownBy(() -> + uniWithTimeout + .await().indefinitely() + ).isInstanceOf(ServiceUnavailableException.class); + } + + @Test + public void testFallback() { + Uni uni = Uni.createFrom().nothing(); + // tag::fallback[] + Uni uniWithTimeout = uni + .ifNoItem().after(Duration.ofMillis(100)).recoverWithItem(() -> "fallback"); + // end::fallback[] + String item = uniWithTimeout + .await().indefinitely(); + assertThat(item).isEqualTo("fallback"); + } + + @Test + public void testFallbackUni() { + Uni uni = Uni.createFrom().nothing(); + // tag::fallback-uni[] + Uni uniWithTimeout = uni + .ifNoItem().after(Duration.ofMillis(100)).recoverWithUni(() -> someFallbackUni()); + // end::fallback-uni[] + String item = uniWithTimeout + .await().indefinitely(); + assertThat(item).isEqualTo("fallback"); + } + + private Uni someFallbackUni() { + return Uni.createFrom().item("fallback"); + } + + + private static class ServiceUnavailableException extends RuntimeException { + + } + + +} diff --git a/content/docs/smallrye-mutiny/latest/_examples/java/guides/infrastructure/DroppedExceptionTest.java b/content/docs/smallrye-mutiny/latest/_examples/java/guides/infrastructure/DroppedExceptionTest.java new file mode 100644 index 000000000..980ecd9b7 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/_examples/java/guides/infrastructure/DroppedExceptionTest.java @@ -0,0 +1,54 @@ +package guides.infrastructure; + +import guides.extension.SystemOut; +import guides.extension.SystemOutCaptureExtension; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.infrastructure.Infrastructure; +import io.smallrye.mutiny.subscription.Cancellable; +import io.smallrye.mutiny.subscription.UniEmitter; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; + +@ExtendWith(SystemOutCaptureExtension.class) +public class DroppedExceptionTest { + + @Test + public void droppedExceptionTest(SystemOut out) { + // tag::override-handler[] + Infrastructure.setDroppedExceptionHandler(err -> + log(Level.SEVERE, "Mutiny dropped exception", err) + ); + // end::override-handler[] + + // tag::code[] + Cancellable cancellable = Uni.createFrom() + .emitter(this::emitter) + .onCancellation().call(() -> Uni.createFrom().failure(new IOException("boom"))) + .subscribe().with(this::onItem, this::onFailure); + + cancellable.cancel(); + // end::code[] + Assertions.assertThat(out.get()).contains("Mutiny dropped exception"); + } + + private void log(Level severe, String message, Throwable err) { + System.out.println(severe.getName() + ":" + message + " " + err.getMessage()); + } + + private void onFailure(Throwable t) { + // Nothing, the snippet is for pure documentation purposes + } + + private void onItem(Object o) { + // Nothing, the snippet is for pure documentation purposes + } + + private void emitter(UniEmitter uniEmitter) { + // Nothing, the snippet is for pure documentation purposes + } +} diff --git a/content/docs/smallrye-mutiny/latest/_examples/java/guides/infrastructure/InfrastructureTest.java b/content/docs/smallrye-mutiny/latest/_examples/java/guides/infrastructure/InfrastructureTest.java new file mode 100644 index 000000000..61da6e036 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/_examples/java/guides/infrastructure/InfrastructureTest.java @@ -0,0 +1,39 @@ +package guides.infrastructure; + +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.infrastructure.Infrastructure; +import io.smallrye.mutiny.infrastructure.MutinyScheduler; +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.util.concurrent.Executor; + +import static org.assertj.core.api.Assertions.assertThat; + +public class InfrastructureTest { + + @Test + public void test() { + // tag::infra[] + Uni uni1 = Uni.createFrom().item(1) + .emitOn(Infrastructure.getDefaultExecutor()); + + Uni uni2 = Uni.createFrom().item(2) + .onItem().delayIt() + .onExecutor(Infrastructure.getDefaultWorkerPool()) + .by(Duration.ofMillis(10)); + // end::infra[] + + assertThat(uni1.await().indefinitely()).isEqualTo(1); + assertThat(uni2.await().indefinitely()).isEqualTo(2); + + + Executor executor = Runnable::run; + // tag::set-infra[] + Infrastructure.setDefaultExecutor(executor); + // end::set-infra[] + + assertThat(Infrastructure.getDefaultExecutor()).isEqualTo(executor); + assertThat(Infrastructure.getDefaultWorkerPool()).isInstanceOf(MutinyScheduler.class); + } +} diff --git a/content/docs/smallrye-mutiny/latest/_examples/java/guides/infrastructure/OperatorLoggingTest.java b/content/docs/smallrye-mutiny/latest/_examples/java/guides/infrastructure/OperatorLoggingTest.java new file mode 100644 index 000000000..32746b70a --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/_examples/java/guides/infrastructure/OperatorLoggingTest.java @@ -0,0 +1,36 @@ +package guides.infrastructure; + +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.infrastructure.Infrastructure; + +public class OperatorLoggingTest { + + @Test + public void test() { + // tag::set-logger[] + Infrastructure.setOperatorLogger((id, event, value, err) -> { + Logger logger = LoggerFactory.getLogger(id); + if (err != null) { + logger.info(event + "(" + err.getClass() + "(" + err.getMessage() + "))"); + } else { + if (value != null) { + logger.info(event + "(" + value + ")"); + } else { + logger.info(event + "()"); + } + } + }); + // end::set-logger[] + + // tag::log[] + Multi.createFrom().items(1, 2, 3) + .onItem().transform(n -> n * 10) + .log() + .subscribe().with(item -> System.out.println(">>> " + item)); + // end::log[] + } +} diff --git a/content/docs/smallrye-mutiny/latest/_examples/java/guides/integration/ImperativeToReactiveTest.java b/content/docs/smallrye-mutiny/latest/_examples/java/guides/integration/ImperativeToReactiveTest.java new file mode 100644 index 000000000..cc4484456 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/_examples/java/guides/integration/ImperativeToReactiveTest.java @@ -0,0 +1,60 @@ +package guides.integration; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.infrastructure.Infrastructure; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ImperativeToReactiveTest { + + + @Test + public void uniRunSubscriptionOn() { + // tag::uni-runSubscriptionOn[] + Uni uni = Uni.createFrom() + .item(this::invokeRemoteServiceUsingBlockingIO) + .runSubscriptionOn(Infrastructure.getDefaultWorkerPool()); + // end::uni-runSubscriptionOn[] + + String res = uni.await().indefinitely(); + assertThat(res).isEqualTo("Hello"); + } + + @Test + public void multiEmitOn() { + // tag::multi-emitOn[] + Multi multi = Multi.createFrom().items("john", "jack", "sue") + .emitOn(Infrastructure.getDefaultWorkerPool()) + .onItem().transform(this::invokeRemoteServiceUsingBlockingIO); + // end::multi-emitOn[] + + List strings = multi.subscribe().asStream().collect(Collectors.toList()); + assertThat(strings).containsExactly("JOHN", "JACK", "SUE"); + } + + public String invokeRemoteServiceUsingBlockingIO() { + assertThat(Thread.currentThread().getName()).doesNotContain("main"); + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + return "Hello"; + } + + public String invokeRemoteServiceUsingBlockingIO(String s) { + assertThat(Thread.currentThread().getName()).doesNotContain("main"); + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + return s.toUpperCase(); + } + +} diff --git a/content/docs/smallrye-mutiny/latest/_examples/java/guides/integration/ReactiveToImperativeTest.java b/content/docs/smallrye-mutiny/latest/_examples/java/guides/integration/ReactiveToImperativeTest.java new file mode 100644 index 000000000..d4cb2397e --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/_examples/java/guides/integration/ReactiveToImperativeTest.java @@ -0,0 +1,57 @@ +package guides.integration; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; + +@SuppressWarnings("unchecked") +public class ReactiveToImperativeTest { + + @Test + public void testAwait() { + Uni uni = (Uni) Uni.createFrom().item(() -> 1); + + // tag::await[] + T t = uni.await().indefinitely(); + // end::await[] + + assertThat(t).isEqualTo(1); + } + + @Test + public void testAwaitAtMost() { + Uni uni = (Uni) Uni.createFrom().item(() -> 1); + + // tag::atMost[] + T t = uni.await().atMost(Duration.ofSeconds(1)); + // end::atMost[] + + assertThat(t).isEqualTo(1); + } + + @Test + public void test() { + Multi multi = (Multi) Multi.createFrom().items(1, 2, 3); + + // tag::iterable[] + Iterable iterable = multi.subscribe().asIterable(); + for (T item : iterable) { + doSomethingWithItem(item); + } + // end::iterable[] + + // tag::stream[] + Stream stream = multi.subscribe().asStream(); + stream.forEach(this::doSomethingWithItem); + // end::stream[] + } + + private void doSomethingWithItem(T item) { + assertThat(item).isIn(1, 2, 3); + } +} diff --git a/content/docs/smallrye-mutiny/latest/_examples/java/guides/integration/ReactorTest.java b/content/docs/smallrye-mutiny/latest/_examples/java/guides/integration/ReactorTest.java new file mode 100644 index 000000000..83f629379 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/_examples/java/guides/integration/ReactorTest.java @@ -0,0 +1,158 @@ +package guides.integration; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.converters.multi.MultiReactorConverters; +import io.smallrye.mutiny.converters.uni.UniReactorConverters; +import mutiny.zero.flow.adapters.AdaptersToFlow; +import mutiny.zero.flow.adapters.AdaptersToReactiveStreams; +import org.junit.jupiter.api.Test; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.List; +import java.util.concurrent.Flow; + +import static org.assertj.core.api.Assertions.assertThat; + +@SuppressWarnings("unchecked") +public class ReactorTest { + + @Test + public void testMultiCreation() { + Flux flux = getFlux(); + Mono mono = getMono(); + + // tag::reactor-multi-create[] + Flow.Publisher fluxAsPublisher = AdaptersToFlow.publisher(flux); + Multi multiFromFlux = Multi.createFrom().publisher(fluxAsPublisher); + + Flow.Publisher monoAsPublisher = AdaptersToFlow.publisher(mono); + Multi multiFromMono = Multi.createFrom().publisher(monoAsPublisher); + // end::reactor-multi-create[] + + List list = multiFromFlux + .onItem().transform(Object::toString) + .collect().asList().await().indefinitely(); + assertThat(list).containsExactly("a", "b", "c"); + + list = multiFromMono + .onItem().transform(Object::toString) + .collect().asList().await().indefinitely(); + assertThat(list).containsExactly("a"); + + } + + @Test + public void testUniCreation() { + Flux flux = getFlux(); + Mono mono = getMono(); + + // tag::reactor-uni-create[] + Flow.Publisher fluxAsPublisher = AdaptersToFlow.publisher(flux); + Uni uniFromFlux = Uni.createFrom().publisher(fluxAsPublisher); + + Flow.Publisher monoAsPublisher = AdaptersToFlow.publisher(mono); + Uni uniFromMono = Uni.createFrom().publisher(monoAsPublisher); + // end::reactor-uni-create[] + + String s = uniFromFlux + .onItem().transform(Object::toString) + .await().indefinitely(); + assertThat(s).isEqualTo("a"); + + s = uniFromMono + .onItem().transform(Object::toString) + .await().indefinitely(); + assertThat(s).isEqualTo("a"); + } + + @Test + public void testCreationFromMulti() { + Multi multi = getMulti(); + + // tag::reactor-create-multi[] + Publisher multiAsLegacyPublisher = AdaptersToReactiveStreams.publisher(multi); + + Flux fluxFromMulti = Flux.from(multiAsLegacyPublisher); + Mono monoFromMulti = Mono.from(multiAsLegacyPublisher); + // end::reactor-create-multi[] + + List list = fluxFromMulti + .map(Object::toString) + .collectList() + .block(); + assertThat(list).containsExactly("a", "b", "c"); + + String s = monoFromMulti + .map(Object::toString) + .block(); + assertThat(s).isEqualTo("a"); + } + + @Test + public void testCreationFromUni() { + Uni uni = getUni(); + + // tag::reactor-create-uni[] + Flux fluxFromUni = uni.convert().with(UniReactorConverters.toFlux()); + Mono monoFromUni = uni.convert().with(UniReactorConverters.toMono()); + // end::reactor-create-uni[] + + List list = fluxFromUni + .map(Object::toString) + .collectList() + .block(); + assertThat(list).containsExactly("a"); + + String s = monoFromUni + .map(Object::toString) + .block(); + assertThat(s).isEqualTo("a"); + } + + private Flux getFlux() { + return Flux.just("a", "b", "c") + .map(s -> (T) s); + } + + private Mono getMono() { + return Mono.just("a") + .map(s -> (T) s); + } + + private Multi getMulti() { + return Multi.createFrom().items("a", "b", "c") + .map(s -> (T) s); + } + + private Uni getUni() { + return Uni.createFrom().item("a") + .map(s -> (T) s); + } + + @Test + public void uniExportToReactor() { + Uni uni = Uni.createFrom().item("hello"); + // tag::uni-export[] + Mono mono = uni.convert().with(UniReactorConverters.toMono()); + Flux flux = uni.convert().with(UniReactorConverters.toFlux()); + // end::uni-export[] + + assertThat(mono.block()).isEqualTo("hello"); + assertThat(flux.blockFirst()).isEqualTo("hello"); + } + + @Test + public void multiExportToReactor() { + Multi multi = Multi.createFrom().items("hello", "bonjour"); + // tag::multi-export[] + Mono mono = multi.convert().with(MultiReactorConverters.toMono()); + Flux flux = multi.convert().with(MultiReactorConverters.toFlux()); + // end::multi-export[] + + assertThat(mono.block()).isEqualTo("hello"); + assertThat(flux.toIterable()).containsExactly("hello", "bonjour"); + } +} diff --git a/content/docs/smallrye-mutiny/latest/_examples/java/guides/integration/RxJavaTest.java b/content/docs/smallrye-mutiny/latest/_examples/java/guides/integration/RxJavaTest.java new file mode 100644 index 000000000..dd1ae117c --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/_examples/java/guides/integration/RxJavaTest.java @@ -0,0 +1,277 @@ +package guides.integration; + +import io.reactivex.rxjava3.core.*; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.converters.multi.MultiRx3Converters; +import io.smallrye.mutiny.converters.uni.UniRx3Converters; +import mutiny.zero.flow.adapters.AdaptersToFlow; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Optional; +import java.util.concurrent.Flow; + +import static org.assertj.core.api.Assertions.assertThat; + +@SuppressWarnings("unchecked") +public class RxJavaTest { + + @Test + public void testMultiCreation() { + Observable observable = getObservable(); + Flowable flowable = getFlowable(); + + // tag::rx-multi-create-observable[] + Multi multiFromObservable = Multi.createFrom() + .converter(MultiRx3Converters.fromObservable(), observable); + // end::rx-multi-create-observable[] + + // tag::rx-multi-create-flowable[] + Flow.Publisher flowableAsPublisher = AdaptersToFlow.publisher(flowable); + + Multi multiFromFlowable = Multi.createFrom().publisher(flowableAsPublisher); + // end::rx-multi-create-flowable[] + + List list = multiFromObservable + .onItem().transform(Object::toString) + .collect().asList().await().indefinitely(); + assertThat(list).containsExactly("a", "b", "c"); + + list = multiFromFlowable + .onItem().transform(Object::toString) + .collect().asList().await().indefinitely(); + assertThat(list).containsExactly("a", "b", "c"); + + Completable completable = getCompletable(); + Single single = getSingle(); + Maybe maybe = getMaybe(); + Maybe emptyMaybe = getEmptyMaybe(); + + // tag::rx-multi-create-single[] + Multi multiFromCompletable = Multi.createFrom() + .converter(MultiRx3Converters.fromCompletable(), completable); + Multi multiFromSingle = Multi.createFrom() + .converter(MultiRx3Converters.fromSingle(), single); + Multi multiFromMaybe = Multi.createFrom() + .converter(MultiRx3Converters.fromMaybe(), maybe); + Multi multiFromEmptyMaybe = Multi.createFrom() + .converter(MultiRx3Converters.fromMaybe(), emptyMaybe); + // end::rx-multi-create-single[] + + list = multiFromCompletable + .onItem().transform(Object::toString) + .collect().asList().await().indefinitely(); + assertThat(list).isEmpty(); + + list = multiFromSingle + .onItem().transform(Object::toString) + .collect().asList().await().indefinitely(); + assertThat(list).containsExactly("a"); + + list = multiFromMaybe + .onItem().transform(Object::toString) + .collect().asList().await().indefinitely(); + assertThat(list).containsExactly("a"); + + list = multiFromEmptyMaybe + .onItem().transform(Object::toString) + .collect().asList().await().indefinitely(); + assertThat(list).isEmpty(); + } + + @Test + public void testUniCreation() { + Observable observable = getObservable(); + Flowable flowable = getFlowable(); + + // tag::rx-uni-create-observable[] + Uni uniFromObservable = Uni.createFrom().converter( + UniRx3Converters.fromObservable(), observable); + // end::rx-uni-create-observable[] + + // tag::rx-uni-create-flowable[] + Flow.Publisher flowableAsPublisher = AdaptersToFlow.publisher(flowable); + + Uni uniFromFlowable = Uni.createFrom().publisher(flowableAsPublisher); + // end::rx-uni-create-flowable[] + + String s = uniFromFlowable + .onItem().transform(Object::toString) + .await().indefinitely(); + assertThat(s).isEqualTo("a"); + + s = uniFromObservable + .onItem().transform(Object::toString) + .await().indefinitely(); + assertThat(s).isEqualTo("a"); + + Completable completable = getCompletable(); + Single single = getSingle(); + Maybe maybe = getMaybe(); + Maybe emptyMaybe = getEmptyMaybe(); + + // tag::rx-uni-create-single[] + Uni multiFromCompletable = Uni.createFrom() + .converter(UniRx3Converters.fromCompletable(), completable); + Uni multiFromSingle = Uni.createFrom() + .converter(UniRx3Converters.fromSingle(), single); + Uni multiFromMaybe = Uni.createFrom() + .converter(UniRx3Converters.fromMaybe(), maybe); + Uni multiFromEmptyMaybe = Uni.createFrom() + .converter(UniRx3Converters.fromMaybe(), emptyMaybe); + // end::rx-uni-create-single[] + + Void v = multiFromCompletable + .await().indefinitely(); + assertThat(v).isNull(); + + s = multiFromSingle + .onItem().transform(Object::toString) + .await().indefinitely(); + assertThat(s).isEqualTo("a"); + + s = multiFromMaybe + .onItem().transform(Object::toString) + .await().indefinitely(); + assertThat(s).isEqualTo("a"); + + s = multiFromEmptyMaybe + .onItem().castTo(String.class) + .await().indefinitely(); + assertThat(s).isNull(); + } + + @Test + public void testCreatingRxFromMulti() { + Multi multi = getMulti(); + + // tag::create-rx-from-multi[] + Completable completable = multi.convert() + .with(MultiRx3Converters.toCompletable()); + Single> single = multi.convert() + .with(MultiRx3Converters.toSingle()); + Single single2 = multi.convert() + .with(MultiRx3Converters + .toSingle().onEmptyThrow(() -> new Exception("D'oh!"))); + Maybe maybe = multi.convert() + .with(MultiRx3Converters.toMaybe()); + Observable observable = multi.convert() + .with(MultiRx3Converters.toObservable()); + Flowable flowable = multi.convert() + .with(MultiRx3Converters.toFlowable()); + // end::create-rx-from-multi[] + + completable.blockingAwait(); + assertThat(single.blockingGet()).contains((T) "a"); + assertThat(single2.blockingGet()).isEqualTo("a"); + assertThat(maybe.blockingGet()).isEqualTo("a"); + assertThat(observable.blockingIterable()).containsExactly((T) "a", (T) "b", (T) "c"); + assertThat(flowable.blockingIterable()).containsExactly((T) "a", (T) "b", (T) "c"); + } + + @Test + public void testCreatingRxFromUni() { + Uni uni = getUni(); + + // tag::create-rx-from-uni[] + Completable completable = uni.convert().with(UniRx3Converters.toCompletable()); + Single> single = uni.convert().with(UniRx3Converters.toSingle()); + Single single2 = uni.convert().with(UniRx3Converters.toSingle().failOnNull()); + Maybe maybe = uni.convert().with(UniRx3Converters.toMaybe()); + Observable observable = uni.convert().with(UniRx3Converters.toObservable()); + Flowable flowable = uni.convert().with(UniRx3Converters.toFlowable()); + // end::create-rx-from-uni[] + + completable.blockingAwait(); + assertThat(single.blockingGet()).contains((T) "a"); + assertThat(single2.blockingGet()).isEqualTo("a"); + assertThat(maybe.blockingGet()).isEqualTo("a"); + assertThat(observable.blockingIterable()).containsExactly((T) "a"); + assertThat(flowable.blockingIterable()).containsExactly((T) "a"); + } + + @Test + public void uniExportToRx() { + Uni uni = Uni.createFrom().item("hello"); + // + Completable completable = uni.convert().with(UniRx3Converters.toCompletable()); + Single> single = uni.convert().with(UniRx3Converters.toSingle()); + Single single2 = uni.convert().with(UniRx3Converters.toSingle().failOnNull()); + Maybe maybe = uni.convert().with(UniRx3Converters.toMaybe()); + Observable observable = uni.convert().with(UniRx3Converters.toObservable()); + Flowable flowable = uni.convert().with(UniRx3Converters.toFlowable()); + // + + completable.test().assertComplete(); + single.test().assertValue(o -> o.isPresent() && o.get().equals("hello")); + single2.test().assertValue("hello"); + maybe.test().assertValue("hello"); + observable.test().assertValue("hello").assertComplete(); + flowable.test().assertValue("hello").assertComplete(); + } + + @Test + public void multiExportToRx() { + Multi multi = Multi.createFrom().items("hello", "bonjour"); + // + Completable completable = multi.convert() + .with(MultiRx3Converters.toCompletable()); + Single> single = multi.convert() + .with(MultiRx3Converters.toSingle()); + Single single2 = multi.convert() + .with(MultiRx3Converters.toSingle().onEmptyThrow(() -> new Exception("D'oh!"))); + Maybe maybe = multi.convert() + .with(MultiRx3Converters.toMaybe()); + Observable observable = multi.convert() + .with(MultiRx3Converters.toObservable()); + Flowable flowable = multi.convert() + .with(MultiRx3Converters.toFlowable()); + // + + completable.test().assertComplete(); + single.test().assertValue(o -> o.isPresent() && o.get().equals("hello")); + single2.test().assertValue("hello"); + maybe.test().assertValue("hello"); + observable.test().assertValues("hello", "bonjour").assertComplete(); + flowable.test().assertValues("hello", "bonjour").assertComplete(); + } + + private Observable getObservable() { + return Observable.just("a", "b", "c") + .map(s -> (T) s); + } + + private Flowable getFlowable() { + return Flowable.just("a", "b", "c") + .map(s -> (T) s); + } + + private Single getSingle() { + return Single.just("a") + .map(s -> (T) s); + } + + private Maybe getMaybe() { + return Maybe.just("a") + .map(s -> (T) s); + } + + private Maybe getEmptyMaybe() { + return Maybe.empty(); + } + + private Completable getCompletable() { + return Completable.complete(); + } + + private Multi getMulti() { + return Multi.createFrom().items("a", "b", "c") + .map(s -> (T) s); + } + + private Uni getUni() { + return Uni.createFrom().item("a") + .map(s -> (T) s); + } +} diff --git a/content/docs/smallrye-mutiny/latest/_examples/java/guides/operators/BroadcastProcessorTest.java b/content/docs/smallrye-mutiny/latest/_examples/java/guides/operators/BroadcastProcessorTest.java new file mode 100644 index 000000000..36172511c --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/_examples/java/guides/operators/BroadcastProcessorTest.java @@ -0,0 +1,35 @@ +package guides.operators; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.helpers.test.AssertSubscriber; +import io.smallrye.mutiny.operators.multi.processors.BroadcastProcessor; +import org.junit.jupiter.api.Test; + +public class BroadcastProcessorTest { + + @Test + public void test() { + // tag::code[] + BroadcastProcessor processor = BroadcastProcessor.create(); + Multi multi = processor + .onItem().transform(String::toUpperCase) + .onFailure().recoverWithItem("d'oh"); + + new Thread(() -> { + for (int i = 0; i < 1000; i++) { + processor.onNext(Integer.toString(i)); + } + processor.onComplete(); + }).start(); + + // Subscribers can subscribe at any time. + // They will only receive items emitted after their subscription. + // If the source is already terminated (by a completion or a failure signal) + // the subscriber receives this signal. + + // end::code[] + AssertSubscriber subscriber = AssertSubscriber.create(Long.MAX_VALUE); + multi.subscribe().withSubscriber(subscriber) + .awaitCompletion(); + } +} diff --git a/content/docs/smallrye-mutiny/latest/_examples/java/guides/operators/CollectingItemsTest.java b/content/docs/smallrye-mutiny/latest/_examples/java/guides/operators/CollectingItemsTest.java new file mode 100644 index 000000000..b48d62c53 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/_examples/java/guides/operators/CollectingItemsTest.java @@ -0,0 +1,99 @@ +package guides.operators; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.tuples.Tuple2; +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.util.*; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.*; + +@SuppressWarnings("Convert2MethodRef") +public class CollectingItemsTest { + + @Test + public void testList() { + // tag::list[] + Multi multi = getMulti(); + Uni> uni = multi.collect().asList(); + // end::list[] + + assertThat(uni.await().indefinitely()).containsExactly("a", "b", "c"); + + // tag::first[] + Uni first = multi.collect().first(); + Uni last = multi.collect().last(); + // end::first[] + + assertThat(first.await().indefinitely()).isEqualTo("a"); + assertThat(last.await().indefinitely()).isEqualTo("c"); + } + + @Test + public void testMap() { + // tag::map[] + Multi multi = getMulti(); + Uni> uni = + multi.collect() + .asMap(item -> getUniqueKeyForItem(item)); + // end::map[] + + assertThat(uni.await().indefinitely()).hasSize(3); + } + + @Test + public void testMultiMap() { + // tag::multimap[] + Multi multi = getMulti(); + Uni>> uni = + multi.collect() + .asMultiMap(item -> getKeyForItem(item)); + // end::multimap[] + + assertThat(uni.await().indefinitely()).hasSize(2); + } + + @Test + public void testCustomAccumulator() { + // tag::accumulator[] + Multi multi = getMulti(); + Uni uni = multi.collect() + .in(MyCollection::new, (col, item) -> col.add(item)); + // end::accumulator[] + + assertThat(uni.await().indefinitely()).hasSize(3); + + // tag::collector[] + Uni count = multi.collect() + .with(Collectors.counting()); + // end::collector[] + + assertThat(count.await().indefinitely()).isEqualTo(3); + } + + private String getKeyForItem(String item) { + if( item.equalsIgnoreCase("b")) { + return "b"; + } else { + return "a"; + } + } + + private String getUniqueKeyForItem(String item) { + return item; + } + + private Multi getMulti() { + return Multi.createFrom().items("a", "b", "c"); + } + + private static class MyCollection extends ArrayList { + + } +} diff --git a/content/docs/smallrye-mutiny/latest/_examples/java/guides/operators/CombiningItemsTest.java b/content/docs/smallrye-mutiny/latest/_examples/java/guides/operators/CombiningItemsTest.java new file mode 100644 index 000000000..1f02148fd --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/_examples/java/guides/operators/CombiningItemsTest.java @@ -0,0 +1,177 @@ +package guides.operators; + +import guides.extension.SystemOut; +import guides.extension.SystemOutCaptureExtension; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.tuples.Tuple2; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.time.Duration; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +@SuppressWarnings({ "unchecked", "Convert2MethodRef" }) +@ExtendWith(SystemOutCaptureExtension.class) +public class CombiningItemsTest { + + @Test + public void testWithUni(SystemOut out) throws InterruptedException { + // tag::invocations[] + Uni uniA = invokeHttpServiceA(); + Uni uniB = invokeHttpServiceB(); + // end::invocations[] + + // tag::combination[] + Uni> responses = Uni.combine() + .all().unis(uniA, uniB).asTuple(); + // end::combination[] + + // tag::subscription[] + Uni.combine().all().unis(uniA, uniB).asTuple() + .subscribe().with(tuple -> { + System.out.println("Response from A: " + tuple.getItem1()); + System.out.println("Response from B: " + tuple.getItem2()); + }); + // end::subscription[] + + await().until(() -> out.get().contains("Response from A: Response{content='A'}")); + await().until(() -> out.get().contains("Response from B: Response{content='B'}")); + + // tag::combined-with[] + Uni> uni = Uni.combine() + .all().unis(uniA, uniB).with( + listOfResponses -> { + Map map = new LinkedHashMap<>(); + map.put("A", (Response) listOfResponses.get(0)); + map.put("B", (Response) listOfResponses.get(1)); + return map; + } + ); + // end::combined-with[] + + assertThat(uni.await().indefinitely()).containsKeys("A", "B"); + + // + Uni> uni1 = Uni.combine() + .all().unis(uniA, uniB).withUni( + listOfResponses -> { + Map map = new LinkedHashMap<>(); + map.put("A", (Response) listOfResponses.get(0)); + map.put("B", (Response) listOfResponses.get(1)); + return Uni.createFrom().item(map); + } + ); + // + + assertThat(uni1.await().indefinitely()).containsKeys("A", "B"); + + + Thread.sleep(100); + } + + @Test + public void testWithMulti() throws InterruptedException { + Multi multiA = getMultiA(); + Multi multiB = getMultiB(); + + // tag::combine-multi[] + Multi> combined = Multi.createBy().combining() + .streams(multiA, multiB).asTuple(); + // end::combine-multi[] + + CountDownLatch latch = new CountDownLatch(1); + AtomicInteger i = new AtomicInteger(); + combined.subscribe().with( + tuple -> { + int v = i.incrementAndGet(); + assertThat(tuple.getItem1()).isEqualTo("a" + v); + assertThat(tuple.getItem2()).isEqualTo("b" + v); + }, + () -> latch.countDown() + ); + + assertThat(latch.await(10, TimeUnit.MILLISECONDS)).isTrue(); + + // tag::combine-multi-with[] + Multi.createBy().combining() + .streams(multiA, multiB).using(list -> combineItems(list)) + .subscribe().with(x -> { + // do something with the combined items + }); + // end::combine-multi-with[] + } + + @Test + public void testCombineLast() { + Multi multiA = getMultiA(); + Multi multiB = getMultiB(); + + // tag::combine-last[] + Multi> multi1 = Multi.createBy().combining() + .streams(multiA, multiB) + .latestItems().asTuple(); + + // or + + Multi multi2 = Multi.createBy().combining() + .streams(multiA, multiB) + .latestItems().using(list -> combineItems(list)); + // end::combine-last[] + + List> list = multi1.collect().asList().await().indefinitely(); + assertThat(list).hasSize(3); + assertThat(list.toString()).contains("a3", "b1", "b2", "b3").doesNotContain("a1", "a2"); + List strings = multi2.collect().asList().await().indefinitely(); + assertThat(strings).hasSize(3).containsExactly("a3b1", "a3b2", "a3b3"); + } + + private String combineItems(List list) { + return "" + list.get(0) + list.get(1); + } + + private Multi getMultiA() { + return Multi.createFrom().items("a1", "a2", "a3") + .onItem().transform(s -> (A) s); + } + + private Multi getMultiB() { + return Multi.createFrom().items("b1", "b2", "b3") + .onItem().transform(s -> (B) s); + } + + private Uni invokeHttpServiceA() { + return Uni.createFrom().item(new Response("A")) + .onItem().delayIt().by(Duration.ofMillis(10)); + } + + private Uni invokeHttpServiceB() { + return Uni.createFrom().item(new Response("B")) + .onItem().delayIt().by(Duration.ofMillis(30)); + } + + static class Response { + + private final String content; + + public Response(String content) { + this.content = content; + } + + @Override + public String toString() { + return "Response{" + + "content='" + content + '\'' + + '}'; + } + } + +} diff --git a/content/docs/smallrye-mutiny/latest/_examples/java/guides/operators/ControlDemandTest.java b/content/docs/smallrye-mutiny/latest/_examples/java/guides/operators/ControlDemandTest.java new file mode 100644 index 000000000..2ea2694e1 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/_examples/java/guides/operators/ControlDemandTest.java @@ -0,0 +1,93 @@ +package guides.operators; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.helpers.test.AssertSubscriber; +import io.smallrye.mutiny.infrastructure.Infrastructure; +import io.smallrye.mutiny.subscription.DemandPacer; +import io.smallrye.mutiny.subscription.FixedDemandPacer; +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; + +import static org.assertj.core.api.Assertions.assertThat; + +class ControlDemandTest { + + @Test + void pacing() { + // tag::pacing[] + FixedDemandPacer pacer = new FixedDemandPacer(25L, Duration.ofMillis(100L)); + + Multi multi = Multi.createFrom().range(0, 100) + .paceDemand().on(Infrastructure.getDefaultWorkerPool()).using(pacer); + // end::pacing[] + + AssertSubscriber sub = multi.subscribe().withSubscriber(AssertSubscriber.create()); + sub.awaitCompletion(); + assertThat(sub.getItems()).hasSize(100); + } + + @Test + void customPacer() { + // tag::custom-pacer[] + DemandPacer pacer = new DemandPacer() { + + @Override + public Request initial() { + return new Request(10L, Duration.ofMillis(100L)); + } + + @Override + public Request apply(Request previousRequest, long observedItemsCount) { + return new Request(previousRequest.demand() * 2, previousRequest.delay().plus(10, ChronoUnit.MILLIS)); + } + }; + // end::custom-pacer[] + } + + @Test + void capping() { + // tag::capConstant[] + AssertSubscriber sub = AssertSubscriber.create(); + + sub = Multi.createFrom().range(0, 100) + .capDemandsTo(50L) + .subscribe().withSubscriber(sub); + + // A first batch of 50 (capped), 25 remain outstanding + sub.request(75L).assertNotTerminated(); + assertThat(sub.getItems()).hasSize(50); + + // Second batch: 25 + 25 = 50 + sub.request(25L).assertCompleted(); + assertThat(sub.getItems()).hasSize(100); + // end::capConstant[] + } + + @Test + void cappingFunction() { + // tag::capFunction[] + AssertSubscriber sub = AssertSubscriber.create(); + + sub = Multi.createFrom().range(0, 100) + .capDemandsUsing(n -> { + if (n > 1) { + return (long) (((double) n) * 0.75d); + } else { + return n; + } + }) + .subscribe().withSubscriber(sub); + + sub.request(100L).assertNotTerminated(); + assertThat(sub.getItems()).hasSize(75); + + sub.request(1L).assertNotTerminated(); + assertThat(sub.getItems()).hasSize(94); + + sub.request(Long.MAX_VALUE).assertCompleted(); + assertThat(sub.getItems()).hasSize(100); + // end::capFunction[] + } +} diff --git a/content/docs/smallrye-mutiny/latest/_examples/java/guides/operators/DelayTest.java b/content/docs/smallrye-mutiny/latest/_examples/java/guides/operators/DelayTest.java new file mode 100644 index 000000000..052004e8b --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/_examples/java/guides/operators/DelayTest.java @@ -0,0 +1,87 @@ +package guides.operators; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.util.List; +import java.util.Random; + +import static org.assertj.core.api.Assertions.assertThat; + +public class DelayTest { + + @Test + public void testDelayBy() { + // tag::delay-by[] + Uni delayed = Uni.createFrom().item("hello") + .onItem().delayIt().by(Duration.ofMillis(10)); + // end::delay-by[] + String r = delayed.map(s -> "Delayed " + s) + .await().indefinitely(); + assertThat(r).isEqualTo("Delayed hello"); + } + + @Test + public void testDelayUntil() { + // tag::delay-until[] + Uni delayed = Uni.createFrom().item("hello") + // The write method returns a Uni completed + // when the operation is done. + .onItem().delayIt().until(this::write); + // end::delay-until[] + String r = delayed + .map(s -> "Written " + s) + .await().indefinitely(); + assertThat(r).isEqualTo("Written hello"); + } + + @Test + public void testDelayMulti() { + Multi multi = Multi.createFrom().items(1, 2, 3, 4, 5); + // tag::delay-multi[] + Multi delayed = multi + .onItem().call(i -> + // Delay the emission until the returned uni emits its item + Uni.createFrom().nullItem().onItem().delayIt().by(Duration.ofMillis(10)) + ); + // end::delay-multi[] + assertThat(delayed.collect().asList().await().indefinitely()).containsExactly(1, 2, 3, 4, 5); + } + + @Test + public void testThrottling() { + Multi multi = Multi.createFrom().items(1, 2, 3, 4, 5); + // tag::throttling-multi[] + // Introduce a one second delay between each item + Multi ticks = Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .onOverflow().drop(); + Multi delayed = Multi.createBy().combining().streams(ticks, multi) + .using((x, item) -> item); + // end::throttling-multi[] + assertThat(delayed.collect().asList().await().indefinitely()).containsExactly(1, 2, 3, 4, 5); + } + + @Test + public void testDelayMultiRandom() { + // tag::delay-multi-random[] + Random random = new Random(); + Multi delayed = Multi.createFrom().items(1, 2, 3, 4, 5) + .onItem().call(i -> { + Duration delay = Duration.ofMillis(random.nextInt(100) + 1); + return Uni.createFrom().nullItem().onItem().delayIt().by(delay); + }); + // end::delay-multi-random[] + assertThat(delayed.collect().asList() + .await().indefinitely()) + .containsExactlyInAnyOrder(1, 2, 3, 4, 5); + } + + private Uni write(String s) { + return Uni.createFrom().item(s) + .onItem().delayIt().by(Duration.ofMillis(20)) + .onItem().ignore().andContinueWithNull(); + } + +} diff --git a/content/docs/smallrye-mutiny/latest/_examples/java/guides/operators/EmitOnTest.java b/content/docs/smallrye-mutiny/latest/_examples/java/guides/operators/EmitOnTest.java new file mode 100644 index 000000000..c9398aa36 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/_examples/java/guides/operators/EmitOnTest.java @@ -0,0 +1,62 @@ +package guides.operators; + +import guides.extension.SystemOut; +import guides.extension.SystemOutCaptureExtension; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import static org.assertj.core.api.Assertions.assertThat; + +@SuppressWarnings("Convert2MethodRef") +@ExtendWith(SystemOutCaptureExtension.class) +public class EmitOnTest { + + @Test + public void testEmitOn() { + // tag::example[] + Uni uni = Uni.createFrom().emitter(emitter -> + new Thread(() -> + emitter.complete("hello from " + + Thread.currentThread().getName()) + ).start() + ) + .onItem().transform(item -> { + // Called on the emission thread. + return item.toUpperCase(); + }); + // end::example[] + + assertThat(uni.await().indefinitely()).startsWith("HELLO FROM").doesNotContain("MAIN"); + } + + @Test + public void test(SystemOut out) { + Uni uni = Uni.createFrom().item("hello"); + Multi multi = Multi.createFrom().items("a", "b", "c"); + + ExecutorService executor = Executors.newFixedThreadPool(4); + // tag::code[] + String res0 = uni.emitOn(executor) + .onItem() + .invoke(s -> System.out.println("Received item `" + s + "` on thread: " + + Thread.currentThread().getName())) + .await().indefinitely(); + + String res1 = multi.emitOn(executor) + .onItem() + .invoke(s -> System.out.println("Received item `" + s + "` on thread: " + + Thread.currentThread().getName())) + .collect().first() + .await().indefinitely(); + // end::code[] + + assertThat(out.get()).doesNotContain("main").contains("pool-").contains("hello", "a"); + assertThat(res0).isEqualTo("hello"); + assertThat(res1).isEqualTo("a"); + } +} diff --git a/content/docs/smallrye-mutiny/latest/_examples/java/guides/operators/FilterTest.java b/content/docs/smallrye-mutiny/latest/_examples/java/guides/operators/FilterTest.java new file mode 100644 index 000000000..39e72d024 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/_examples/java/guides/operators/FilterTest.java @@ -0,0 +1,101 @@ +package guides.operators; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class FilterTest { + + @Test + public void filter() { + Multi multi = Multi.createFrom().range(1, 11); + // tag::filter[] + List list = multi + .select().where(i -> i > 6) + .collect().asList() + .await().indefinitely(); + // end::filter[] + + // tag::test[] + List list2 = multi + .select().when(i -> Uni.createFrom().item(i > 6)) + .collect().asList() + .await().indefinitely(); + // end::test[] + + assertThat(list).containsExactly(7, 8, 9, 10); + assertThat(list2).containsExactly(7, 8, 9, 10); + } + + @Test + public void take() { + Multi multi = Multi.createFrom().range(1, 11); + // + List list = multi + .select().first(2) + .collect().asList() + .await().indefinitely(); + + List list2 = multi + .select().first(i -> i < 3) + .collect().asList() + .await().indefinitely(); + + List list3 = multi + .select().last(2) + .collect().asList() + .await().indefinitely(); + // + assertThat(list).containsExactly(1, 2); + assertThat(list2).containsExactly(1, 2); + assertThat(list3).containsExactly(9, 10); + } + + @Test + public void skip() { + Multi multi = Multi.createFrom().range(1, 11); + // + List list = multi + .skip().first(8) + .collect().asList() + .await().indefinitely(); + + List list2 = multi + .skip().first(i -> i < 9) + .collect().asList() + .await().indefinitely(); + + List list3 = multi + .skip().last(8) + .collect().asList() + .await().indefinitely(); + // + assertThat(list).containsExactly(9, 10); + assertThat(list2).containsExactly(9, 10); + assertThat(list3).containsExactly(1, 2); + } + + @Test + public void distinct() { + Multi multi = Multi.createFrom().items(1, 1, 2, 3, 4, 5, 5, 6); + // + List list = multi + .select().distinct() + .collect().asList() + .await().indefinitely(); + // + + // + List list2 = multi + .skip().repetitions() + .collect().asList() + .await().indefinitely(); + // + assertThat(list).containsExactly(1, 2, 3, 4, 5, 6); + assertThat(list2).containsExactly(1, 2, 3, 4, 5, 6); + } +} diff --git a/content/docs/smallrye-mutiny/latest/_examples/java/guides/operators/GroupingItemsTest.java b/content/docs/smallrye-mutiny/latest/_examples/java/guides/operators/GroupingItemsTest.java new file mode 100644 index 000000000..022c11219 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/_examples/java/guides/operators/GroupingItemsTest.java @@ -0,0 +1,188 @@ +package guides.operators; + +import io.smallrye.mutiny.GroupedMulti; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.groups.Gatherer; +import io.smallrye.mutiny.groups.Gatherers; +import io.smallrye.mutiny.helpers.test.AssertSubscriber; +import io.smallrye.mutiny.subscription.BackPressureFailure; +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collector; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class GroupingItemsTest { + + @Test + public void groupIntoLists() { + // tag::groupIntoLists[] + Multi> grouped = Multi.createFrom().range(0, 10) + .group().intoLists().of(3); // Group every 3 items into a list + + List> lists = grouped + .collect().asList() + .await().indefinitely(); + + assertThat(lists).containsExactly( + List.of(0, 1, 2), + List.of(3, 4, 5), + List.of(6, 7, 8), + List.of(9) + ); + // end::groupIntoLists[] + } + + @Test + public void timeBasedListGrouping() { + // tag::timeBasedListGrouping[] + Multi> timeWindows = Multi.createFrom() + .ticks().every(Duration.ofMillis(10)) + .group().intoLists().every(Duration.ofMillis(100)); + + // Each list contains items emitted within a 100ms window + // end::timeBasedListGrouping[] + } + + @Test + public void sizeAndTimeBasedListGrouping() { + // tag::sizeAndTimeBasedListGrouping[] + Multi> combined = Multi.createFrom() + .ticks().every(Duration.ofMillis(10)) + .group().intoLists().of(5, Duration.ofMillis(100)); + + // Emits a list when either 5 items are collected OR 100ms expires, + // whichever happens first + // end::sizeAndTimeBasedListGrouping[] + } + + @Test + public void groupIntoMultis() { + // tag::groupIntoMultis[] + Multi> grouped = Multi.createFrom().range(0, 7) + .group().intoMultis().of(2); // Group every 2 items into a Multi + + // Process each Multi independently - for example, join each group + Multi sums = grouped + .onItem().transformToUniAndConcatenate(multi -> + multi.collect().asList() + .onItem().transform(couple -> couple.stream() + .map(Objects::toString) + .collect(Collectors.joining("-")))); + + List results = sums.collect().asList().await().indefinitely(); + assertThat(results).containsExactly("0-1", "2-3", "4-5", "6"); + // end::groupIntoMultis[] + } + + @Test + public void timeBasedGrouping() { + // tag::timeBasedGrouping[] + Multi> timeWindows = Multi.createFrom() + .ticks().every(Duration.ofMillis(10)) + .group().intoMultis().every(Duration.ofMillis(100)); + + // Each Multi contains items emitted within a 100ms window + // end::timeBasedGrouping[] + } + + @Test + public void groupByKey() { + // tag::groupByKey[] + Multi> groups = Multi.createFrom().range(1, 10) + .group().by(i -> i % 3); // Group by modulo 3 + + // Process each group separately + Uni> result = groups + .onItem().transformToUniAndConcatenate(group -> + group.onItem().transform(Objects::toString) + .collect().asList() + .onItem().transform(l -> group.key() + "(" + String.join(",", l) + ")")) + .collect().asList(); + + List lists = result.await().indefinitely(); + assertThat(lists).containsExactly("1(1,4,7)", "2(2,5,8)", "0(3,6,9)"); + // end::groupByKey[] + } + + @Test + public void groupByKeyAndValue() { + // tag::groupByKeyAndValue[] + Multi> groups = Multi.createFrom().range(1, 10) + .group().by( + i -> i % 2 == 0, // Key: true for even, false for odd + i -> "Number: " + i // Value: transform to string + ); + + Uni>> result = groups + .onItem().transformToUniAndMerge(group -> + group.collect().asList() + ) + .collect().asList(); + + List> lists = result.await().indefinitely(); + assertThat(lists).hasSize(2); + // end::groupByKeyAndValue[] + } + + @Test + public void groupByWithMerge() { + // tag::groupByWithMerge[] + Multi results = Multi.createFrom().range(1, 100) + .group().by(i -> i % 10) // Create 10 groups (0-9) + .onItem().transformToMulti( + group -> group.onItem().transform(i -> "Group " + group.key() + ": " + i) + ) + .merge(10); // Set concurrency to at least the number of groups + + List items = results.collect().asList().await().indefinitely(); + assertThat(items).hasSize(99); + // end::groupByWithMerge[] + } + + @Test + public void groupByDeadlockExample() { + // tag::groupByDeadlock[] + // Creating 10 groups but limiting concurrency to only 2 + Multi dangerous = Multi.createFrom().ticks().every(Duration.ofMillis(10)) + .group().by(i -> i % 10) // Creates 10 groups + .onItem().transformToMulti( + group -> group.onItem().transform(i -> "Group " + group.key() + ": " + i) + ) + .merge(2); // Only 2 groups can be processed concurrently + + // This may hang because groups 3-10 cannot make progress + // while waiting for groups 1-2 to complete + AssertSubscriber sub = dangerous.subscribe() + .withSubscriber(AssertSubscriber.create(Long.MAX_VALUE)); + + sub.awaitFailure(t -> assertThat(t).isInstanceOf(BackPressureFailure.class)); + // end::groupByDeadlock[] + } + + @Test + public void groupByWithConcatenate() { + // tag::groupByWithConcatenate[] + // Safe alternative: use concatenate to process groups sequentially + Multi safe = Multi.createFrom().range(1, 100) + .invoke(i -> System.out.println("emit " + i)) + .group().by(i -> i % 10) + .onItem().transformToMultiAndConcatenate( + group -> group.onItem().transform(i -> "Group " + group.key() + ": " + i) + .invoke(i -> System.out.println("group " + i)) + ); + // Groups are processed one at a time, so no deadlock + + List items = safe.collect().asList().await().indefinitely(); + assertThat(items).hasSize(99); + // end::groupByWithConcatenate[] + } + +} diff --git a/content/docs/smallrye-mutiny/latest/_examples/java/guides/operators/MergeConcatTest.java b/content/docs/smallrye-mutiny/latest/_examples/java/guides/operators/MergeConcatTest.java new file mode 100644 index 000000000..580404e7e --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/_examples/java/guides/operators/MergeConcatTest.java @@ -0,0 +1,119 @@ +package guides.operators; + +import guides.extension.SystemOut; +import guides.extension.SystemOutCaptureExtension; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.subscription.Cancellable; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.time.Duration; +import java.util.List; +import java.util.Random; +import java.util.concurrent.CopyOnWriteArrayList; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +@SuppressWarnings({ "unchecked", "Convert2MethodRef" }) +@ExtendWith(SystemOutCaptureExtension.class) +public class MergeConcatTest { + + private final Random random = new Random(); + + @Test + public void testMerge() { + // tag::merge[] + Multi multi1 = getFirstMulti(); + Multi multi2 = getSecondMulti(); + + Multi merged = Multi.createBy().merging().streams(multi1, multi2); + // end::merge[] + + List received = new CopyOnWriteArrayList<>(); + merged.subscribe().with(received::add); + await().until(() -> received.size() == 9); + assertThat(received).contains(1, 2, 3, 4, 5, 6, 7, 8, 9); + } + + @Test + public void testConcat() { + // tag::concat[] + Multi multi1 = getFirstMulti(); + Multi multi2 = getSecondMulti(); + + Multi concatenated = Multi.createBy().concatenating().streams(multi1, multi2); + // end::concat[] + + List received = new CopyOnWriteArrayList<>(); + concatenated.subscribe().with(received::add); + await().until(() -> received.size() == 9); + assertThat(received).containsExactly(1, 2, 3, 4, 5, 6, 7, 8, 9); + } + + @Test + public void testMergeTicks(SystemOut out) { + // tag::merge-ticks[] + Multi first = Multi.createFrom().ticks().every(Duration.ofMillis(10)) + .onItem().transform(l -> "Stream 1 - " + l); + + Multi second = Multi.createFrom().ticks().every(Duration.ofMillis(15)) + .onItem().transform(l -> "Stream 2 - " + l); + + Multi third = Multi.createFrom().ticks().every(Duration.ofMillis(5)) + .onItem().transform(l -> "Stream 3 - " + l); + + Cancellable cancellable = Multi.createBy().merging().streams(first, second, third) + .subscribe().with(s -> System.out.println("Got item: " + s)); + // end::merge-ticks[] + + await().until(() -> out.get().contains("Got item: Stream 3 - 10")); + cancellable.cancel(); + } + + @Test + public void testConcatenateStrings(SystemOut out) { + // tag::concatenate-strings[] + Multi first = Multi.createFrom().items("A1", "A2", "A3"); + Multi second = Multi.createFrom().items("B1", "B2", "B3"); + + Multi.createBy().concatenating().streams(first, second) + .subscribe().with(item -> System.out.print(item)); // "A1A2A3B1B2B3" + + Multi.createBy().concatenating().streams(second, first) + .subscribe().with(item -> System.out.print(item)); // "B1B2B3A1A2A3" + // end::concatenate-strings[] + + await().until(() -> out.get().contains("A1A2A3B1B2B3")); + await().until(() -> out.get().contains("B1B2B3A1A2A3")); + + assertThat( + Multi.createBy().concatenating().streams(first, second) + .collect().in(StringBuffer::new, StringBuffer::append) + .await().indefinitely() + ).isEqualToIgnoringCase("A1A2A3B1B2B3"); + + assertThat( + Multi.createBy().concatenating().streams(second, first) + .collect().in(StringBuffer::new, StringBuffer::append) + .await().indefinitely() + ).isEqualToIgnoringCase("B1B2B3A1A2A3"); + + } + + private Multi getFirstMulti() { + return Multi.createFrom().items(1, 2, 3, 4, 5, 6) + .onItem().call(() -> Uni.createFrom().nullItem().onItem().delayIt() + .by(Duration.ofMillis(random.nextInt(100) + 1))) + .onItem().transform(i -> (T) i); + } + + private Multi getSecondMulti() { + return Multi.createFrom().items(7, 8, 9) + .onItem().call(() -> Uni.createFrom().nullItem().onItem().delayIt() + .by(Duration.ofMillis(random.nextInt(100) + 1))) + .onItem().transform(i -> (T) i); + } +} diff --git a/content/docs/smallrye-mutiny/latest/_examples/java/guides/operators/PaginationTest.java b/content/docs/smallrye-mutiny/latest/_examples/java/guides/operators/PaginationTest.java new file mode 100644 index 000000000..e032de2a6 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/_examples/java/guides/operators/PaginationTest.java @@ -0,0 +1,97 @@ +package guides.operators; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.helpers.test.AssertSubscriber; +import org.junit.jupiter.api.Test; + +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.assertj.core.api.Assertions.assertThat; + +public class PaginationTest { + + @SuppressWarnings("Convert2MethodRef") + @Test + public void test() { + // tag::code[] + PaginatedApi api = new PaginatedApi(); + + Multi stream = Multi.createBy().repeating() + .completionStage( + () -> new AtomicInteger(), + state -> api.getPage(state.getAndIncrement())) + .until(list -> list.isEmpty()) + .onItem().disjoint(); + // end::code[] + stream.subscribe().withSubscriber(AssertSubscriber.create(10)) + .assertCompleted() + .assertItems("a", "b", "c", "d", "e", "f", "g", "h"); + + } + + @SuppressWarnings("Convert2MethodRef") + @Test + public void test2() { + // tag::code2[] + PaginatedApi api = new PaginatedApi(); + + Multi stream = Multi.createBy().repeating() + .uni( + () -> new AtomicInteger(), + state -> api.retrieve(state.getAndIncrement())) + .whilst(page -> page.hasNext()); + // end::code2[] + AssertSubscriber subscriber = stream.subscribe() + .withSubscriber(AssertSubscriber.create(10)) + .assertCompleted(); + + assertThat(subscriber.getItems()).hasSize(3); + + } + + private static class Page { + final boolean hasNext; + + private Page(boolean hasNext) { + this.hasNext = hasNext; + } + + public boolean hasNext() { + return hasNext; + } + } + + + private class PaginatedApi { + + Map> pages = new LinkedHashMap<>(); + + public PaginatedApi() { + pages.put(0, Arrays.asList("a", "b", "c")); + pages.put(1, Arrays.asList("d", "e", "f")); + pages.put(2, Arrays.asList("g", "h")); + pages.put(3, Collections.emptyList()); + } + + CompletionStage> getPage(int page) { + List strings = pages.get(page); + if (strings == null) { + strings = Collections.emptyList(); + } + return CompletableFuture.completedFuture(strings); + } + + + Uni retrieve(int page) { + if (page == 2) { + return Uni.createFrom().item(new Page(false)); + } + return Uni.createFrom().item(new Page(true)); + } + } + +} diff --git a/content/docs/smallrye-mutiny/latest/_examples/java/guides/operators/PausingDemandTest.java b/content/docs/smallrye-mutiny/latest/_examples/java/guides/operators/PausingDemandTest.java new file mode 100644 index 000000000..9f27d3dd1 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/_examples/java/guides/operators/PausingDemandTest.java @@ -0,0 +1,267 @@ +package guides.operators; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.helpers.test.AssertSubscriber; +import io.smallrye.mutiny.subscription.BackPressureStrategy; +import io.smallrye.mutiny.subscription.DemandPauser; +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +class PausingDemandTest { + + @Test + void basicPausing() { + // tag::basic[] + DemandPauser pauser = new DemandPauser(); + + AssertSubscriber sub = AssertSubscriber.create(); + Multi.createFrom().range(0, 100) + .pauseDemand().using(pauser) + // Throttle the multi + .onItem().call(i -> Uni.createFrom().nullItem() + .onItem().delayIt().by(Duration.ofMillis(10))) + .subscribe().withSubscriber(sub); + + // Unbounded request + sub.request(Long.MAX_VALUE); + // Wait for some items + await().untilAsserted(() -> assertThat(sub.getItems()).hasSizeGreaterThan(10)); + + // Pause the stream + pauser.pause(); + assertThat(pauser.isPaused()).isTrue(); + + int sizeWhenPaused = sub.getItems().size(); + + // Wait - no new items should arrive (except a few in-flight) + await().pollDelay(Duration.ofMillis(100)).until(() -> true); + assertThat(sub.getItems()).hasSizeLessThanOrEqualTo(sizeWhenPaused + 5); + + // Resume the stream + pauser.resume(); + assertThat(pauser.isPaused()).isFalse(); + + // All items eventually arrive + sub.awaitCompletion(); + assertThat(sub.getItems()).hasSize(100); + // end::basic[] + } + + @Test + void initiallyPaused() { + // tag::initially-paused[] + DemandPauser pauser = new DemandPauser(); + + AssertSubscriber sub = Multi.createFrom().range(0, 50) + .pauseDemand() + .paused(true) // Start paused + .using(pauser) + .subscribe().withSubscriber(AssertSubscriber.create(Long.MAX_VALUE)); + + // No items arrive while paused + await().pollDelay(Duration.ofMillis(100)).until(() -> true); + assertThat(sub.getItems()).isEmpty(); + assertThat(pauser.isPaused()).isTrue(); + + // Resume to start receiving items + pauser.resume(); + sub.awaitCompletion(); + assertThat(sub.getItems()).hasSize(50); + // end::initially-paused[] + } + + @Test + void lateSubscription() { + // tag::late-subscription[] + DemandPauser pauser = new DemandPauser(); + AtomicBoolean subscribed = new AtomicBoolean(false); + + AssertSubscriber sub = Multi.createFrom().range(0, 50) + .onSubscription().invoke(() -> subscribed.set(true)) + .pauseDemand() + .paused(true) // Start paused + .lateSubscription(true) // Delay subscription until resumed + .using(pauser) + .subscribe().withSubscriber(AssertSubscriber.create(Long.MAX_VALUE)); + + // Stream is not subscribed yet + await().pollDelay(Duration.ofMillis(100)).until(() -> true); + assertThat(subscribed.get()).isFalse(); + assertThat(sub.getItems()).isEmpty(); + + // Resume triggers subscription and item flow + pauser.resume(); + await().untilAsserted(() -> assertThat(subscribed.get()).isTrue()); + sub.awaitCompletion(); + assertThat(sub.getItems()).hasSize(50); + // end::late-subscription[] + } + + @Test + void bufferStrategy() { + // tag::buffer-strategy[] + DemandPauser pauser = new DemandPauser(); + + AssertSubscriber sub = AssertSubscriber.create(Long.MAX_VALUE); + + Multi.createFrom().ticks().every(Duration.ofMillis(10)) + .select().first(100) + .pauseDemand() + .bufferStrategy(BackPressureStrategy.BUFFER) + .bufferSize(20) // Buffer up to 20 items + .using(pauser) + .subscribe().withSubscriber(sub); + + // Wait for some items + await().untilAsserted(() -> assertThat(sub.getItems()).hasSizeGreaterThan(5)); + + // Pause - items buffer up to the limit + pauser.pause(); + + // Wait for buffer to fill + await().pollDelay(Duration.ofMillis(100)) + .untilAsserted(() -> assertThat(pauser.bufferSize()).isGreaterThan(0)); + + // Buffer size is capped + assertThat(pauser.bufferSize()).isLessThanOrEqualTo(20); + + // Resume drains the buffer + pauser.resume(); + await().untilAsserted(() -> assertThat(pauser.bufferSize()).isEqualTo(0)); + // end::buffer-strategy[] + } + + @Test + void dropStrategy() { + // tag::drop-strategy[] + DemandPauser pauser = new DemandPauser(); + + AssertSubscriber sub = Multi.createFrom().ticks().every(Duration.ofMillis(5)) + .select().first(200) + .pauseDemand() + .bufferStrategy(BackPressureStrategy.DROP) // Drop items while paused + .using(pauser) + .subscribe().withSubscriber(AssertSubscriber.create(Long.MAX_VALUE)); + + // Wait for some items + await().untilAsserted(() -> assertThat(sub.getItems()).hasSizeGreaterThan(20)); + + // Pause - subsequent items are dropped + pauser.pause(); + int sizeWhenPaused = sub.getItems().size(); + + // Wait while items are dropped + await().pollDelay(Duration.ofMillis(200)).until(() -> true); + + // Resume - continue from current position + pauser.resume(); + await().atMost(Duration.ofSeconds(3)) + .untilAsserted(() -> assertThat(sub.getItems().size()).isGreaterThan(sizeWhenPaused + 20)); + + sub.awaitCompletion(); + // Not all items arrived (some were dropped) + assertThat(sub.getItems()).hasSizeLessThan(200); + // end::drop-strategy[] + } + + @Test + void bufferManagement() { + // tag::buffer-management[] + DemandPauser pauser = new DemandPauser(); + + AssertSubscriber sub = Multi.createFrom().ticks().every(Duration.ofMillis(10)) + .select().first(100) + .pauseDemand() + .bufferStrategy(BackPressureStrategy.BUFFER) + .bufferUnconditionally() // Unbounded buffer + .using(pauser) + .subscribe().withSubscriber(AssertSubscriber.create(Long.MAX_VALUE)); + + // Wait for some items + await().untilAsserted(() -> assertThat(sub.getItems()).hasSizeGreaterThan(5)); + + // Pause and let buffer fill + pauser.pause(); + await().pollDelay(Duration.ofMillis(100)) + .untilAsserted(() -> assertThat(pauser.bufferSize()).isGreaterThan(0)); + + int bufferSize = pauser.bufferSize(); + assertThat(bufferSize).isGreaterThan(0); + + // Clear the buffer + boolean cleared = pauser.clearBuffer(); + assertThat(cleared).isTrue(); + assertThat(pauser.bufferSize()).isEqualTo(0); + + // Resume - items continue from current position (cleared items are lost) + pauser.resume(); + // end::buffer-management[] + } + + @Test + void externalControl() { + // + DemandPauser pauser = new DemandPauser(); + + // Create and transform the stream + Multi stream = Multi.createFrom().range(0, 1000) + .pauseDemand().using(pauser) + .onItem().transform(i -> "Item-" + i) + .onItem().transform(String::toUpperCase); + + // Control the stream from anywhere + pauser.pause(); + + // Later, subscribe to the stream + AssertSubscriber sub = stream + .subscribe().withSubscriber(AssertSubscriber.create(Long.MAX_VALUE)); + + // No items while paused + await().pollDelay(Duration.ofMillis(100)).until(() -> true); + assertThat(sub.getItems()).isEmpty(); + + // Resume to receive items + pauser.resume(); + sub.awaitCompletion(); + assertThat(sub.getItems()).hasSize(1000); + // + } + + @Test + void conditionalFlow() { + // + DemandPauser pauser = new DemandPauser(); + AtomicInteger errorCount = new AtomicInteger(0); + + AssertSubscriber sub = Multi.createFrom().range(0, 100) + .pauseDemand().using(pauser) + .onItem().invoke(item -> { + // Simulate error-prone processing + if (item % 20 == 0 && item > 0) { + errorCount.incrementAndGet(); + pauser.pause(); // Pause on errors + } + }) + .subscribe().withSubscriber(AssertSubscriber.create(Long.MAX_VALUE)); + + // Stream pauses when errors occur + await().untilAsserted(() -> assertThat(errorCount.get()).isGreaterThan(0)); + await().untilAsserted(() -> assertThat(pauser.isPaused()).isTrue()); + + // Handle the error, then resume + errorCount.set(0); + pauser.resume(); + + // Stream continues + await().pollDelay(Duration.ofMillis(50)).until(() -> true); + assertThat(sub.getItems().size()).isGreaterThan(20); + // + } +} diff --git a/content/docs/smallrye-mutiny/latest/_examples/java/guides/operators/PollableSourceTest.java b/content/docs/smallrye-mutiny/latest/_examples/java/guides/operators/PollableSourceTest.java new file mode 100644 index 000000000..d5b10e736 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/_examples/java/guides/operators/PollableSourceTest.java @@ -0,0 +1,84 @@ +package guides.operators; + +import guides.extension.SystemOut; +import guides.extension.SystemOutCaptureExtension; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.subscription.Cancellable; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.awaitility.Awaitility.await; + +@ExtendWith(SystemOutCaptureExtension.class) +public class PollableSourceTest { + + private final ExecutorService executor = Executors.newSingleThreadExecutor(); + + @Test + public void test(SystemOut out) { // NOSONAR + // tag::code[] + PollableDataSource source = new PollableDataSource(); + // First creates a uni that emit the polled item. + // Because `poll` blocks, let's use a specific executor + Uni pollItemFromSource = Uni.createFrom().item(source::poll) + .runSubscriptionOn(executor); + // To get the stream of items, just repeat the uni indefinitely + Multi stream = pollItemFromSource.repeat().indefinitely(); + + Cancellable cancellable = stream.subscribe().with(item -> System.out.println("Polled item: " + item)); + // end::code[] + await().until(() -> source.counter.get() >= 4); + // tag::code[] + // ... later .. + // when you don't want the items anymore, cancel the subscription and close the source if needed. + cancellable.cancel(); + source.close(); + // end::code[] + } + + @SuppressWarnings("Convert2MethodRef") + @Test + public void test2(SystemOut out) { // NOSONAR + // tag::code2[] + PollableDataSource source = new PollableDataSource(); + Multi stream = Multi.createBy().repeating() + .supplier(source::poll) + .until(s -> s == null) + .runSubscriptionOn(executor); + + stream.subscribe().with(item -> System.out.println("Polled item: " + item)); + // end::code2[] + await().until(() -> source.counter.get() >= 5); + } + + private static class PollableDataSource { + + private final AtomicInteger counter = new AtomicInteger(); + + String poll() { + block(); + if (counter.get() == 5) { + return null; + } + return Integer.toString(counter.getAndIncrement()); + } + + private void block() { + try { + Thread.sleep(100); // NOSONAR + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + public void close() { + // do nothing. + } + } + +} diff --git a/content/docs/smallrye-mutiny/latest/_examples/java/guides/operators/RepetitionsTest.java b/content/docs/smallrye-mutiny/latest/_examples/java/guides/operators/RepetitionsTest.java new file mode 100644 index 000000000..c32dcf760 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/_examples/java/guides/operators/RepetitionsTest.java @@ -0,0 +1,31 @@ +package guides.operators; + +import io.smallrye.mutiny.Multi; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class RepetitionsTest { + + @Test + public void distinct() { + Multi multi = Multi.createFrom().items(1, 1, 2, 3, 4, 5, 5, 6, 1, 4, 4); + // tag::distinct[] + List list = multi + .select().distinct() + .collect().asList() + .await().indefinitely(); + // end::distinct[] + + // tag::repetition[] + List list2 = multi + .skip().repetitions() + .collect().asList() + .await().indefinitely(); + // end::repetition[] + assertThat(list).containsExactly(1, 2, 3, 4, 5, 6); + assertThat(list2).containsExactly(1, 2, 3, 4, 5, 6, 1, 4); + } +} diff --git a/content/docs/smallrye-mutiny/latest/_examples/java/guides/operators/ReplayTest.java b/content/docs/smallrye-mutiny/latest/_examples/java/guides/operators/ReplayTest.java new file mode 100644 index 000000000..268421678 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/_examples/java/guides/operators/ReplayTest.java @@ -0,0 +1,82 @@ +package guides.operators; + +import io.smallrye.mutiny.Multi; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ReplayTest { + + @Test + public void replayAll() { + // tag::replay-all[] + Multi upstream = Multi.createFrom().range(0, 10); + + Multi replay = Multi.createBy().replaying().ofMulti(upstream); + + List items_1 = replay.collect().asList().await().indefinitely(); + List items_2 = replay.collect().asList().await().indefinitely(); + // end::replay-all[] + + assertThat(items_1) + .isEqualTo(items_2) + .hasSize(10); + System.out.println(items_1); + } + + @Test + public void replayLast() { + // tag::replay-last[] + Multi upstream = Multi.createFrom().range(0, 10); + + Multi replay = Multi.createBy().replaying().upTo(3).ofMulti(upstream); + + List items_1 = replay.collect().asList().await().indefinitely(); + List items_2 = replay.collect().asList().await().indefinitely(); + // end::replay-last[] + + assertThat(items_1) + .isEqualTo(items_2) + .hasSize(3); + System.out.println(items_1); + } + + @Test + public void replayWithSeed() { + // tag::replay-seed[] + Multi upstream = Multi.createFrom().range(0, 10); + Iterable seed = Arrays.asList(-10, -5, -1); + + Multi replay = Multi.createBy().replaying().ofSeedAndMulti(seed, upstream); + + List items_1 = replay.collect().asList().await().indefinitely(); + List items_2 = replay.collect().asList().await().indefinitely(); + // end::replay-seed[] + + assertThat(items_1) + .isEqualTo(items_2) + .hasSize(13); + System.out.println(items_1); + } + + @Test + public void errors() { + // tag::replay-errors[] + Multi upstream = Multi.createBy().concatenating().streams( + Multi.createFrom().range(0, 10), + Multi.createFrom().failure(() -> new IOException("boom")) + ); + + Multi replay = Multi.createBy().replaying().upTo(3).ofMulti(upstream); + + replay.subscribe().with( + n -> System.out.println(" -> " + n), + failure -> System.out.println("Failed: " + failure.getMessage()), + () -> System.out.println("Completed")); + // end::replay-errors[] + } +} diff --git a/content/docs/smallrye-mutiny/latest/_examples/java/guides/operators/RunSubscriptionOnTest.java b/content/docs/smallrye-mutiny/latest/_examples/java/guides/operators/RunSubscriptionOnTest.java new file mode 100644 index 000000000..fbe4d38bf --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/_examples/java/guides/operators/RunSubscriptionOnTest.java @@ -0,0 +1,67 @@ +package guides.operators; + +import guides.extension.SystemOut; +import guides.extension.SystemOutCaptureExtension; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.infrastructure.Infrastructure; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Stream; + +import static org.awaitility.Awaitility.await; +import static org.hamcrest.CoreMatchers.is; + +@SuppressWarnings("Convert2MethodRef") +@ExtendWith(SystemOutCaptureExtension.class) +public class RunSubscriptionOnTest { + + @Test + public void testRunSubscriptionOn(SystemOut out) { + Executor executor = Infrastructure.getDefaultExecutor(); + AtomicBoolean completed = new AtomicBoolean(); + //tag::runSubscriptionOn[] + Multi.createFrom().items(() -> { + // called on a thread from the executor + return retrieveItemsFromSource(); + }) + .onItem().transform(this::applySomeOperation) + .runSubscriptionOn(executor) + .subscribe().with( + item -> System.out.println("Item: " + item), + Throwable::printStackTrace, + () -> completed.set(true) + ); + //end::runSubscriptionOn[] + + await().untilAtomic(completed, is(true)); + } + + @Test + public void testEmitOn(SystemOut out) { + Executor executor = Infrastructure.getDefaultExecutor(); + AtomicBoolean completed = new AtomicBoolean(); + //tag::emitOn[] + Multi.createFrom().items(this::retrieveItemsFromSource) + .emitOn(executor) + .onItem().transform(this::applySomeOperation) + .subscribe().with( + item -> System.out.println("Item: " + item), + Throwable::printStackTrace, + () -> completed.set(true) + ); + //end::emitOn[] + + await().untilAtomic(completed, is(true)); + } + + public String applySomeOperation(String s) { + return s.toUpperCase(); + } + + public Stream retrieveItemsFromSource() { + return Stream.of("a", "b", "c"); + } +} diff --git a/content/docs/smallrye-mutiny/latest/_examples/java/guides/operators/SelectAndSkipTest.java b/content/docs/smallrye-mutiny/latest/_examples/java/guides/operators/SelectAndSkipTest.java new file mode 100644 index 000000000..a8ffa4508 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/_examples/java/guides/operators/SelectAndSkipTest.java @@ -0,0 +1,65 @@ +package guides.operators; + +import io.smallrye.mutiny.Multi; +import org.junit.jupiter.api.Test; + +import java.time.Duration; + +import static org.assertj.core.api.Assertions.assertThat; + +public class SelectAndSkipTest { + + @Test + public void testSelect() { + Multi multi = Multi.createFrom().items(1, 2, 3, 4, 5, 6, 7, 8, 9); + + // tag::take-first[] + Multi firstThreeItems = multi.select().first(3); + // end::take-first[] + + // tag::take-last[] + Multi lastThreeItems = multi.select().last(3); + // end::take-last[] + + // tag::take-while[] + Multi takeWhile = multi.select().first(i -> i < 4); + // end::take-while[] + + // tag::take-for[] + Multi takeForDuration = multi.select().first(Duration.ofSeconds(1)); + // end::take-for[] + + assertThat(firstThreeItems.collect().asList().await().indefinitely()).containsExactly(1, 2, 3); + assertThat(takeWhile.collect().asList().await().indefinitely()).containsExactly(1, 2, 3); + assertThat(lastThreeItems.collect().asList().await().indefinitely()).containsExactly(7, 8, 9); + assertThat(takeForDuration.collect().asList().await().indefinitely()).containsExactly(1, 2, 3, 4, 5, 6, 7, 8, 9); + + } + + @Test + public void testSkip() { + Multi multi = Multi.createFrom().items(1, 2, 3, 4, 5, 6, 7, 8, 9); + + // tag::skip-first[] + Multi skipThreeItems = multi.skip().first(3); + // end::skip-first[] + + // tag::skip-last[] + Multi skipLastThreeItems = multi.skip().last(3); + // end::skip-last[] + + // tag::skip-while[] + Multi skipWhile = multi.skip().first(i -> i < 4); + // end::skip-while[] + + // tag::skip-for[] + Multi skipForDuration = multi.skip().first(Duration.ofSeconds(1)); + // end::skip-for[] + + assertThat(skipThreeItems.collect().asList().await().indefinitely()).containsExactly(4, 5, 6, 7, 8, 9); + assertThat(skipLastThreeItems.collect().asList().await().indefinitely()).containsExactly(1, 2, 3, 4 ,5, 6); + assertThat(skipWhile.collect().asList().await().indefinitely()).containsExactly(4, 5, 6, 7, 8, 9); + assertThat(skipForDuration.collect().asList().await().indefinitely()).isEmpty(); + + } +} diff --git a/content/docs/smallrye-mutiny/latest/_examples/java/guides/operators/SplitTest.java b/content/docs/smallrye-mutiny/latest/_examples/java/guides/operators/SplitTest.java new file mode 100644 index 000000000..bf2855bb9 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/_examples/java/guides/operators/SplitTest.java @@ -0,0 +1,45 @@ +package guides.operators; + +import io.smallrye.mutiny.Multi; +import org.junit.jupiter.api.Test; + +public class SplitTest { + + // tag::enum[] + enum Signals { + INPUT, + OUTPUT, + OTHER + } + // end::enum[] + + @Test + public void splitDemo() { + // tag::splits[] + Multi multi = Multi.createFrom().items( + "!a", "?b", "!c", "!d", "123", "?e" + ); + + var splitter = multi.split(Signals.class, s -> { + if (s.startsWith("?")) { + return Signals.INPUT; + } else if (s.startsWith("!")) { + return Signals.OUTPUT; + } else { + return Signals.OTHER; + } + }); + + splitter.get(Signals.INPUT) + .onItem().transform(s -> s.substring(1)) + .subscribe().with(signal -> System.out.println("input - " + signal)); + + splitter.get(Signals.OUTPUT) + .onItem().transform(s -> s.substring(1)) + .subscribe().with(signal -> System.out.println("output - " + signal)); + + splitter.get(Signals.OTHER) + .subscribe().with(signal -> System.out.println("other - " + signal)); + // end::splits[] + } +} diff --git a/content/docs/smallrye-mutiny/latest/_examples/java/guides/operators/UniJoinTest.java b/content/docs/smallrye-mutiny/latest/_examples/java/guides/operators/UniJoinTest.java new file mode 100644 index 000000000..a02897084 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/_examples/java/guides/operators/UniJoinTest.java @@ -0,0 +1,54 @@ +package guides.operators; + +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.groups.UniJoin; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.List; +import java.util.function.Supplier; + +class UniJoinTest { + + @Test + void joinAll() { + // tag::join-all[] + Uni a = Uni.createFrom().item(1); + Uni b = Uni.createFrom().item(2); + Uni c = Uni.createFrom().item(3); + + Uni> res = Uni.join().all(a, b, c).andCollectFailures(); + // end::join-all[] + + // tag::join-all-ff[] + res = Uni.join().all(a, b, c).andFailFast(); + // end::join-all-ff[] + } + + void joinFirst(Uni a, Uni b, Uni c) { + + // tag::join-first[] + Uni res = Uni.join().first(a, b, c).toTerminate(); + // end::join-first[] + + // tag::join-first-withitem[] + res = Uni.join().first(a, b, c).withItem(); + // end::join-first-withitem[] + + Supplier> supplier = () -> Uni.createFrom().item(63); + boolean someCondition = false; + + // tag::builder[] + UniJoin.Builder builder = Uni.join().builder(); + + while (someCondition) { + Uni uni = supplier.get(); + builder.add(uni); + } + + Uni> all = builder.joinAll().andFailFast(); + + Uni first = builder.joinFirst().withItem(); + // end::builder[] + } +} diff --git a/content/docs/smallrye-mutiny/latest/_examples/java/guides/operators/UnicastProcessorTest.java b/content/docs/smallrye-mutiny/latest/_examples/java/guides/operators/UnicastProcessorTest.java new file mode 100644 index 000000000..237b59345 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/_examples/java/guides/operators/UnicastProcessorTest.java @@ -0,0 +1,34 @@ +package guides.operators; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.helpers.test.AssertSubscriber; +import io.smallrye.mutiny.operators.multi.processors.UnicastProcessor; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class UnicastProcessorTest { + + @Test + public void test() { + // tag::code[] + UnicastProcessor processor = UnicastProcessor.create(); + Multi multi = processor + .onItem().transform(String::toUpperCase) + .onFailure().recoverWithItem("d'oh"); + + // Create a source of items that does not follow the request protocol + new Thread(() -> { + for (int i = 0; i < 1000; i++) { + processor.onNext(Integer.toString(i)); + } + processor.onComplete(); + }).start(); + + // end::code[] + AssertSubscriber subscriber = AssertSubscriber.create(Long.MAX_VALUE); + multi.subscribe().withSubscriber(subscriber) + .awaitCompletion() + .run(() -> assertThat(subscriber.getItems()).hasSize(1000)); + } +} diff --git a/content/docs/smallrye-mutiny/latest/_examples/java/guides/reference/PitfallsTest.java b/content/docs/smallrye-mutiny/latest/_examples/java/guides/reference/PitfallsTest.java new file mode 100644 index 000000000..1db116a84 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/_examples/java/guides/reference/PitfallsTest.java @@ -0,0 +1,64 @@ +package guides.reference; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import org.junit.jupiter.api.Test; + +import java.util.List; + +public class PitfallsTest { + + private Uni fetch(String id) { + return Uni.createFrom().item("Yolo"); + } + + @Test + public void noMagicScheduling() { + // tag::noMagicJoin[] + Uni> data = Uni.join().all( + fetch("abc"), + fetch("def"), + fetch("123")).andFailFast(); + // end::noMagicJoin[] + } + + @Test + public void inMemoryData() { + // tag::inMemoryData[] + Uni uni = Uni.createFrom().item(123); + Multi multi = Multi.createFrom().items(1, 2, 3, 4, 5); + // end::inMemoryData[] + } + + private List obtainValues(String key) { + return List.of(); + } + + // tag::suspiciousPublisher[] + public Multi fetchData(String key) { + List strings = obtainValues(key); + return Multi.createFrom().iterable(strings); + } + // end::suspiciousPublisher[] + + private Multi streamData(String key) { + return Multi.createFrom().items(""); + } + + private Multi extractData(String line) { + return Multi.createFrom().items(""); + } + + private Multi flatmap() { + // tag::flatmap-ism[] + Multi stream = streamData("abc"); + return stream.onItem().transformToMultiAndMerge(line -> { + if (line.trim().length() > 10) { + return extractData(line); + } else { + return Multi.createFrom().item("[N/A]"); + } + }); + // end::flatmap-ism[] + } +} diff --git a/content/docs/smallrye-mutiny/latest/_examples/java/tutorials/CreatingMultiTest.java b/content/docs/smallrye-mutiny/latest/_examples/java/tutorials/CreatingMultiTest.java new file mode 100644 index 000000000..b2c5e621a --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/_examples/java/tutorials/CreatingMultiTest.java @@ -0,0 +1,134 @@ +package tutorials; + +import guides.extension.SystemOut; +import guides.extension.SystemOutCaptureExtension; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.helpers.BlockingIterable; +import io.smallrye.mutiny.subscription.Cancellable; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.time.Duration; +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.IntStream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.awaitility.Awaitility.await; + +@SuppressWarnings({"Convert2MethodRef"}) +@ExtendWith(SystemOutCaptureExtension.class) +public class CreatingMultiTest { + + @Test + void pipeline(SystemOut out) { + // tag::pipeline[] + Multi.createFrom().items(1, 2, 3, 4, 5) + .onItem().transform(i -> i * 2) + .select().first(3) + .onFailure().recoverWithItem(0) + .subscribe().with(System.out::println); + // end::pipeline[] + assertThat(out.get()).contains("2", "4", "6"); + } + + @Test + void subscription(SystemOut out) { + Multi multi = Multi.createFrom().item(1); + // tag::subscription[] + Cancellable cancellable = multi + .subscribe().with( + item -> System.out.println(item), + failure -> System.out.println("Failed with " + failure), + () -> System.out.println("Completed")); + // end::subscription[] + assertThat(cancellable).isNotNull(); + assertThat(out.get()).contains("1", "Completed").doesNotContain("Failed"); + } + + @Test + public void creation() { + { + // tag::simple[] + Multi multiFromItems = Multi.createFrom().items(1, 2, 3, 4); + Multi multiFromIterable = Multi.createFrom().iterable(Arrays.asList(1, 2, 3, 4, 5)); + // end::simple[] + assertThat(multiFromItems.collect().asList().await().indefinitely()).containsExactly(1, 2, 3, 4); + assertThat(multiFromIterable.collect().asList().await().indefinitely()).containsExactly(1, 2, 3, 4, 5); + } + + { + // tag::supplier[] + AtomicInteger counter = new AtomicInteger(); + Multi multi = Multi.createFrom().items(() -> + IntStream.range(counter.getAndIncrement(), counter.get() * 2).boxed()); + // end::supplier[] + assertThat(multi.collect().asList().await().indefinitely()).containsExactly(0, 1); + assertThat(multi.collect().asList().await().indefinitely()).containsExactly(1, 2, 3); + assertThat(multi.collect().asList().await().indefinitely()).containsExactly(2, 3, 4, 5); + } + + { + // tag::failed[] + // Pass an exception directly: + Multi failed1 = Multi.createFrom().failure(new Exception("boom")); + + // Pass a supplier called for every subscriber: + Multi failed2 = Multi.createFrom().failure(() -> new Exception("boom")); + // end::failed[] + + assertThatThrownBy(() -> failed1.toUni().await().indefinitely()) + .hasMessageContaining("boom"); + assertThatThrownBy(() -> failed2.toUni().await().indefinitely()) + .hasMessageContaining("boom"); + + } + + { + // tag::empty[] + Multi multi = Multi.createFrom().empty(); + // end::empty[] + assertThat(multi.toUni().await().indefinitely()).isNull(); + } + + { + // tag::emitter[] + Multi multi = Multi.createFrom().emitter(em -> { + em.emit(1); + em.emit(2); + em.emit(3); + em.complete(); + }); + // end::emitter[] + assertThat(multi.collect().asList().await().indefinitely()).containsExactly(1, 2, 3); + } + + { + // tag::ticks[] + Multi ticks = Multi.createFrom().ticks().every(Duration.ofMillis(100)); + // end::ticks[] + BlockingIterable longs = ticks + .select().first(3) + .subscribe().asIterable(); + await().until(() -> longs.stream().count() == 3); + } + + { + // tag::generator[] + Multi sequence = Multi.createFrom().generator(() -> 1, (n, emitter) -> { + int next = n + (n / 2) + 1; + if (n < 50) { + emitter.emit(next); + + } else { + emitter.complete(); + } + return next; + }); + // end::generator[] + assertThat(sequence.collect().asList().await().indefinitely()).containsExactly(2, 4, 7, 11, 17, 26, 40, 61); + } + } + +} diff --git a/content/docs/smallrye-mutiny/latest/_examples/java/tutorials/CreatingUniTest.java b/content/docs/smallrye-mutiny/latest/_examples/java/tutorials/CreatingUniTest.java new file mode 100644 index 000000000..157e621ee --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/_examples/java/tutorials/CreatingUniTest.java @@ -0,0 +1,111 @@ +package tutorials; + +import guides.extension.SystemOut; +import guides.extension.SystemOutCaptureExtension; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.subscription.Cancellable; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.awaitility.Awaitility.await; + +@SuppressWarnings({ "ConstantConditions", "Convert2MethodRef" }) +@ExtendWith(SystemOutCaptureExtension.class) +public class CreatingUniTest { + + @Test + void pipeline(SystemOut out) { + // tag::pipeline[] + Uni.createFrom().item(1) + .onItem().transform(i -> "hello-" + i) + .onItem().delayIt().by(Duration.ofMillis(100)) + .subscribe().with(System.out::println); + // end::pipeline[] + await().untilAsserted(() -> + assertThat(out.get()).contains("hello-1") + ); + } + + @Test + void subscription(SystemOut out) { + Uni uni = Uni.createFrom().item(1); + // tag::subscription[] + Cancellable cancellable = uni + .subscribe().with( + item -> System.out.println(item), + failure -> System.out.println("Failed with " + failure)); + // end::subscription[] + assertThat(cancellable).isNotNull(); + assertThat(out.get()).contains("1").doesNotContain("Failed"); + } + + @Test + public void creation() { + { + // tag::simple[] + Uni uni = Uni.createFrom().item(1); + // end::simple[] + assertThat(uni.await().indefinitely()).isEqualTo(1); + } + + { + // tag::supplier[] + AtomicInteger counter = new AtomicInteger(); + Uni uni = Uni.createFrom().item(() -> counter.getAndIncrement()); + // end::supplier[] + assertThat(uni.await().indefinitely()).isEqualTo(0); + assertThat(uni.await().indefinitely()).isEqualTo(1); + assertThat(uni.await().indefinitely()).isEqualTo(2); + } + + { + // tag::failed[] + // Pass an exception directly: + Uni failed1 = Uni.createFrom().failure(new Exception("boom")); + + // Pass a supplier called for every subscriber: + Uni failed2 = Uni.createFrom().failure(() -> new Exception("boom")); + // end::failed[] + + assertThatThrownBy(() -> failed1.await().indefinitely()) + .hasMessageContaining("boom"); + assertThatThrownBy(() -> failed2.await().indefinitely()) + .hasMessageContaining("boom"); + + } + + { + // tag::null[] + Uni uni = Uni.createFrom().nullItem(); + // end::null[] + assertThat(uni.await().indefinitely()).isNull(); + } + + { + String result = "hello"; + // tag::emitter[] + Uni uni = Uni.createFrom().emitter(em -> { + // When the result is available, emit it + em.complete(result); + }); + // end::emitter[] + assertThat(uni.await().indefinitely()).isEqualTo("hello"); + } + + { + CompletionStage stage = CompletableFuture.completedFuture("hello"); + // tag::cs[] + Uni uni = Uni.createFrom().completionStage(stage); + // end::cs[] + assertThat(uni.await().indefinitely()).isEqualTo("hello"); + } + } + +} diff --git a/content/docs/smallrye-mutiny/latest/_examples/java/tutorials/HandlingFailuresTest.java b/content/docs/smallrye-mutiny/latest/_examples/java/tutorials/HandlingFailuresTest.java new file mode 100644 index 000000000..15d8e3f1d --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/_examples/java/tutorials/HandlingFailuresTest.java @@ -0,0 +1,105 @@ +package tutorials; + +import guides.extension.SystemOut; +import guides.extension.SystemOutCaptureExtension; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@SuppressWarnings("Convert2MethodRef") +@ExtendWith(SystemOutCaptureExtension.class) +public class HandlingFailuresTest { + + @Test + public void testInvoke(SystemOut out) { + Uni uni = Uni.createFrom().failure(new Exception("boom")); + Multi multi = Multi.createFrom().failure(new Exception("boom")); + // tag::invoke[] + Uni u = uni + .onFailure().invoke(failure -> log(failure)); + + Multi m = multi + .onFailure().invoke(failure -> log(failure)); + // end::invoke[] + assertThatThrownBy(() -> u.await().indefinitely()).hasMessageContaining("boom"); + assertThatThrownBy(() -> m.collect().asList() + .await().indefinitely()).hasMessageContaining("boom"); + assertThat(out.get()).contains("boom"); + } + + @Test + public void testTransform(SystemOut out) { + Uni uni = Uni.createFrom().failure(new Exception("boom")); + // tag::transform[] + Uni u = uni + .onFailure().transform(failure -> + new ServiceUnavailableException(failure)); + // end::transform[] + assertThatThrownBy(() -> u.await().indefinitely()) + .hasCauseExactlyInstanceOf(ServiceUnavailableException.class); + } + + @Test + public void testRecoverWithItem() { + Uni uni = Uni.createFrom().failure(new Exception("boom")); + // tag::recover-item[] + Uni u1 = uni + .onFailure().recoverWithItem("hello"); + + Uni u2 = uni + .onFailure().recoverWithItem(f -> getFallback(f)); + // end::recover-item[] + assertThat(u1.await().indefinitely()).isEqualTo("hello"); + assertThat(u2.await().indefinitely()).isEqualTo("hey"); + } + + @Test + public void testCompletionOnFailure() { + Multi multi = Multi.createFrom().failure(new Exception("boom")); + // tag::recover-completion[] + Multi m = multi + .onFailure().recoverWithCompletion(); + // end::recover-completion[] + assertThat(m.collect().asList().await().indefinitely()).isEmpty(); + } + + @Test + public void testSwitch() { + Uni uni = Uni.createFrom().failure(new Exception("boom")); + Multi multi = Multi.createFrom().failure(new Exception("boom")); + // tag::recover-switch[] + Uni u = uni + .onFailure().recoverWithUni(f -> getFallbackUni(f)); + + Multi m = multi + .onFailure().recoverWithMulti(f -> getFallbackMulti(f)); + // end::recover-switch[] + assertThat(u.await().indefinitely()).isEqualTo("hello"); + assertThat(m.collect().asList().await().indefinitely()).containsExactly("hey"); + } + + private Multi getFallbackMulti(Throwable f) { + return Multi.createFrom().item("hey"); + } + + private Uni getFallbackUni(Throwable f) { + return Uni.createFrom().item("hello"); + } + + private String getFallback(Throwable f) { + return "hey"; + } + + private void log(Throwable failure) { + System.out.println(failure.getMessage()); + } + + private static class ServiceUnavailableException extends Throwable { + public ServiceUnavailableException(Throwable ignored) { + } + } +} diff --git a/content/docs/smallrye-mutiny/latest/_examples/java/tutorials/ObserveTest.java b/content/docs/smallrye-mutiny/latest/_examples/java/tutorials/ObserveTest.java new file mode 100644 index 000000000..c32774f81 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/_examples/java/tutorials/ObserveTest.java @@ -0,0 +1,65 @@ +package tutorials; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import org.junit.jupiter.api.Test; + +import java.time.Duration; + +public class ObserveTest { + + @Test + public void test() { + Uni uni = Uni.createFrom().item("hello"); + Multi multi = Multi.createFrom().items("a", "b", "c"); + // tag::invoke[] + Uni u = uni.onItem() + .invoke(i -> System.out.println("Received item: " + i)); + + Multi m = multi.onItem() + .invoke(i -> System.out.println("Received item: " + i)); + // end::invoke[] + + // tag::call[] + multi + .onItem().call(i -> + Uni.createFrom().voidItem() + .onItem().delayIt().by(Duration.ofSeconds(1) + ) + ); + // end::call[] + + MyResource resource = new MyResource(); + // tag::close[] + multi + .onCompletion().call(() -> resource.close()); + // end::close[] + } + + @Test + public void all() { + Multi multi = Multi.createFrom().items("a", "b", "c"); + // tag::invoke-all[] + multi + .onSubscription() + .invoke(() -> System.out.println("⬇️ Subscribed")) + .onItem() + .invoke(i -> System.out.println("⬇️ Received item: " + i)) + .onFailure() + .invoke(f -> System.out.println("⬇️ Failed with " + f)) + .onCompletion() + .invoke(() -> System.out.println("⬇️ Completed")) + .onCancellation() + .invoke(() -> System.out.println("⬆️ Cancelled")) + .onRequest() + .invoke(l -> System.out.println("⬆️ Requested: " + l)); + // end::invoke-all[] + + } + + static class MyResource { + Uni close() { + return Uni.createFrom().voidItem(); + } + } +} diff --git a/content/docs/smallrye-mutiny/latest/_examples/java/tutorials/RetryTest.java b/content/docs/smallrye-mutiny/latest/_examples/java/tutorials/RetryTest.java new file mode 100644 index 000000000..be31f63cc --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/_examples/java/tutorials/RetryTest.java @@ -0,0 +1,61 @@ +package tutorials; + +import guides.extension.SystemOutCaptureExtension; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.time.Duration; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@SuppressWarnings("Convert2MethodRef") +@ExtendWith(SystemOutCaptureExtension.class) +public class RetryTest { + + @Test + public void testRetryAtMost() { + Uni uni = Uni.createFrom().failure(new Exception("boom")); + Multi multi = Multi.createFrom().failure(new Exception("boom")); + // tag::retry-at-most[] + Uni u = uni + .onFailure().retry().atMost(3); + Multi m = multi + .onFailure().retry().atMost(3); + // end::retry-at-most[] + assertThatThrownBy(() -> u.await().indefinitely()).hasMessageContaining("boom"); + assertThatThrownBy(() -> m.collect().asList() + .await().indefinitely()).hasMessageContaining("boom"); + } + + @Test + public void testRetryWithBackoff() { + Uni uni = Uni.createFrom().failure(new Exception("boom")); + // tag::retry-backoff[] + Uni u = uni + .onFailure().retry() + .withBackOff(Duration.ofMillis(100), Duration.ofSeconds(1)) + .atMost(3); + // end::retry-backoff[] + assertThatThrownBy(() -> u.await().indefinitely()) + .getCause() // Expected exception is wrapped in a java.util.concurrent.CompletionException + .hasMessageContaining("boom") + .hasSuppressedException(new IllegalStateException("Retries exhausted: 3/3")); + } + + @Test + public void testRetryUntil() { + Uni uni = Uni.createFrom().failure(new Exception("boom")); + // tag::retry-until[] + Uni u = uni + .onFailure().retry() + .until(f -> shouldWeRetry(f)); + // end::retry-until[] + assertThatThrownBy(() -> u.await().indefinitely()).hasMessageContaining("boom"); + } + + private boolean shouldWeRetry(Throwable ignored) { + return false; + } +} diff --git a/content/docs/smallrye-mutiny/latest/_examples/java/tutorials/TransformItemsAsyncTest.java b/content/docs/smallrye-mutiny/latest/_examples/java/tutorials/TransformItemsAsyncTest.java new file mode 100644 index 000000000..b8d0ae490 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/_examples/java/tutorials/TransformItemsAsyncTest.java @@ -0,0 +1,138 @@ +package tutorials; + +import guides.extension.SystemOut; +import guides.extension.SystemOutCaptureExtension; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import static org.assertj.core.api.Assertions.assertThat; + +@SuppressWarnings("Convert2MethodRef") +@ExtendWith(SystemOutCaptureExtension.class) +public class TransformItemsAsyncTest { + + interface MyAsyncService { + // tag::service[] + Uni invokeRemoteGreetingService(String name); + // end::service[] + } + + static class MyAsyncServiceImpl implements MyAsyncService { + + @Override + public Uni invokeRemoteGreetingService(String name) { + return Uni.createFrom().item(name); + } + } + + MyAsyncServiceImpl service = new MyAsyncServiceImpl(); + + + private Uni invokeRemoteGreetingService(String name) { + return service.invokeRemoteGreetingService(name); + } + + @Test + public void testCall() { + + Uni uni = Uni.createFrom().item("mutiny"); + // tag::call[] + Uni result = uni + .onItem().transformToUni(name -> invokeRemoteGreetingService(name)); + // end::call[] + + assertThat(result.await().indefinitely()).isEqualTo("mutiny"); + } + + @Test + public void testChain(SystemOut out) { + // tag::chain[] + Uni uni = Uni.createFrom().item("Cameron"); + uni + .onItem().transformToUni(name -> invokeRemoteGreetingService(name)) + .subscribe().with( + item -> System.out.println(item), // Print "Hello Cameron", + fail -> fail.printStackTrace()); // Print the failure stack trace + // end::chain[] + assertThat(out.get()).contains("Cameron"); + } + + + @Test + public void testUniToMulti(SystemOut out) { + Uni uni = Uni.createFrom().item("Cameron"); + // tag::uni-to-multi[] + Multi result = uni + .onItem().transformToMulti(item -> Multi.createFrom().items(item, item)); + // end::uni-to-multi[] + assertThat(result.collect().asList().await().indefinitely()).containsExactly("Cameron", "Cameron"); + + // tag::uni-to-multi-2[] + uni + .onItem().transformToMulti(item -> Multi.createFrom().items(item, item)) + .subscribe().with( + item -> System.out.println(item)); // Called twice + // end::uni-to-multi-2[] + assertThat(out.get()).contains("Cameron"); + } + + @Test + public void testMergeAndConcatUni() { + Multi multi = Multi.createFrom().items("a", "b"); + + // tag::merge-concat[] + Multi merged = multi + .onItem().transformToUniAndMerge(name -> invokeRemoteGreetingService(name)); + + Multi concat = multi + .onItem().transformToUniAndConcatenate(name -> invokeRemoteGreetingService(name)); + // end::merge-concat[] + + assertThat(merged.collect().asList().await().indefinitely()).containsExactly("a", "b"); + assertThat(concat.collect().asList().await().indefinitely()).containsExactly("a", "b"); + } + + @Test + public void testMergeAndConcatMulti() { + Multi multi = Multi.createFrom().items("a", "b"); + + // tag::merge-concat-multi[] + Multi merged = multi + .onItem().transformToMultiAndMerge(item -> someMulti(item)); + + Multi concat = multi + .onItem().transformToMultiAndConcatenate(item -> someMulti(item)); + // end::merge-concat-multi[] + + assertThat(merged.collect().asList().await().indefinitely()).containsExactly("a", "b"); + assertThat(concat.collect().asList().await().indefinitely()).containsExactly("a", "b"); + } + + @Test + public void testMergeConcurrency() { + Multi multi = Multi.createFrom().items("a", "b"); + + // tag::merge-concurrency[] + Multi limitConcurrent = multi + .onItem().transformToUni(item -> callService(item)) + .merge(2); + + Multi defaultConcurrent = multi + .onItem().transformToUni(item -> callService(item)) + .merge(); + // end::merge-concurrency[] + + assertThat(limitConcurrent.collect().asList().await().indefinitely()).containsExactly("a", "b"); + assertThat(defaultConcurrent.collect().asList().await().indefinitely()).containsExactly("a", "b"); + } + + private Uni callService(String item) { + return Uni.createFrom().item(item); + } + + private Multi someMulti(String item) { + return Multi.createFrom().item(item); + } +} diff --git a/content/docs/smallrye-mutiny/latest/_examples/java/tutorials/TransformItemsTest.java b/content/docs/smallrye-mutiny/latest/_examples/java/tutorials/TransformItemsTest.java new file mode 100644 index 000000000..e975e56a8 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/_examples/java/tutorials/TransformItemsTest.java @@ -0,0 +1,61 @@ +package tutorials; + +import guides.extension.SystemOut; +import guides.extension.SystemOutCaptureExtension; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import static org.assertj.core.api.Assertions.assertThat; + +@SuppressWarnings("Convert2MethodRef") +@ExtendWith(SystemOutCaptureExtension.class) +public class TransformItemsTest { + + @Test + public void testUniTransform(SystemOut out) { + // tag::uni-transform[] + Uni someUni = Uni.createFrom().item("hello"); + someUni + .onItem().transform(i -> i.toUpperCase()) + .subscribe().with( + item -> System.out.println(item)); // Print HELLO + // end::uni-transform[] + assertThat(out.get()).contains("HELLO"); + } + + @Test + public void testMultiTransform() { + Multi multi = Multi.createFrom().items("a", "b", "c"); + // tag::multi-transform[] + Multi m = multi.onItem().transform(i -> i.toUpperCase()); + // end::multi-transform[] + assertThat(m.collect().asList().await().indefinitely()).containsExactly("A", "B", "C"); + } + + @Test + public void testMultiTransformWithSubscription(SystemOut out) { + // tag::multi-transform-2[] + Multi someMulti = Multi.createFrom().items("a", "b", "c"); + someMulti + .onItem().transform(i -> i.toUpperCase()) + .subscribe().with( + item -> System.out.println(item)); // Print A B C + // end::multi-transform-2[] + assertThat(out.get()).contains("A", "B", "C"); + } + + @Test + public void testChain() { + Uni uni = Uni.createFrom().item("hello"); + // tag::chain[] + Uni u = uni + .onItem().transform(i -> i.toUpperCase()) + .onItem().transform(i -> i + "!"); + // end::chain[] + + assertThat(u.await().indefinitely()).isEqualTo("HELLO!"); + + } +} diff --git a/content/docs/smallrye-mutiny/latest/_examples/kotlin/guides/Coroutines.kt b/content/docs/smallrye-mutiny/latest/_examples/kotlin/guides/Coroutines.kt new file mode 100644 index 000000000..087e809bb --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/_examples/kotlin/guides/Coroutines.kt @@ -0,0 +1,62 @@ +package guides + +import io.smallrye.mutiny.Multi +import io.smallrye.mutiny.Uni +// tag::importStatements[] +import io.smallrye.mutiny.coroutines.asFlow +import io.smallrye.mutiny.coroutines.asMulti +import io.smallrye.mutiny.coroutines.asUni +import io.smallrye.mutiny.coroutines.awaitSuspending +// end::importStatements[] +import io.smallrye.mutiny.coroutines.uni +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf + +suspend fun uniAwaitSuspending() { + // tag::uniAwaitSuspending[] + val uni: Uni = Uni.createFrom().item("Mutiny ❤ Kotlin") + try { + // Available within suspend function and CoroutineScope + val item: String = uni.awaitSuspending() + } catch (failure: Throwable) { + // onFailure event happened + } + // end::uniAwaitSuspending[] +} + +@ExperimentalCoroutinesApi +suspend fun deferredAsUni() { + // tag::deferredAsUni[] + val deferred: Deferred = GlobalScope.async { "Kotlin ❤ Mutiny" } + val uni: Uni = deferred.asUni() + // end::deferredAsUni[] +} + +@ExperimentalCoroutinesApi +suspend fun multiAsFlow() { + // tag::multiAsFlow[] + val multi: Multi = Multi.createFrom().items("Mutiny", "❤", "Kotlin") + val flow: Flow = multi.asFlow() + // end::multiAsFlow[] +} + +suspend fun flowAsMulti() { + // tag::flowAsMulti[] + val flow: Flow = flowOf("Kotlin", "❤", "Mutiny") + val multi: Multi = flow.asMulti() + // end::flowAsMulti[] +} + +suspend fun uniBuilder() { + // tag::uniBuilder[] + // import io.smallrye.mutiny.coroutines.uni + coroutineScope { + val uni: Uni = uni { "λ 🚧" } + } + // end::uniBuilder[] +} diff --git a/content/docs/smallrye-mutiny/latest/_examples/kotlin/guides/FailureExt.kt b/content/docs/smallrye-mutiny/latest/_examples/kotlin/guides/FailureExt.kt new file mode 100644 index 000000000..69b40368f --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/_examples/kotlin/guides/FailureExt.kt @@ -0,0 +1,24 @@ +package guides + +import io.smallrye.mutiny.Multi +import io.smallrye.mutiny.Uni +import io.smallrye.mutiny.onFailure +import java.io.IOException + +fun uniReifiedOnFailure() { + // tag::uniReifiedOnFailure[] + // import io.smallrye.mutiny.onFailure + val uni = Uni.createFrom().failure(IOException("disk full")) + val recovered: Uni = uni.onFailure() + .recoverWithItem("recovered") + // end::uniReifiedOnFailure[] +} + +fun multiReifiedOnFailure() { + // tag::multiReifiedOnFailure[] + // import io.smallrye.mutiny.onFailure + val multi = Multi.createFrom().failure(IOException("disk full")) + val recovered: Multi = multi.onFailure() + .recoverWithItem("recovered") + // end::multiReifiedOnFailure[] +} diff --git a/content/docs/smallrye-mutiny/latest/_examples/kotlin/guides/MultiAwait.kt b/content/docs/smallrye-mutiny/latest/_examples/kotlin/guides/MultiAwait.kt new file mode 100644 index 000000000..c8b17f6ed --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/_examples/kotlin/guides/MultiAwait.kt @@ -0,0 +1,41 @@ +package guides + +import io.smallrye.mutiny.Multi +import io.smallrye.mutiny.coroutines.awaitList +import io.smallrye.mutiny.coroutines.awaitFirst +import io.smallrye.mutiny.coroutines.awaitFirstOrThrow +import io.smallrye.mutiny.coroutines.awaitLast +import io.smallrye.mutiny.coroutines.awaitLastOrThrow +import io.smallrye.mutiny.coroutines.awaitEach + +suspend fun multiAwaitList() { + // tag::multiAwaitList[] + val multi = Multi.createFrom().items("a", "b", "c") + val list: List = multi.awaitList() + // end::multiAwaitList[] +} + +suspend fun multiAwaitFirst() { + // tag::multiAwaitFirst[] + val multi = Multi.createFrom().items(1, 2, 3) + val first: Int? = multi.awaitFirst() + val firstOrThrow: Int = multi.awaitFirstOrThrow() + // end::multiAwaitFirst[] +} + +suspend fun multiAwaitLast() { + // tag::multiAwaitLast[] + val multi = Multi.createFrom().items(1, 2, 3) + val last: Int? = multi.awaitLast() + val lastOrThrow: Int = multi.awaitLastOrThrow() + // end::multiAwaitLast[] +} + +suspend fun multiAwaitEach() { + // tag::multiAwaitEach[] + val multi = Multi.createFrom().items(1, 2, 3) + multi.awaitEach { item -> + println("Processing $item") + } + // end::multiAwaitEach[] +} diff --git a/content/docs/smallrye-mutiny/latest/_examples/kotlin/guides/MultiExt.kt b/content/docs/smallrye-mutiny/latest/_examples/kotlin/guides/MultiExt.kt new file mode 100644 index 000000000..fca9c651a --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/_examples/kotlin/guides/MultiExt.kt @@ -0,0 +1,50 @@ +package guides + +import io.smallrye.mutiny.multi +import io.smallrye.mutiny.subscription.BackPressureStrategy +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.coroutineScope +import io.smallrye.mutiny.coroutines.multi as multiSuspend + +fun regularMultiBuilder() { + // tag::multiBuilder[] + // import io.smallrye.mutiny.multi + val multi = multi { + emit(1) + emit(2) + emit(3) + } + // end::multiBuilder[] +} + +fun multiBuilderWithBackPressure() { + // tag::multiBuilderBackPressure[] + // import io.smallrye.mutiny.multi + val multi = multi(backPressure = BackPressureStrategy.DROP) { + emit(1) + emit(2) + emit(3) + } + // end::multiBuilderBackPressure[] +} + +@ExperimentalCoroutinesApi +suspend fun suspendMultiBuilder() { + // tag::multiBuilderSuspend[] + // import io.smallrye.mutiny.coroutines.multi + val queryResult = fetchResults() + coroutineScope { + val multi = multiSuspend(context = this) { + queryResult.forEach { result -> emit(result.download()) } + } + } + // end::multiBuilderSuspend[] +} + +interface ResultHandle { + suspend fun download(): String +} + +suspend fun fetchResults(): Iterable { + TODO("Not yet implemented") +} diff --git a/content/docs/smallrye-mutiny/latest/_examples/kotlin/guides/TupleExt.kt b/content/docs/smallrye-mutiny/latest/_examples/kotlin/guides/TupleExt.kt new file mode 100644 index 000000000..ae6e56392 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/_examples/kotlin/guides/TupleExt.kt @@ -0,0 +1,21 @@ +package guides + +import io.smallrye.mutiny.Uni +import io.smallrye.mutiny.component1 +import io.smallrye.mutiny.component2 + +fun tupleDestructuring() { + // tag::tupleDestructuring[] + // import io.smallrye.mutiny.component1 + // import io.smallrye.mutiny.component2 + val uni = Uni.combine().all() + .unis( + Uni.createFrom().item("Alice"), + Uni.createFrom().item(30) + ).asTuple() + + val (name, age) = uni.await().indefinitely() + assert(name == "Alice") + assert(age == 30) + // end::tupleDestructuring[] +} diff --git a/content/docs/smallrye-mutiny/latest/_examples/kotlin/guides/UniExt.kt b/content/docs/smallrye-mutiny/latest/_examples/kotlin/guides/UniExt.kt new file mode 100644 index 000000000..988ba47dc --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/_examples/kotlin/guides/UniExt.kt @@ -0,0 +1,21 @@ +package guides + +import io.smallrye.mutiny.Uni +import io.smallrye.mutiny.replaceWithUnit +import io.smallrye.mutiny.uni + +fun uniReplaceWithUnit() { + val uni: Uni = Uni.createFrom().item("") + // tag::uniReplaceWithUnit[] + // import io.smallrye.mutiny.replaceWithUnit + val unitUni : Uni = uni.replaceWithUnit() + assert(unitUni.await().indefinitely() === Unit) + // end::uniReplaceWithUnit[] +} + +fun regularUniBuilder() { + // tag::uniBuilder[] + // import io.smallrye.mutiny.uni + val uni: Uni = uni { "λ 🚧" } + // end::uniBuilder[] +} diff --git a/content/docs/smallrye-mutiny/latest/_nav.yml b/content/docs/smallrye-mutiny/latest/_nav.yml new file mode 100644 index 000000000..b8ecc5e64 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/_nav.yml @@ -0,0 +1,69 @@ +title: SmallRye Mutiny +nav: +- page: index +- title: Tutorials + items: + - page: tutorials/getting-mutiny + - page: tutorials/hello-mutiny + - page: tutorials/creating-uni-pipelines + - page: tutorials/creating-multi-pipelines + - page: tutorials/observing-events + - page: tutorials/transforming-items + - page: tutorials/transforming-items-asynchronously + - page: tutorials/handling-failures + - page: tutorials/retrying + - page: tutorials/mutiny-workshop +- title: Guides + items: + - page: guides/imperative-to-reactive + - page: guides/reactive-to-imperative + - page: guides/unchecked-exceptions + - page: guides/filtering-items + - page: guides/collecting-items + - page: guides/grouping-items + - page: guides/take-skip-items + - page: guides/eliminate-duplicates-and-repetitions + - page: guides/handling-null + - page: guides/handling-timeouts + - page: guides/delaying-events + - page: guides/pagination + - page: guides/polling + - page: guides/emission-threads + - page: guides/emit-on-vs-run-subscription-on + - page: guides/completion-stage + - page: guides/rx + - page: guides/shortcut-methods + - page: guides/merging-and-concatenating-streams + - page: guides/combining-items + - page: guides/joining-unis + - page: guides/converters + - page: guides/testing + - page: guides/spies + - page: guides/custom-operators + - page: guides/dropped-exceptions + - page: guides/framework-integration + - page: guides/hot-streams + - page: guides/integrate-a-non-reactive-source + - page: guides/kotlin + - page: guides/logging + - page: guides/context-passing + - page: guides/replaying-multis + - page: guides/controlling-demand + - page: guides/multi-split + - page: guides/branching + - page: guides/broadcasting-to-multiple-subscribers +- title: Reference + items: + - page: reference/migrating-to-mutiny-2 + - page: reference/why-is-asynchronous-important + - page: reference/what-is-reactive-programming + - page: reference/what-makes-mutiny-different + - page: reference/uni-and-multi + - page: reference/going-reactive-a-few-pitfalls + - page: reference/publications + - title: Tags index + page: tags-index +- title: API + url: https://javadoc.io/doc/io.smallrye.reactive/mutiny/latest/index.html +- title: Discussions/Questions + url: https://github.com/smallrye/smallrye-mutiny/discussions diff --git a/content/docs/smallrye-mutiny/latest/assets/CF_logo_horizontal_single_reverse.svg b/content/docs/smallrye-mutiny/latest/assets/CF_logo_horizontal_single_reverse.svg new file mode 100644 index 000000000..2b8907471 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/assets/CF_logo_horizontal_single_reverse.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/latest/assets/logo-white.png b/content/docs/smallrye-mutiny/latest/assets/logo-white.png new file mode 100644 index 000000000..4472249bb Binary files /dev/null and b/content/docs/smallrye-mutiny/latest/assets/logo-white.png differ diff --git a/content/docs/smallrye-mutiny/latest/assets/logo-white.svg b/content/docs/smallrye-mutiny/latest/assets/logo-white.svg new file mode 100644 index 000000000..fa27cb24f --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/assets/logo-white.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/latest/assets/logo.png b/content/docs/smallrye-mutiny/latest/assets/logo.png new file mode 100644 index 000000000..f8e4ca32a Binary files /dev/null and b/content/docs/smallrye-mutiny/latest/assets/logo.png differ diff --git a/content/docs/smallrye-mutiny/latest/guides/branching.adoc b/content/docs/smallrye-mutiny/latest/guides/branching.adoc new file mode 100644 index 000000000..9391061fb --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/guides/branching.adoc @@ -0,0 +1,57 @@ += How to do branching in a reactive pipeline? +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,intermediate + +Mutiny and similar reactive programming libraries do not have _branching_ operators similar to `if / else` and `switch/case` statements in Java. + +This does not mean that we can't express _branching_ in a reactive pipeline, and the most classic way is to use a transformation to a `Uni` (also called `flatMap` in functional programming). + +== Expressing branches as Uni operations + +Suppose that we have a pipeline where a `Uni` is created from a random value, and suppose that we want to have a different processing pipeline depending on whether the value is odd or even. +Let's have these 2 `Uni`-returning methods to model different behaviors: + +[source,java] +---- +include::{examples-dir}/java/guides/BranchingTest.java[tags=branches,indent=0] +---- + +We can use the `transformToUni` operator to plug either method depending on the random number: + +[source,java] +---- +include::{examples-dir}/java/guides/BranchingTest.java[tags=pipeline,indent=0] +---- + +Having such a mapping function is a common pattern: it has conditional logic and each branch returns a `Uni` that represents the "sub-pipeline" of what each branch shall do. + +Note that such constructs are primarily relevant when asynchronous I/O are involved and that such asynchronous I/O operations are typically `Uni`-returning methods such as those found in the https://smallrye.io/smallrye-mutiny-vertx-bindings/[Mutiny Vert.x bindings]. + +[TIP] +==== + +There are other ways to express the "result" of a branch. +You could wrap results in a custom type or a container like `java.util.Optional`. + +You could also return a failed `Uni`, and later react by continuing with another `Uni`, another value, or retrying (which would model a loop!). + +==== + +== Branching in a Multi + +The case of `Multi` is even more interesting because a `null`-completed `Uni` is discarded from the stream by any of the `transformToUni++{++...}` methods: + +[source,java] +---- +include::{examples-dir}/java/guides/BranchingTest.java[tags=multi-pipeline,indent=0] +---- + +where `drop()` is as follows: + +[source,java] +---- +include::{examples-dir}/java/guides/BranchingTest.java[tags=drop,indent=0] +---- + +Any negative value is discarded in this `Multi` pipeline, while the positive even and odd numbers get forwarded to the subscriber. diff --git a/content/docs/smallrye-mutiny/latest/guides/broadcasting-to-multiple-subscribers.adoc b/content/docs/smallrye-mutiny/latest/guides/broadcasting-to-multiple-subscribers.adoc new file mode 100644 index 000000000..c0b359329 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/guides/broadcasting-to-multiple-subscribers.adoc @@ -0,0 +1,185 @@ += Broadcasting to multiple subscribers (like server-sent events, websockets, etc) +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,beginner + +It is a common requirement in event-driven applications to have multiple subscribers receiving events from a single events source: + +image::images/diagrams/mermaid-f74f7917dcf7.svg[mermaid diagram] + +A good example would be a periodic events stream where the events get pushed every second to multiple https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events[server-sent event] subscribers. + +Let us see how this can be done using Mutiny and the https://quarkus.io/[Quarkus framework]. + +[NOTE] +==== + +We use Quarkus to illustrate a classic real-world usage for the broadcast operator, but the same kind of construct +could be done with another framework or the https://vertx.io/[Vert.x toolkit]. + +==== + +== Use-case: dynamic subscribers + +We expose a HTTP endpoint for server-sent events, and each second we receive the current list of subscribers. +The endpoint is exposed on path `/hello/ticks/++{++id}` where `id` is an identifier for a subscriber. + +We could subscribe and follow the stream using https://httpie.io/[HTTPie] for subscriber `1`: + +[source,text] +---- +$ http --stream :8080/hello/ticks/1 +HTTP/1.1 200 OK +Content-Type: text/event-stream +X-SSE-Content-Type: text/plain +transfer-encoding: chunked + +data:1 + +data:1 + +data:1 + +data:1,2 + +data:1,2 +---- + +The first 3 ticks have just one subscriber (`data:1`), but then once another subscriber arrives at path `/hello/ticks/2` we get ticks with identifiers `1,2`. + +When the connection for subscriber `1` closes, we see the impact on subscriber `2`: + +[source,text] +---- +http --stream :8080/hello/ticks/2 +HTTP/1.1 200 OK +Content-Type: text/event-stream +X-SSE-Content-Type: text/plain +transfer-encoding: chunked + +data:1,2 + +data:1,2 + +data:2 + +data:2 + +data:2 +---- + +== Why is broadcasting required? + +Mutiny offers a publisher for periodic event streams: + +[source,java] +---- +var ticks = Multi.createFrom().ticks().every(Duration.ofSeconds(1)); +---- + +You might wonder why we couldn't simply build our Quarkus endpoints as in: + +[source,java] +---- +@Path("/hello") +public class GreetingResource { + + @GET + @Path("ticks/{id}") + @RestStreamElementType(MediaType.TEXT_PLAIN) + public Multi ticks(String id) { + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + // (rest of the pipeline...) + ; + } +} +---- + +The problem here is that instead of having a single events source to emit ticks, you will have as many as there are subscribers: + +image::images/diagrams/mermaid-cbd13fb6d19b.svg[mermaid diagram] + +You might be able to articulate some logic around such construct, but you will end up with many scheduled operations on the Quarkus thread pool. + +Even worse: if your source is not periodic ticks emitted by Mutiny but some event source (e.g., a Kafka topic, a message broker queue, etc) then you might add correctness issues (e.g., not all subscribers get all messages). + +== Broadcasting, listening to subscriptions and cancellations + +It is fortunately quite easy to express such construct by sharing a common `Multi`, and broadcasting events to each subscriber. + +Let's start with the HTTP endpoint: + +[source,java] +---- +private ConcurrentHashSet identifiers = new ConcurrentHashSet<>(); + +private Multi ticks = (...); + +@GET +@Path("ticks/{id}") +@RestStreamElementType(MediaType.TEXT_PLAIN) +public Multi ticks(String id) { + Log.info("New client with id " + id); + identifiers.add(id); + return ticks.onCancellation().invoke(() -> { + Log.info("Removing client with id " + id); + identifiers.remove(id); + }); +} +---- + +The `ticks` method registers a new client in `identifiers`, and removes it upon cancellation. +The returned `Multi` pipeline piggy-backs on top of the _shared_ `Multi`, which is built as follows: + +[source,java] +---- +private Multi ticks = Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .onItem().transform(tick -> { + Log.info("tick"); + return identifiers.stream().collect(Collectors.joining(",")); + }) + .onSubscription().invoke(() -> Log.info("Starting to emit ticks")) + .onCancellation().invoke(() -> Log.info("No more ticks")) + .broadcast() + .withCancellationAfterLastSubscriberDeparture() + .toAtLeast(1); +---- + +Here are a few observations. + +[arabic] +. For each periodic tick event, we assemble the current subscribers as a string of the form `"1,2,3"` with `.onItem().transform(...)`. +. We log an event when the periodic event stream starts (see `onSubscription().invoke(...)`). +. We log an event when the periodic event stream stops (see `onCancellation().invoke(...)`). +. We broadcast events to all subscribers, but: +[arabic] +.. there must be at least one subscriber before the stream starts, and +.. the stream is cancelled when the last subscriber departs. + +This construction is quite interesting because we don't emit ticks when there are no subscribers, and we stop it when there are none. + +If you play with such an example then you will see logs similar to these: + +[source,text] +---- +INFO [org.acm.GreetingResource] (vert.x-eventloop-thread-0) New client with id 1 +INFO [org.acm.GreetingResource] (vert.x-eventloop-thread-0) Starting to emit ticks +INFO [org.acm.GreetingResource] (executor-thread-1) tick +INFO [org.acm.GreetingResource] (executor-thread-1) tick +INFO [org.acm.GreetingResource] (vert.x-eventloop-thread-0) Removing client with id 1 +INFO [org.acm.GreetingResource] (vert.x-eventloop-thread-0) No more ticks +INFO [org.acm.GreetingResource] (vert.x-eventloop-thread-1) New client with id 1 +INFO [org.acm.GreetingResource] (vert.x-eventloop-thread-1) Starting to emit ticks +INFO [org.acm.GreetingResource] (executor-thread-1) tick +INFO [org.acm.GreetingResource] (executor-thread-1) tick +INFO [org.acm.GreetingResource] (executor-thread-1) tick +INFO [org.acm.GreetingResource] (vert.x-eventloop-thread-0) New client with id 2 +INFO [org.acm.GreetingResource] (executor-thread-1) tick +INFO [org.acm.GreetingResource] (executor-thread-1) tick +INFO [org.acm.GreetingResource] (vert.x-eventloop-thread-1) Removing client with id 1 +INFO [org.acm.GreetingResource] (executor-thread-1) tick +INFO [org.acm.GreetingResource] (executor-thread-1) tick +INFO [org.acm.GreetingResource] (executor-thread-1) tick +INFO [org.acm.GreetingResource] (vert.x-eventloop-thread-0) Removing client with id 2 +INFO [org.acm.GreetingResource] (vert.x-eventloop-thread-0) No more ticks +---- diff --git a/content/docs/smallrye-mutiny/latest/guides/collecting-items.adoc b/content/docs/smallrye-mutiny/latest/guides/collecting-items.adoc new file mode 100644 index 000000000..83772a291 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/guides/collecting-items.adoc @@ -0,0 +1,91 @@ += Collecting items from Multi +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,beginner + +There are cases where you want to accumulate the items from a `Multi` and produce a `Uni` as a final result. +This is also called a _reduction_ in functional programming. + +The `Uni` fires its item when the `Multi` completes. +Mutiny provides multiple operators to deal with that scenario. +They are available from the `collect()` group. +For example, you can store the items in a list, emit the list on completion, or use a Java `Collector` to customize the aggregation. + +[CAUTION] +==== + +Don't collect items from infinite streams or you will likely end with an out-of-memory failure! + +==== + +== Collecting items into a list + +One of the most common approaches to collect items is to store them in a list (`Uni++<++List++<++T++>>++`) +It emits the final list when the `Multi` completes. + +image::images/diagrams/mermaid-69aa56febc93.svg[mermaid diagram] + +How to achieve this with Mutiny? + +[source,java] +---- +include::{examples-dir}/java/guides/operators/CollectingItemsTest.java[tags=list,indent=0] +---- + +It's important to note that the returned type is a `Uni`. +It emits the list when the multi completes. + +== Collecting items into a map + +You can also collect the items into a `Map`. +In this case, you need to provide a function to compute the key for each item: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/CollectingItemsTest.java[tags=map,indent=0] +---- + +If the key mapper function returns the same key for multiple items, the last one with that key is stored in the final `Map`. +You can collect items in a _multimap_ to handle items with the same keys. + +== Collecting items into a multimap + +A multimap is a `Map++<++K, List++<++T++>>++.` +In the case of a conflicting key, it stores all the items in a list associated with that key. + +[source,java] +---- +include::{examples-dir}/java/guides/operators/CollectingItemsTest.java[tags=multimap,indent=0] +---- + +== Using a custom accumulator + +You can also use a custom _accumulator_ function: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/CollectingItemsTest.java[tags=accumulator,indent=0] +---- + +The `in` method receives two arguments: + +[arabic] +. a supplier providing the new instance of your collection/container +. the accumulator function + +You can also use a Java `Collector`. +For example, in the next example, count the number of items, and produce the final count as item: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/CollectingItemsTest.java[tags=collector,indent=0] +---- + +== Getting the first and last items + +While they are not strictly speaking collecting items, `collect().first()` and `collect().last()` allow retrieving the first and last item from a `Multi`: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/CollectingItemsTest.java[tags=first,indent=0] +---- diff --git a/content/docs/smallrye-mutiny/latest/guides/combining-items.adoc b/content/docs/smallrye-mutiny/latest/guides/combining-items.adoc new file mode 100644 index 000000000..05cf2c2b8 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/guides/combining-items.adoc @@ -0,0 +1,107 @@ += Combining items from streams +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,intermediate + +Combining items from various streams is an essential pattern in Reactive Programming. + +It associates the emitted items from multiple streams and emits an _aggregate_. +The downstream receives this _aggregate_ and can handle it smoothly. + +There are plenty of use cases, such as executing two tasks concurrently and waiting for both completions, getting the last items from different streams to build an always up-to-date view, and so on. + +== Combining Unis + +Imagine that you have two asynchronous operations to perform like 2 HTTP requests. +You want to send these requests and be notified when both have completed with their responses ready to be consumed. + +Of course, you could send the first request, wait for the response, and then send the second request. +If both requests are independent, we can do something better: send both concurrently and await for both completions! + +image::images/diagrams/mermaid-c54f60b30f42.svg[mermaid diagram] + +How can you achieve this with Mutiny? + +First, each request is a `Uni`, so we have: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/CombiningItemsTest.java[tags=invocations,indent=0] +---- + +Then, we want to combine both _responses_: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/CombiningItemsTest.java[tags=combination,indent=0] +---- + +This code creates a new `Uni` produced by combining `uniA` and `uniB`. +The responses are aggregated inside a `Tuple`: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/CombiningItemsTest.java[tags=subscription,indent=0] +---- + +The `tuple` aggregates the responses in the same order as the `Uni` sequence. + +If one of the `Uni` fails, so does the combination and you receive the failure: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/CombiningItemsTest.java[tags=invocations,indent=0] +include::{examples-dir}/java/guides/operators/CombiningItemsTest.java[tags=combination,indent=0] +include::{examples-dir}/java/guides/operators/CombiningItemsTest.java[tags=subscription,indent=0] +---- + +Using tuples is convenient but only works if you have less than 10 `Uni` objects. +If you want another structure or deal with 10 `Uni` objects or more then use `combineWith`: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/CombiningItemsTest.java[tags=combined-with,indent=0] +---- + +== Combining Multis + +Combining `Multis` consists of associating items from different stream per _index_: + +image::images/diagrams/mermaid-3d4459e3c6a6.svg[mermaid diagram] + +It associates the first items from the combined streams, then the second items: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/CombiningItemsTest.java[tags=combine-multi,indent=0] +---- + +As for `Uni`, you can aggregate the item into tuples (up to 9 items) or combine with a combinator function: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/CombiningItemsTest.java[tags=combine-multi-with,indent=0] +---- + +If one of the streams fails, the combined stream propagates the failure and stops the emission. +The combined stream completes as soon as one of the observed stream sends the completion event. + +[NOTE] +==== + +If one of the observed streams never emits any item then the combined stream will not emit anything. + +==== + +== Combining the latest items of Multis + +It can be useful to combine multiple `Multi` streams and receive the _latest_ items from each stream on every emission: + +image::images/diagrams/mermaid-f0001b5cde9f.svg[mermaid diagram] + +This is achieved using `latest()`: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/CombiningItemsTest.java[tags=combine-last,indent=0] +---- diff --git a/content/docs/smallrye-mutiny/latest/guides/completion-stage.adoc b/content/docs/smallrye-mutiny/latest/guides/completion-stage.adoc new file mode 100644 index 000000000..9ad73ee5a --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/guides/completion-stage.adoc @@ -0,0 +1,86 @@ += How to deal with CompletionStage? +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,intermediate + +`CompletionStage` and `CompletableFuture` are classes provided by Java to represent asynchronous actions. + +== Differences between Uni and CompletionStage + +While `CompletionStage` and `CompletableFuture` are close to `Uni` in terms of use case, there are some fundamental differences. + +`CompletionStage` are _eager_. +When a method returns a `CompletionStage,` the operation has already been triggered. +The outcome is used to complete the returned `CompletionStage`. +On the other side, `Unis` are lazy. +The operation is only triggered once there is a subscription. + +`CompletionStage` _caches_ the outcome. +So, once received, you can retrieve the result. +Every retrieval will get the same result. +With `Uni`, every subscription has the opportunity to re-trigger the operation and gets a different result. + +[TIP] +==== + +You can also _cache_ the outcome with `Uni.memoize().indefinitely()`. + +==== + +== From Uni to CompletionStage + +You can create a `CompletionStage` from `Uni` using `uni.subscribeAsCompletionStage()`. + +[source,java] +---- +include::{examples-dir}/java/guides/CompletionStageTest.java[tags=uni-subscribe-cs,indent=0] +---- + +It's important to understand that retrieving a `CompletionStage` subscribes to the `Uni`. +If you do this operation twice, it subscribes to the `Uni` twice and re-trigger the operation. + +[source,java] +---- +include::{examples-dir}/java/guides/CompletionStageTest.java[tags=uni-subscribe-cs-twice,indent=0] +---- + +== Creating a Uni from a CompletionStage + +To create a `Uni` from a `CompletionStage`, use `Uni.createFrom().completionStage(...)`. + +[source,java] +---- +include::{examples-dir}/java/guides/CompletionStageTest.java[tags=create-uni,indent=0] +---- + +As you can see, there are two versions. +The first one receives the `CompletionStage` directly, while the second one gets a supplier. +In the case of multiple subscriptions on the produced `Uni`, the supplier is called multiple times (once per subscription), and so can change the return `CompletionStage`. +It also delays the creation of the `CompletionStage` until there is a subscription, which only triggers the operation at that time. +If you pass the instance directly, it will always use the same one (even for multiple subscriptions) and triggers the operation even if there is no subscription. +For these reasons, it is generally better to use the variant accepting a supplier. + +Note that if the completion stage produces a `null` value, the resulting `Uni` emits `null` as item. +If the completion stages complete exceptionally, the failure is emitted by the resulting `Uni`. + +== Creating a Multi from a CompletionStage + +To create a `Multi` from a `CompletionStage`, use `Multi.createFrom().completionStage(...)`. +It produces: + +* a multi emitting an item and completing - if the value produced by the completion stage is not `null`, +* an empty multi if the value produced by the completion stage is `null`, +* a failed multi is completion stage is completed exceptionally. + +[source,java] +---- +include::{examples-dir}/java/guides/CompletionStageTest.java[tags=create-multi,indent=0] +---- + +For the same reason as for `Uni`, there are two versions: + +[arabic] +. one accepting a `CompletionStage` directly +. one accepting a `Supplier++<++CompletionStage++>++`, called at subscription-time, for every subscription. + +It is recommended to use the second version. diff --git a/content/docs/smallrye-mutiny/latest/guides/context-passing.adoc b/content/docs/smallrye-mutiny/latest/guides/context-passing.adoc new file mode 100644 index 000000000..4dcb0b0a1 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/guides/context-passing.adoc @@ -0,0 +1,90 @@ += Context passing +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,advanced + +Mutiny reactive pipelines let data flow from publishers to subscribers. + +In the vast majority of cases a publisher shall have _all_ required data, and operators shall perform processing based on item values. +For instance a network request shall be made with all request data known in advance, and response processing shall only depend on the response payload. + +That being said there are cases were this is not sufficient, and some data has to be carried along with items. +For instance one intermediary operator in a pipeline may have to make another networked request from which we need to extract some correlation identifier which will be used by another operator down the pipeline. +In such cases one will be tempted to forward tuples consisting of some item value plus some "extra" data. + +For such cases Mutiny offers a _subscriber-provided context_, so all operators involved in a subscription can share some form of _implicit data_. + +== What's in a context? + +A context is a simple key / value, in-memory storage. +Data can be queried, added and deleted from a context, as shown in the following snippet: + +[source,java] +---- +include::{examples-dir}/java/guides/ContextPassingTest.java[tags=contextManipulation,indent=0] +---- + +`Context` objects are thread-safe, and can be created from sequences of key / value pairs (as shown above), from a Java `Map`, or they can be created empty. + +Note that an empty-created context defers its internal storage allocation until the first call to `put`. +You can see `Context` as a glorified `ConcurrentHashMap` delegate, although this is an implementation detail and Mutiny might explore various internal storage strategies in the future. + +[TIP] +==== + +Contexts shall be primarily used to share transient data used for networked I/O processing such as correlation identifiers, tokens, etc. + +They should not be used as general-purpose data structures that are frequently updated and that hold large amounts of data. + +==== + +== How to access a context? + +Given a `Uni` or a `Multi`, a context can be accessed using the `withContext` operator, as in: + +[source,java] +---- +include::{examples-dir}/java/guides/ContextPassingTest.java[tags=contextSampleUsage,indent=0] +---- + +This operator builds a sub-pipeline using 2 parameters: the current `Uni` or `Multi` and the context. + +[IMPORTANT] +==== + +The function passed to `withContext` is called at subscription time. + +This means that the context has not had a chance to be updated by upstream operators yet, so be careful with what you do in the body of that function. + +==== + +There is another way to access the context by using the `attachContext` method: + +[source,java] +---- +include::{examples-dir}/java/guides/ContextPassingTest.java[tags=contextAttachedSampleUsage,indent=0] +---- + +This method materializes the context in the regular pipeline items using the wrapper `ItemWithContext` class. +The `get` method provides the item while the `context` method provides the context. + +== How to access a context at the pipeline source? + +The `Uni` and `Multi` _builder_ methods like `Multi.createFrom()` provide publishers, not operators, so they don't have the `withContext` method. + +The first option is to use the `Uni.createFrom().context(...)` or `Multi.createFrom().context(...)` general purpose method to materialize the context: + +[source,java] +---- +include::{examples-dir}/java/guides/ContextPassingTest.java[tags=builderUsage,indent=0] +---- + +The `context` method takes a function that accepts a `Context` and returns a pipeline. +This is very similar to the `deferred` builder. + +If you use an `emitter` builder then for both `Uni` and `Multi` cases the emitter object offers a `context` method to access the context: + +[source,java] +---- +include::{examples-dir}/java/guides/ContextPassingTest.java[tags=emitterUsage,indent=0] +---- diff --git a/content/docs/smallrye-mutiny/latest/guides/controlling-demand.adoc b/content/docs/smallrye-mutiny/latest/guides/controlling-demand.adoc new file mode 100644 index 000000000..2b4d098b4 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/guides/controlling-demand.adoc @@ -0,0 +1,158 @@ += Controlling the demand +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,advanced + +== Pacing the demand + +A subscription is used for 2 purposes: cancelling a request and demanding batches of items. + +The `Multi.paceDemand()` operator can be used to automatically issue requests at certain points in time. + +The following example issues requests of 25 items every 100ms: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/ControlDemandTest.java[tags=pacing,indent=0] +---- + +`FixedDemandPacer` is a simple _pacer_ with a fixed demand and a fixed delay. + +You can create more elaborated pacers by implementing the `DemandPacer` interface. +To do so you provide an initial request and a function to evaluate the next request which is evaluated based on the previous request and the number of items emitted since the last request: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/ControlDemandTest.java[tags=custom-pacer,indent=0] +---- + +The previous example is a custom pacer that doubles the demand and increases the delay for each new request. + +== Capping the demand requests + +The `capDemandsTo` and `capDemandUsing` operators can be used to cap the demand from downstream subscribers. + +The `capDemandTo` operator defines a maximum demand that can flow: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/ControlDemandTest.java[tags=capConstant,indent=0] +---- + +Here we cap requests to 50 items, so it takes 2 requests to get all 100 items of the upstream range. +The first request of 75 items is capped to a request of 50 items, leaving an outstanding demand of 25 items. +The second request of 25 items is added to the outstanding demand, resulting in a request of 50 items and completing the stream. + +You can also define a custom function that provides a capping value based on a custom formula, or based on earlier demand observations: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/ControlDemandTest.java[tags=capFunction,indent=0] +---- + +Here we have a function that requests 75% of the downstream requests. + +Note that the function must return a value `n` that satisfies `(0 ++<++ n ++<++= requested)` where `requested` is the downstream demand. + +== Pausing the demand + +The `Multi.pauseDemand()` operator provides fine-grained control over demand propagation in reactive streams. +Unlike cancellation, which terminates the subscription, pausing allows to suspend demand without unsubscribing from the upstream. +This is useful for implementing flow control patterns where item flow needs to be paused based on external conditions. + +=== Basic pausing and resuming + +The `pauseDemand()` operator works with a `DemandPauser` handle that allows to control the stream: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/PausingDemandTest.java[tags=basic,indent=0] +---- + +The `DemandPauser` provides methods to: + +* `pause()`: Stop propagating demand to upstream +* `resume()`: Resume demand propagation and deliver buffered items +* `isPaused()`: Check the current pause state + +Note that a few items may still arrive after pausing due to in-flight requests that were already issued to upstream. + +=== Starting in a paused state + +You can create a stream that starts paused and only begins flowing when explicitly resumed: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/PausingDemandTest.java[tags=initially-paused,indent=0] +---- + +This is useful when you want to prepare a stream but delay its execution until certain conditions are met. + +=== Late subscription + +By default, the upstream subscription happens immediately even when starting paused. +The `lateSubscription()` option delays the upstream subscription until the stream is resumed: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/PausingDemandTest.java[tags=late-subscription,indent=0] +---- + +=== Buffer strategies + +When a stream is paused, the operator stops requesting new items from upstream. +However, items that were already requested (due to downstream demand) may still arrive. +Buffer strategies control what happens to these in-flight items. + +The `pauseDemand()` operator supports three buffer strategies: `BUFFER` (default), `DROP`, and `IGNORE`. +Configuring any other strategy will throw an `IllegalArgumentException`. + +==== BUFFER strategy (default) + +Already-requested items are buffered while paused and delivered when resumed: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/PausingDemandTest.java[tags=buffer-strategy,indent=0] +---- + +You can configure the buffer size: + +* `bufferUnconditionally()`: Unbounded buffer +* `bufferSize(n)`: Buffer up to `n` items, then fail with buffer overflow + +When the buffer overflows, the stream fails with an `IllegalStateException`. + +*Important*: The buffer only holds items that were already requested from upstream before pausing. +When paused, no new requests are issued to upstream, so the buffer size is bounded by the outstanding demand at the time of pausing. + +==== DROP strategy + +Already-requested items are dropped while paused: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/PausingDemandTest.java[tags=drop-strategy,indent=0] +---- + +Items that arrive while paused are discarded, and when resumed, the stream continues requesting fresh items. + +==== IGNORE strategy + +Already-requested items continue to flow downstream while paused. +This strategy doesn't use any buffers. +It only pauses demand from being issued to upstream, but does not pause the flow of already requested items. + +=== Buffer management + +When using the BUFFER strategy, you can inspect and manage the buffer: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/PausingDemandTest.java[tags=buffer-management,indent=0] +---- + +The `DemandPauser` provides: + +* `bufferSize()`: Returns the current number of buffered items +* `clearBuffer()`: Clears the buffer (only works while paused), returns `true` if successful diff --git a/content/docs/smallrye-mutiny/latest/guides/converters.adoc b/content/docs/smallrye-mutiny/latest/guides/converters.adoc new file mode 100644 index 000000000..313692321 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/guides/converters.adoc @@ -0,0 +1,253 @@ += Using other reactive programming libraries +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,advanced + +You may need to integrate libraries exposing an API using other reactive programming libraries such as RX Java or Reactor. +Mutiny has a built-in conversion mechanism to ease that integration. + +== Picking the right dependency + +You need to add another dependency to access the converters. +Each artifact contains the converters for a specific reactive library. +Pick the right one and add it to your project: + +[.tabs] +==== + +Reactor:: ++ +-- + +[source,xml,subs="attributes+"] +---- + + + io.smallrye.reactive + mutiny-reactor + {versions-mutiny} + +---- + +-- + +RxJava 3:: ++ +-- + +[source,xml,subs="attributes+"] +---- + + + io.smallrye.reactive + mutiny-rxjava3 + {versions-mutiny} + +---- + +-- + +==== + +== Integration with Project Reactor + +https://projectreactor.io/[Project Reactor] is a popular reactive programming library. +It offers two types: `Mono` and `Flux,` both implementing Reactive Stream `Publisher`. + +To use the Reactor `++<++-++>++` Mutiny converter, add the following imports to your class: + +[source,java] +---- +import io.smallrye.mutiny.converters.multi.MultiReactorConverters; +import io.smallrye.mutiny.converters.uni.UniReactorConverters; +---- + +=== Converting a Flux or a Mono into a Multi + +Both `Flux` and `Mono` implement `Publisher`. +As a result, we can use the Reactive Streams interoperability to convert instances from `Flux++<++T++>++` and `Mono++<++T++>++` to `Multi++<++T++>++`: + +[source,java] +---- +include::{examples-dir}/java/guides/integration/ReactorTest.java[tags=reactor-multi-create,indent=0] +---- + +[WARNING] +==== + +Reactor still uses the legacy Reactive Streams APIs instead of `java.util.concurrent.Flow`, so you need to perform an adaptation. + +We recommend using the https://smallrye.io/smallrye-mutiny-zero/[Mutiny Zero Flow Adapters library] as in these examples (Maven coordinates `io.smallrye.reactive:mutiny-zero-flow-adapters`). + +==== + +=== Converting a Flux or a Mono into a Uni + +As you can create `Uni` from a `Publisher`, the same approach can be used to create `Uni` instances: + +[source,java] +---- +include::{examples-dir}/java/guides/integration/ReactorTest.java[tags=reactor-uni-create,indent=0] +---- + +When a `Flux` or `Mono` sends the _completion_ event without having emitted any item, the resulting `Uni` emits `null`. + +When converting a `Flux` to `Uni`, the resulting `Uni` emits the first item. +After that emission, it cancels the subscription to the `Flux`. + +=== Converting a Multi into a Flux or Mono + +Converting a `Multi` into a `Flux` or a `Mono` uses the Reactive Streams interoperability: + +[source,java] +---- +include::{examples-dir}/java/guides/integration/ReactorTest.java[tags=reactor-create-multi,indent=0] +---- + +=== Converting a Uni into a Flux or Mono + +Converting a `Uni` into a `Flux` or a `Mono` requires a converter, as `Uni` does not implement Reactive Streams. + +[source,java] +---- +include::{examples-dir}/java/guides/integration/ReactorTest.java[tags=reactor-create-uni,indent=0] +---- + +If the `Uni` emits `null`, it sends the _completion_ event. + +=== Using converter instead of Reactive Streams + +While Reactive Streams interoperability is convenient, Mutiny also provides converters to create `Flux` and `Mono` from `Uni` and `Multi`: + +[source,java] +---- +include::{examples-dir}/java/guides/integration/ReactorTest.java[tags=uni-export,indent=0] +include::{examples-dir}/java/guides/integration/ReactorTest.java[tags=multi-export,indent=0] +---- + +== Integration with RX Java 3 + +RxJava is another popular reactive programming library. +It offers 5 types: `Completable` (no item), `Single` (one item), `Maybe` (0 or 1 item), `Observable` (multiple items), `Flowable` (multiple items, implements Reactive Stream `Publisher`). + +To use the RxJava `++<++-++>++` Mutiny converters, add the following imports to your class: + +[source,java] +---- +import io.smallrye.mutiny.converters.multi.MultiRx3Converters; +import io.smallrye.mutiny.converters.uni.UniRx3Converters; +---- + +=== Converting an Observable or a Flowable into a Multi + +Both `Observable` and `Flowable` are item streams. +However, `Observable` does not implement `Publisher` and so does not have back-pressure support. + +To create `Multi` from an `Observable,` you need a specific converter: + +[source,java] +---- +include::{examples-dir}/java/guides/integration/RxJavaTest.java[tags=rx-multi-create-observable,indent=0] +---- + +Converting a `Flowable` is easier, as it's a `Publisher`: + +[source,java] +---- +include::{examples-dir}/java/guides/integration/RxJavaTest.java[tags=rx-multi-create-flowable,indent=0] +---- + +[WARNING] +==== + +Like Reactor, RxJava still uses the legacy Reactive Streams APIs instead of `java.util.concurrent.Flow`, so you need to perform an adaptation. + +==== + +=== Converting a Completable, Single or Maybe into a Multi + +To create a `Multi` from a `Completable,` `Single` or `Maybe` you need specific converters, as none of these types implement Reactive Streams. + +[source,java] +---- +include::{examples-dir}/java/guides/integration/RxJavaTest.java[tags=rx-multi-create-single,indent=0] +---- + +* Creating a `Multi` from a `Completable` always produces a `Multi++<++Void++>++` that only emits the _completion_ or _failure_ event. +* Creating a `Multi` from a `Single` produces a `Multi`. That `Multi` emits the item and then completes it. +* Creating a `Multi` from a `Maybe` produces a `Multi`. That `Multi` emits the item (if any) and then completes it. +If the `Maybe` is empty, then the created `Multi` emits the _completion_ event. + +When a `Completable,` `Single,` or `Maybe` emits a failure, then the resulting `Multi` emits that failure. + +=== Converting an Observable or a Flowable into a Uni + +To create a `Uni` from an `Observable,` you need to use a specific converter: + +[source,java] +---- +include::{examples-dir}/java/guides/integration/RxJavaTest.java[tags=rx-uni-create-observable,indent=0] +---- + +The creation from a `Flowable` can be done using the Reactive Streams interoperability: + +[source,java] +---- +include::{examples-dir}/java/guides/integration/RxJavaTest.java[tags=rx-uni-create-flowable,indent=0] +---- + +In both cases, it cancels the subscription to the `Flowable` or `Observable` after receiving the first item. +If the `Flowable` or `Observable` completes without items, the `Uni` emits a `null` item. + +=== Converting a Completable, Single or Maybe into a Uni + +To create a `Uni` from a `Completable,` `Single,` or `Maybe`, you need to use a specific converter: + +[source,java] +---- +include::{examples-dir}/java/guides/integration/RxJavaTest.java[tags=rx-uni-create-single,indent=0] +---- + +Converting a `Completable` to a `Uni` always produces a `Uni++<++Void++>++,` that emits either `null` once the `Completable` completes or the failure if it fails. +The `Maybe` to `Uni` conversion emits a `null` item if the `Maybe` completes without an item. + +=== Converting a Multi into a RX Java objects + +The conversion from a `Multi` to the various RX Java objects is done using converters: + +[source,java] +---- +include::{examples-dir}/java/guides/integration/RxJavaTest.java[tags=create-rx-from-multi,indent=0] +---- + +The creation of a `Completable` from a `Multi` discards all the items emitted by the `Multi`. +It only forwards the _completion_ or _failure_ event. + +Converting a `Multi` into a `Single` returns a `Single++<++Optional++<++T++>>++,` as the `Multi` may complete without items. +You can also produce a `Single++<++T++>++` and emit a _failure_ event if the `Multi` completes without items. +You can configure the thrown exception using `onEmptyThrow.` + +[TIP] +==== + +You can also create a `Flowable` from a `Multi` using: `Flowable.fromPublisher(multi)`. + +==== + +=== Converting a Uni into a RX Java type + +Similarly to the conversion from a `Multi` into an RX Type, converting a `Uni` requires a converter: + +[source,java] +---- +include::{examples-dir}/java/guides/integration/RxJavaTest.java[tags=create-rx-from-uni,indent=0] +---- + +The creation of a `Completable` from a `Uni` discards the item and sends the _completion_ signal after emission. + +Converting a `Uni` into a `Single` returns a `Single++<++Optional++<++T++>>++,` as the `Uni` may emit `null.` +You can also produce a `Single++<++T++>++` and emits a _failure_ event if the `Uni` sends `null.` +Configure the failure to forward using `failOnNull.` + +The creation of a `Maybe,` `Flowable,` or an `Observable` from a `Uni` produces an empty `Maybe,` `Flowable,` or `Observable` if the `Uni` emits `null.` +For `Flowable` and `Observable,` if the `Uni` emits a _non-null_ item, that item is emitted, followed immediately by the _completion_ signal. diff --git a/content/docs/smallrye-mutiny/latest/guides/custom-operators.adoc b/content/docs/smallrye-mutiny/latest/guides/custom-operators.adoc new file mode 100644 index 000000000..8c2baa638 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/guides/custom-operators.adoc @@ -0,0 +1,30 @@ += Can I have custom operators? +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,advanced + +Yes, but please write operators responsibly! + +Both `Uni` and `Multi` support custom operators using the `plug` operator. +Here is an example where we use a custom `Multi` operator that randomly drops items: + +[source,java] +---- +include::{examples-dir}/java/guides/PlugTest.java[tags=plug,indent=0] +---- + +with the operator defined as follows: + +[source,java] +---- +include::{examples-dir}/java/guides/PlugTest.java[tags=custom-operator,indent=0] +---- + +[CAUTION] +==== + +Custom operators are an advanced feature: when possible please use the existing operators and use helpers such as `stage` to write readable code. + +In the case of custom `Multi` operators it is wise to test them against the _Reactive Streams TCK_. + +==== diff --git a/content/docs/smallrye-mutiny/latest/guides/delaying-events.adoc b/content/docs/smallrye-mutiny/latest/guides/delaying-events.adoc new file mode 100644 index 000000000..c62577753 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/guides/delaying-events.adoc @@ -0,0 +1,68 @@ += How to delay events? +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,intermediate + +== Delaying Uni's item + +When you have a `Uni`, you can delay the item emission using `onItem().delayIt().by(...)`: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/DelayTest.java[tags=delay-by,indent=0] +---- + +You pass a duration. +When the item is received, it _waits for_ that duration before propagating it to the downstream consumer. + +You can also delay the item's emission based on another _companion_ `Uni`: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/DelayTest.java[tags=delay-until,indent=0] +---- + +The item is propagated downstream when the `Uni` returned by the function emits an item (possibly `null`). +If the function emits a failure (or throws an exception), this failure is propagated downstream. + +== Throttling a Multi + +Multi does not have a _delayIt_ operator because applying the same delay to all items is rarely what you want to do. +However, there are several ways to apply a delay in a `Multi`. + +First, you can use the `onItem().call()`, which delays the emission until the `Uni` produced the `call` emits an item. +For example, the following snippet delays all the items by 10 ms: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/DelayTest.java[tags=delay-multi,indent=0] +---- + +In general, you don't want to apply the same delay to all the items. +You can combine `call` with a random delay as follows: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/DelayTest.java[tags=delay-multi-random,indent=0] +---- + +Finally, you may want to throttle the items. +For example, you can introduce a (minimum) one-second delay between each item. +To achieve this, combine `Multi.createFrom().ticks()` and the multi to throttled: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/DelayTest.java[tags=throttling-multi,indent=0] +---- + +[TIP] +==== + +The `onOverflow().drop()` is used to avoid the _ticks_ to fail if the other stream (`multi`) is too slow. + +==== + +== Delaying other types of events + +We have looked at how to delay items, but you may need to delay other events, such as subscription or failure. +For these, use the `call` approach, and return a `Uni` that delay the event's propagation. diff --git a/content/docs/smallrye-mutiny/latest/guides/dropped-exceptions.adoc b/content/docs/smallrye-mutiny/latest/guides/dropped-exceptions.adoc new file mode 100644 index 000000000..10d0a1524 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/guides/dropped-exceptions.adoc @@ -0,0 +1,26 @@ += How to deal with dropped exceptions? +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,advanced + +There are a few corner cases where Mutiny cannot propagate an exception to a `Uni` or a `Multi` subscriber. + +Consider the following example: + +[source,java] +---- +include::{examples-dir}/java/guides/infrastructure/DroppedExceptionTest.java[tags=code,indent=0] +---- + +The `onCancellation().call(...)` method is called when the `Uni` subscription is cancelled. +The returned `Uni` is failed with a `IOException`, but since the subscription itself has been cancelled then there is no way to catch the exception. + +By default Mutiny reports such dropped exceptions to the standard error stream along with the corresponding stack trace. +You can change how these exceptions are handled using `Infrastructure.setDroppedExceptionHandler`. + +The following logs dropped exceptions to a logger: + +[source,java] +---- +include::{examples-dir}/java/guides/infrastructure/DroppedExceptionTest.java[tags=override-handler,indent=0] +---- diff --git a/content/docs/smallrye-mutiny/latest/guides/eliminate-duplicates-and-repetitions.adoc b/content/docs/smallrye-mutiny/latest/guides/eliminate-duplicates-and-repetitions.adoc new file mode 100644 index 000000000..4781db7ef --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/guides/eliminate-duplicates-and-repetitions.adoc @@ -0,0 +1,60 @@ += Eliminate duplicates and repetitions +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,beginner + +When observing a `Multi`, you may see duplicated items or repetitions. +The `multi.select()` and `multi.skip()` groups provide methods to only select distinct items or drop repetitions. + +== Selecting distinct + +The `.select().distinct()` operator removes all the duplicates. +As a result, the downstream only contains distinct items: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/RepetitionsTest.java[tags=distinct,indent=0] +---- + +If you have a stream emitting the `++{++1, 1, 2, 3, 4, 5, 5, 6, 1, 4, 4}` items, then applying `.select().distinct()` on such a stream produces: `++{++1, 2, 3, 4, 5, 6}`. + +[IMPORTANT] +==== + +The operator keeps a reference on all the emitted items, and so, it could lead to memory issues if the stream contains too many distinct items. + +==== + +[TIP] +==== + +By default, `select().distinct()` uses the `hashCode` method from the item's class. +You can pass a custom comparator for more advanced checks. + +==== + +If you have a stream emitting items of type `T`, where duplicates can be identified through an attribute of `T` of type `K`, +then an `extractor` of type `Function++<++T, K++>++` can be defined. Applying `.select().distinct(extractor)` on such a stream will +eliminate duplicates but have a lesser memory overhead as only the references to the extracted keys need to be kept, not the whole object. +A typical usage of this might be for a stream of records where uniqueness is determined by a UUID assigned to every record. + +== Skipping repetitions + +The `.skip().repetitions()` operator removes subsequent repetitions of an item: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/RepetitionsTest.java[tags=repetition,indent=0] +---- + +If you have a stream emitting the `++{++1, 1, 2, 3, 4, 5, 5, 6, 1, 4, 4}` items, then applying `.skip().repetitions()` on such a stream produces: `++{++1, 2, 3, 4, 5, 6, 1, 4}`. + +Unlike `.select().distinct()`, you can use this operator on large or infinite streams. + +[TIP] +==== + +By default, `skip().repetitions()` uses the `equals` method from the item's class. +You can pass a custom comparator for more advanced checks. + +==== diff --git a/content/docs/smallrye-mutiny/latest/guides/emission-threads.adoc b/content/docs/smallrye-mutiny/latest/guides/emission-threads.adoc new file mode 100644 index 000000000..4e3144069 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/guides/emission-threads.adoc @@ -0,0 +1,36 @@ += How to change the emission thread? +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,intermediate + +Except indicated otherwise, Mutiny invokes the next _stage_ using the thread emitting the event from upstream. +So, in the following code, the _transform_ stage is invoked from the thread emitting the event. + +[source,java] +---- +include::{examples-dir}/java/guides/operators/EmitOnTest.java[tags=example,indent=0] +---- + +You can switch to another thread using the `emitOn` operator. +The `emitOn` operator lets you switch the thread used to dispatch (upstream -++>++ downstream) events, so items, failure and completion events. +Just pass the _executor_ you want to use. + +[source,java] +---- +include::{examples-dir}/java/guides/operators/EmitOnTest.java[tags=code,indent=0] +---- + +[NOTE] +==== + +You cannot pass a specific thread, but you can implement a simple `Executor` dispatching on that specific thread, or use a _single threaded executor_. + +==== + +[WARNING] +==== + +Be careful as this operator can lead to concurrency problems with non thread-safe objects such as CDI request-scoped beans. +It might also break reactive-streams semantics with items being emitted concurrently. + +==== diff --git a/content/docs/smallrye-mutiny/latest/guides/emit-on-vs-run-subscription-on.adoc b/content/docs/smallrye-mutiny/latest/guides/emit-on-vs-run-subscription-on.adoc new file mode 100644 index 000000000..7588f9234 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/guides/emit-on-vs-run-subscription-on.adoc @@ -0,0 +1,43 @@ += What is the difference between emitOn and runSubscriptionOn? +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,intermediate + +The `emitOn` and `runSubscriptionOn` are 2 operators influencing on which threads the event are dispatched. +However, they target different types of events and different directions. + +== The case of emitOn + +`emitOn` takes events coming from upstream (items, completion, failure) and replays them downstream on a thread from the given executor. +Consequently, it affects where the subsequent operators execute (until another `emitOn` is used): + +[source,java] +---- +include::{examples-dir}/java/guides/operators/RunSubscriptionOnTest.java[tags=emitOn,indent=0] +---- + +The previous code produces the following sequence: + +image::images/diagrams/mermaid-18f9a50c0627.svg[mermaid diagram] + +[WARNING] +==== + +Be careful as this operator can lead to concurrency problems with non thread-safe objects such as CDI request-scoped beans. +It might also break reactive-streams semantics with items being emitted concurrently. + +==== + +== The case of runSubscriptionOn + +`runSubscriptionOn` applies to the subscription process. +It requests the upstream to run its subscription (call of the `subscribe` method on its own upstream) on a thread from the given executor: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/RunSubscriptionOnTest.java[tags=runSubscriptionOn,indent=0] +---- + +So, if we consider the previous code snippet, it produces the following sequence: + +image::images/diagrams/mermaid-76df06b35153.svg[mermaid diagram] diff --git a/content/docs/smallrye-mutiny/latest/guides/filtering-items.adoc b/content/docs/smallrye-mutiny/latest/guides/filtering-items.adoc new file mode 100644 index 000000000..e621770d9 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/guides/filtering-items.adoc @@ -0,0 +1,33 @@ += Filtering items from Multi +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,beginner + +When observing a `Multi`, you may not want to forward all the received items to the downstream. + +Use the `multi.select()` group to select items. + +[source,java] +---- +include::{examples-dir}/java/guides/operators/FilterTest.java[tags=filter,indent=0] +---- + +To _select_ items passing a given predicate, use `multi.select().where(predicate)`: + +`where` accepts a predicate called for each item. +If the predicate returns `true`, the item propagated downstream. +Otherwise, it drops the item. + +The predicate passed to `where` is synchronous. +The `when` method provides an asynchronous version: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/FilterTest.java[tags=test,indent=0] +---- + +`when` accepts a function called for each item. + +Unlike `where` where the predicate returns a boolean synchronously, the function returns a `Uni++<++Boolean++>++`. +It forwards the item downstream if the `uni` produced by the function emits `true`. +Otherwise, it drops the item. diff --git a/content/docs/smallrye-mutiny/latest/guides/framework-integration.adoc b/content/docs/smallrye-mutiny/latest/guides/framework-integration.adoc new file mode 100644 index 000000000..7dfbb8eb8 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/guides/framework-integration.adoc @@ -0,0 +1,32 @@ += How can I integrate Mutiny with my framework? +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,advanced + +Sometimes, Mutiny needs to execute tasks on other threads, such as monitoring time or delaying actions. +Most operators relying on such capacity let you pass either a `ScheduledExecutorService` or an `ExecutorService`. + +By default, Mutiny uses the a _cached_ thread pool as default executor, that creates new threads as needed, but reuse previously constructed threads when they are available. +A `ScheduledExecutorService` is also created but delegates the execution of the delayed/scheduled tasks to the default executor. + +In the case you want to integrate Mutiny with a thread pool managed by a platform, you can configure it using `Infrastructure.setDefaultExecutor()` method: + +[source,java] +---- +include::{examples-dir}/java/guides/infrastructure/InfrastructureTest.java[tags=infra,indent=0] +---- + +You can configure the default executor using the `Infrastructure.setDefaultExecutor` method: + +[source,java] +---- +include::{examples-dir}/java/guides/infrastructure/InfrastructureTest.java[tags=set-infra,indent=0] +---- + +[TIP] +==== + +If you are using Quarkus, the default executor is already configured to use the Quarkus worker thread pool. +Logging is also configured correctly. + +==== diff --git a/content/docs/smallrye-mutiny/latest/guides/grouping-items.adoc b/content/docs/smallrye-mutiny/latest/guides/grouping-items.adoc new file mode 100644 index 000000000..de10842de --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/guides/grouping-items.adoc @@ -0,0 +1,153 @@ += Grouping items from Multi +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,intermediate + +Mutiny provides several operators to group items from a `Multi` stream. +You can group items by a key function (similar to SQL's `GROUP BY`), split items into fixed-size chunks, or create time-based windows. + +The grouping operators are available from the `group()` method on `Multi`. + +== Grouping into Lists + +The `group().intoLists()` operator allows you to collect items into lists based on size or time. + +=== Fixed-size lists + +Use `group().intoLists().of(size)` to create fixed-size lists from the stream: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/GroupingItemsTest.java[tags=groupIntoLists,indent=0] +---- + +The last list may contain fewer items if the stream doesn't divide evenly. + +=== Time-based lists + +You can create time-based lists using `group().intoLists().every(Duration)`: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/GroupingItemsTest.java[tags=timeBasedListGrouping,indent=0] +---- + +=== Size and time-based lists + +You can combine both size and time constraints using `group().intoLists().of(size, Duration)`: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/GroupingItemsTest.java[tags=sizeAndTimeBasedListGrouping,indent=0] +---- + +This will emit a list when either the size limit is reached or the duration expires, whichever comes first. + +== Grouping into Multi Streams + +The `group().intoMultis()` operator allows you to create separate `Multi` streams from your data. Unlike `intoLists()` which materializes all items into memory, `intoMultis()` keeps items as streams, which is better for: + +* Applying stream transformations to each group +* Processing large groups without loading everything into memory +* Composing with other reactive operators + +=== Fixed-size Multi streams + +Use `group().intoMultis().of(size)` to create `Multi` streams of a fixed size: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/GroupingItemsTest.java[tags=groupIntoMultis,indent=0] +---- + +=== Time-based Multi streams + +You can create time-based windows using `group().intoMultis().every(Duration)`: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/GroupingItemsTest.java[tags=timeBasedGrouping,indent=0] +---- + +== Grouping by a key function + +The `group().by()` operator groups items based on a key function, emitting a `Multi++<++GroupedMulti++<++K, V++>>++` where each `GroupedMulti` represents a group of items sharing the same key. + +[source,java] +---- +include::{examples-dir}/java/guides/operators/GroupingItemsTest.java[tags=groupByKey,indent=0] +---- + +Each `GroupedMulti` has a `key()` method that returns the key for that group. +Items are distributed to groups based on the key function result. + +=== Using both key and value mappers + +You can transform items while grouping them by providing both a key mapper and a value mapper: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/GroupingItemsTest.java[tags=groupByKeyAndValue,indent=0] +---- + +== Processing groups with merge vs concatenate + +When processing groups, you need to decide how to combine the results back into a single stream. Similar to xref:tutorials/transforming-items-asynchronously.adoc[transforming items asynchronously], you can use either *merge* or *concatenate*: + +=== Using merge + +With merge, groups are processed concurrently - items from different groups can interleave in the output stream: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/GroupingItemsTest.java[tags=groupByWithMerge,indent=0] +---- + +[WARNING] +.Upstream request starvation with merge +==== + +When using `.merge(concurrency)` or similar merge operations after `group().by()`, +*the concurrency parameter must be greater than or equal to the number of groups that are created but not terminated*. + +If you create more groups than the concurrency limit allows, some groups cannot make progress while waiting for others to complete. +This leads to a *request starvation* where the upstream won't receive requests for emitting new items. + +==== + +==== Example causing request starvation + +[source,java] +---- +include::{examples-dir}/java/guides/operators/GroupingItemsTest.java[tags=groupByDeadlock,indent=0] +---- + +In this example, 10 groups are created but only 2 can be processed concurrently. +Groups 3-10 cannot make progress because the downstream subscriber is busy with groups 1-2. +Meanwhile, groups 1-2 may not complete because they're waiting for backpressure signals from the full pipeline. +The problem is even more exacerbated with infinite streams and infinite groups. + +==== How to avoid request starvation + +[arabic] +. *Set concurrency ++>++= number of groups*: If you know the maximum number of groups in advance, set the concurrency parameter to at least that number using `.merge(n)` +. *Use unbounded concurrency*: Call `.merge(Integer.MAX++_++VALUE)` to allow unlimited number of concurrent groups +. *Use concatenate instead*: Process groups sequentially (see below) + +=== Using concatenate + +With concatenate, groups are processed sequentially - each group must fully terminate before the next group can start processing: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/GroupingItemsTest.java[tags=groupByWithConcatenate,indent=0] +---- + +== Choosing between group().by() and split() + +Mutiny provides both `group().by()` and `split()` operators. Here's when to use each: + +* *Use `group().by()`* when you don't know the keys in advance and the number of groups is dynamic. +* *Use `split()`* when you know all possible keys upfront (defined by an enum) and you want individual `Multi` instances for each split. + +See the xref:guides/multi-split.adoc[splitting guide] for more details on `split()`. diff --git a/content/docs/smallrye-mutiny/latest/guides/handling-null.adoc b/content/docs/smallrye-mutiny/latest/guides/handling-null.adoc new file mode 100644 index 000000000..fa3abd3dc --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/guides/handling-null.adoc @@ -0,0 +1,40 @@ += How to handle null? +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,beginner + +The `Uni` type can emit `null` as item. + +While there are mixed feelings about `null`, it's part of the Java language and so handled in the `Uni` type. + +[IMPORTANT] +==== + +`Multi` does not support `null` items as it would break the compatibility with the _Reactive Streams_ protocol. + +==== + +Emitting `null` is convenient when returning `Uni++<++Void++>++`. +However, the downstream must expect `null` as item. + +Thus, `Uni` provides specific methods to handle `null` item. +`uni.onItem().ifNull()` lets you decide what you want to do when the received item is `null`: + +[source,java] +---- +include::{examples-dir}/java/guides/UniNullTest.java[tags=code,indent=0] +---- + +A symmetric group of methods is also available with `ifNotNull` which let you handle the case where the item is _not null_: + +[source,java] +---- +include::{examples-dir}/java/guides/UniNullTest.java[tags=code-not-null,indent=0] +---- + +[IMPORTANT] +==== + +While supported, emitting `null` should be avoided except for `Uni++<++Void++>++`. + +==== diff --git a/content/docs/smallrye-mutiny/latest/guides/handling-timeouts.adoc b/content/docs/smallrye-mutiny/latest/guides/handling-timeouts.adoc new file mode 100644 index 000000000..a687b0a37 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/guides/handling-timeouts.adoc @@ -0,0 +1,53 @@ += How to handle timeouts? +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,intermediate + +Unis are often used to represent asynchronous operations, like making an HTTP call. +So, it's not rare to need to add a timeout or a deadline on this kind of operation. +If we don't get a response (receive an item in the Mutiny lingo) before that deadline, we consider that the operation failed. + +We can then recover from this failure by using a fallback value, retrying, or any other failure handling strategy. + +To configure a timeout use `Uni.ifNoItem().after(Duration)`: + +[source,java] +---- +include::{examples-dir}/java/guides/UniTimeoutTest.java[tags=code,indent=0] +---- + +When the deadline is reached, you can do various actions. +First you can simply fail: + +[source,java] +---- +include::{examples-dir}/java/guides/UniTimeoutTest.java[tags=fail,indent=0] +---- + +A `TimeoutException` is propagated in this case. +So you can handle it specifically in the downstream: + +[source,java] +---- +include::{examples-dir}/java/guides/UniTimeoutTest.java[tags=fail-recover,indent=0] +---- + +You can also pass a custom exception: + +[source,java] +---- +include::{examples-dir}/java/guides/UniTimeoutTest.java[tags=fail-with,indent=0] +---- + +Failing and recovering might be inconvenient. +So, you can pass a fallback item or `Uni` directly: + +[source,java] +---- +include::{examples-dir}/java/guides/UniTimeoutTest.java[tags=fallback,indent=0] +---- + +[source,java] +---- +include::{examples-dir}/java/guides/UniTimeoutTest.java[tags=fallback-uni,indent=0] +---- diff --git a/content/docs/smallrye-mutiny/latest/guides/hot-streams.adoc b/content/docs/smallrye-mutiny/latest/guides/hot-streams.adoc new file mode 100644 index 000000000..87c93ce06 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/guides/hot-streams.adoc @@ -0,0 +1,26 @@ += Hot streams +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,advanced + +In a _cold_ stream, the stream is created when one subscriber subscribes to the stream. +So, if no one subscribes, the actual stream is not created, saving resources (that would be wasted because nobody is interested in the items). + +In a _hot_ stream, the stream exists before subscribers subscribe. +The stream emits items even if no subscribers observe the stream. +If there are no subscribers, the items are just dropped. +Subscribers only get items emitted after their subscription, meaning that any previous items would not be received. + +To create a hot stream, you can use `io.smallrye.mutiny.operators.multi.processors.BroadcastProcessor` that: + +* drops items if no subscribers are present, +* forwards items to the set of observing subscribers. + +[source,java] +---- +include::{examples-dir}/java/guides/operators/BroadcastProcessorTest.java[tags=code,indent=0] +---- + +Note that the `BroadcastProcessor` subscribes to the _hot_ source aggressively and without back-pressure. +However, the `BroadcastProcessor` enforces the back-pressure protocol per subscriber. +If a subscriber is not ready to handle an item emitted by the _hot_ source, an `io.smallrye.mutiny.subscription.BackPressureFailure` is forwarded to this subscriber. diff --git a/content/docs/smallrye-mutiny/latest/guides/imperative-to-reactive.adoc b/content/docs/smallrye-mutiny/latest/guides/imperative-to-reactive.adoc new file mode 100644 index 000000000..535317c49 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/guides/imperative-to-reactive.adoc @@ -0,0 +1,71 @@ += From imperative to reactive +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,advanced + +If you use Mutiny, there is a good chance you may want to avoid blocking the caller thread. + +In a _pure_ reactive application, the application logic is executed on one of the few I/O threads, and blocking one of these would have dramatic consequences. +So, here is the big question: _how do you deal with blocking code?_ + +Let's imagine you have blocking code (e.g., connecting to a database using JDBC, reading a file from the file system...), and you want to integrate that into your reactive pipelines while avoiding blocking. +You would need to isolate such blocking parts of your code and run these parts on worker threads. + +Mutiny provides two operators to customize the threads used to handle events: + +* `runSubscriptionOn` - to configure the thread used to execute the code happening at subscription-time +* `emitOn` - to configure the thread used to dispatch events downstream + +== Running blocking code on subscription + +It is very usual to deal with the blocking call during the subscription. +In this case, the `runSubscription` operator is what you need: + +[source,java] +---- +include::{examples-dir}/java/guides/integration/ImperativeToReactiveTest.java[tags=uni-runSubscriptionOn,indent=0] +---- + +The code above creates a Uni that will supply the item using a blocking call, here the `invokeRemoteServiceUsingBlockingIO` method. +To avoid blocking the subscriber thread, it uses `runSubscriptionOn` which switches the thread and call `invokeRemoteServiceUsingBlockingIO` on another thread. +Here we pass the default worker thread pool, but you can use your own executor. + +[TIP] +==== + +What's that default worker pool? + +In the previous snippet, you may wonder about `Infrastructure.getDefaultWorkerPool()`. +Mutiny allows the underlying platform to provide a default worker pool. +`Infrastructure.getDefaultWorkerPool()` provides access to this pool. + +==== + +If the underlying platform does not provide a pool, a default one is used. + +Note that `runSubscriptionOn` does not subscribe to the Uni. +It specifies the executor to use when a subscription happens. + +While the snippet above uses `Uni`, you can also use `runSubscriptionOn` on a `Multi`. + +== Executing blocking calls on event + +Using `runSubscriptionOn` works when the blocking operation happens at subscription time. +But, when dealing with `Multi` and need to execute blocking operations for each item, you need to use `emitOn`. + +While `runSubscriptionOn` runs the subscription on the given executor, `emitOn` configures the executor used to propagate downstream the items, failure and completion events: + +[source,java] +---- +include::{examples-dir}/java/guides/integration/ImperativeToReactiveTest.java[tags=multi-emitOn,indent=0] +---- + +`emitOn` is also available on `Uni`. + +[WARNING] +==== + +Be careful as this operator can lead to concurrency problems with non thread-safe objects such as CDI request-scoped beans. +It might also break reactive-streams semantics with items being emitted concurrently. + +==== diff --git a/content/docs/smallrye-mutiny/latest/guides/integrate-a-non-reactive-source.adoc b/content/docs/smallrye-mutiny/latest/guides/integrate-a-non-reactive-source.adoc new file mode 100644 index 000000000..0b0a7982c --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/guides/integrate-a-non-reactive-source.adoc @@ -0,0 +1,20 @@ += How can I create a Multi from a non-reactive source? +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,advanced + +The `UnicastProcessor` is an implementation of `Multi` that lets you enqueue items in a queue. + +The items are then dispatched to the subscriber using the request protocol. +While this pattern is against the idea of back-pressure, it lets you connect sources of data that do not support back-pressure with your subscriber. + +In the following example, the `UnicastProcessor` is used by a thread emitting items. +These items are enqueued in the processor and replayed when the subscriber is connected, following the request protocol. + +[source,java] +---- +include::{examples-dir}/java/guides/operators/UnicastProcessorTest.java[tags=code,indent=0] +---- + +By default, the `UnicastProcessor` uses an unbounded queue. +You can also pass a fixed size queue that would reject the items once full. diff --git a/content/docs/smallrye-mutiny/latest/guides/joining-unis.adoc b/content/docs/smallrye-mutiny/latest/guides/joining-unis.adoc new file mode 100644 index 000000000..eaaa1a204 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/guides/joining-unis.adoc @@ -0,0 +1,67 @@ += Joining several unis +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,intermediate + +A `Uni` represents an operation that either emits a value or a failure. +Examples of operations that fit into a `Uni` include: HTTP client requests, database `insert` queries, sending messages to a broker, etc. + +It is common to trigger several _concurrent_ operations, then _join_ on the results. +For instance you can make HTTP requests to 3 different HTTP APIs, then collect all HTTP responses. +Or you can just take the response from the one who was the fastest. + +`Uni` offers the `join` group to assemble all results from a list of `Uni`, pick the first one that terminates, or pick the first one that terminates with a value. + +== Joining multiple unis + +Given multiple `Uni`, you can join them all and obtain a `Uni` that emits a list of values: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/UniJoinTest.java[tags=join-all,indent=0] +---- + +The assembled values are in the same order as the list of unis. +The last call to `.andCollectFailures()` specifies that if one or several `Uni` fail, then the failures are assembled in a `CompositeException`. + +Sometimes you just want to _fail fast_ if any of the `Uni` fails, and not wait for all unis to terminate: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/UniJoinTest.java[tags=join-all-ff,indent=0] +---- + +When any `Uni` fails, then the failure is directly forwarded as a failure of `res`. + +== Joining on the first Uni + +In some cases you do not want to have all the results but just that of the first `Uni` to respond. +There are actually 2 different cases, depending on whether you want the result of the first `Uni` that emits a value, or just the result of the first `Uni` to terminate. + +If you want to get the first `Uni` that terminates: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/UniJoinTest.java[tags=join-first,indent=0] +---- + +If you want to have the first `Uni` that emits a value (and forget the first failures), then: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/UniJoinTest.java[tags=join-first-withitem,indent=0] +---- + +When all unis fail then `res` fails with a `CompositeException` that reports all failures. + +== Using a builder object + +There are situations where it can be more convenient to gather the unis to join in an iterative fashion. +For this purpose you can use a builder object, as in: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/UniJoinTest.java[tags=builder,indent=0] +---- + +The builder offers `joinAll()` and `joinFirst()` methods. diff --git a/content/docs/smallrye-mutiny/latest/guides/kotlin.adoc b/content/docs/smallrye-mutiny/latest/guides/kotlin.adoc new file mode 100644 index 000000000..06fd1b4e3 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/guides/kotlin.adoc @@ -0,0 +1,205 @@ += Kotlin integration +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,intermediate + +The module `mutiny-kotlin` provides an integration with Kotlin for use with coroutines and convenient language features. + +There are extension methods available for converting between Mutiny and Kotlin (coroutine) types. +For implementation details please have also a look to these methods' documentation. + +== Dependency coordinates + +The coroutine extension functions are shipped in the package `io.smallrye.mutiny.coroutines`. + +[source,kotlin] +---- +include::{examples-dir}/kotlin/guides/Coroutines.kt[tags=importStatements,indent=0] +---- + +You need to add the following dependency to your project: + +[.tabs] +==== + +Maven:: ++ +-- + +[source,xml,subs="attributes+"] +---- + + io.smallrye.reactive + mutiny-kotlin + {versions-mutiny} + +---- + +-- + +Gradle (Kotlin):: ++ +-- + +[source,kotlin,subs="attributes+"] +---- +implementation("io.smallrye.reactive:mutiny-kotlin:{versions-mutiny}") +---- + +-- + +Gradle (Groovy):: ++ +-- + +[source,groovy,subs="attributes+"] +---- +implementation "io.smallrye.reactive:mutiny-kotlin:{versions-mutiny}" +---- + +-- + +==== + +== Awaiting a Uni in coroutines + +Within a coroutine or suspend function you can easily await Uni events in a suspended way: + +[source,kotlin] +---- +include::{examples-dir}/kotlin/guides/Coroutines.kt[tags=uniAwaitSuspending,indent=0] +---- + +== Processing a Multi as Flow + +The coroutine `Flow` type matches `Multi` semantically, even though it isn't a feature complete reactive streams implementation. +You can process a `Multi` as `Flow` as follows: + +[source,kotlin] +---- +include::{examples-dir}/kotlin/guides/Coroutines.kt[tags=multiAsFlow,indent=0] +---- + +[NOTE] +==== + +There's no flow control availabe for Kotlin's `Flow`. Published items are buffered for consumption using a coroutine `Channel`. +The buffer size and overflow strategy of that `Channel` can be configured using optional arguments: +`Multi.asFlow(bufferCapacity = Channel.UNLIMITED, bufferOverflowStrategy = BufferOverflow.SUSPEND)`, +for more details please consult the method documentation. + +==== + +== Providing a Deferred value as Uni + +The other way around is also possible, let a Deferred become a Uni: + +[source,kotlin] +---- +include::{examples-dir}/kotlin/guides/Coroutines.kt[tags=deferredAsUni,indent=0] +---- + +== Creating a Multi from a Flow + +Finally, creating a Multi from a Flow is also possible: + +[source,kotlin] +---- +include::{examples-dir}/kotlin/guides/Coroutines.kt[tags=flowAsMulti,indent=0] +---- + +== Awaiting Multi results in coroutines + +You can consume `Multi` results directly from suspend functions without converting to `Flow` first: + +[source,kotlin] +---- +include::{examples-dir}/kotlin/guides/MultiAwait.kt[tags=multiAwaitList,indent=0] +---- + +[source,kotlin] +---- +include::{examples-dir}/kotlin/guides/MultiAwait.kt[tags=multiAwaitFirst,indent=0] +---- + +[source,kotlin] +---- +include::{examples-dir}/kotlin/guides/MultiAwait.kt[tags=multiAwaitLast,indent=0] +---- + +[source,kotlin] +---- +include::{examples-dir}/kotlin/guides/MultiAwait.kt[tags=multiAwaitEach,indent=0] +---- + +== Language convenience + +=== Unit instead of Void (null) value + +Kotlin has a special value type `Unit` similar to Java's `Void`. +While regular `Uni++<++Void++>++` holds a `null` item, you can get a `Unit` by using the extension function `replaceWithUnit()`: + +[source,kotlin] +---- +include::{examples-dir}/kotlin/guides/UniExt.kt[tags=uniReplaceWithUnit,indent=0] +---- + +=== Uni builder + +Building a `Uni` from Kotlin code can easily be achieved using the following builders available as regular or coroutine variant: + +[source,kotlin] +---- +include::{examples-dir}/kotlin/guides/UniExt.kt[tags=uniBuilder,indent=0] +---- + +[source,kotlin] +---- +include::{examples-dir}/kotlin/guides/Coroutines.kt[tags=uniBuilder,indent=0] +---- + +=== Multi builder + +Building a `Multi` from Kotlin code can be achieved using the `multi` builder, available as regular or coroutine variant: + +[source,kotlin] +---- +include::{examples-dir}/kotlin/guides/MultiExt.kt[tags=multiBuilder,indent=0] +---- + +The builder accepts an optional back-pressure strategy: + +[source,kotlin] +---- +include::{examples-dir}/kotlin/guides/MultiExt.kt[tags=multiBuilderBackPressure,indent=0] +---- + +A coroutine variant allows suspend calls within the builder: + +[source,kotlin] +---- +include::{examples-dir}/kotlin/guides/MultiExt.kt[tags=multiBuilderSuspend,indent=0] +---- + +=== Tuple destructuring + +Mutiny's `Tuple2` through `Tuple9` support Kotlin destructuring declarations: + +[source,kotlin] +---- +include::{examples-dir}/kotlin/guides/TupleExt.kt[tags=tupleDestructuring,indent=0] +---- + +=== Typed failure handling + +Use reified generics for concise failure type matching on both `Uni` and `Multi`: + +[source,kotlin] +---- +include::{examples-dir}/kotlin/guides/FailureExt.kt[tags=uniReifiedOnFailure,indent=0] +---- + +[source,kotlin] +---- +include::{examples-dir}/kotlin/guides/FailureExt.kt[tags=multiReifiedOnFailure,indent=0] +---- diff --git a/content/docs/smallrye-mutiny/latest/guides/logging.adoc b/content/docs/smallrye-mutiny/latest/guides/logging.adoc new file mode 100644 index 000000000..4135995b1 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/guides/logging.adoc @@ -0,0 +1,66 @@ += Logging events +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,beginner + +Both `Uni` and `Multi` offer a `log` operator that can be used to trace events as they flow through operators. + +Mutiny does not make any assumption on _how_ logging is defined, and does not rely on any specific logging API. + +== Using a logging operator + +The `log` method comes in 2 forms: one that takes an identifier and one that derives the identifier from the upstream class: + +[source,java] +---- +include::{examples-dir}/java/guides/infrastructure/OperatorLoggingTest.java[tags=log,indent=0] +---- + +Here the `log` operator traces all events between the `onItem().transform(...)` operator and the subscriber, as in the following output: + +.... +11:01:48.709 [main] INFO Multi.MultiMapOp.0 - onSubscription() +11:01:48.711 [main] INFO Multi.MultiMapOp.0 - request(9223372036854775807) +11:01:48.711 [main] INFO Multi.MultiMapOp.0 - onItem(10) +>>> 10 +11:01:48.711 [main] INFO Multi.MultiMapOp.0 - onItem(20) +>>> 20 +11:01:48.711 [main] INFO Multi.MultiMapOp.0 - onItem(30) +>>> 30 +11:01:48.711 [main] INFO Multi.MultiMapOp.0 - onCompletion() +.... + +There are a few things to note here: + +[arabic] +. we are logging on a `Multi`, so the logging event is prefixed with `Multi` (and `Uni` in the case of a... `Uni`), and +. since we did not specify any identifier in the `log` method call, `MultiMapOp` has been derived from the preceding operator (non-qualified) class name, and +. since there can be multiple subscriptions an integer is appended to the identifier (`0`, `1`, `2`, ...). + +== Defining logging + +What happens when events are being logged is defined with the `Infrastructure` class. +Events are written by default to the standard console output in a format similar to: + +.... +[--> Multi.MultiMapOp.0 | onSubscription() +[--> Multi.MultiMapOp.0 | request(9223372036854775807) +[--> Multi.MultiMapOp.0 | onItem(10) +[--> Multi.MultiMapOp.0 | onItem(20) +[--> Multi.MultiMapOp.0 | onItem(30) +[--> Multi.MultiMapOp.0 | onCompletion() +.... + +The following is an example of configuring logging with http://www.slf4j.org++[++SLF4J++]++: + +[source,java] +---- +include::{examples-dir}/java/guides/infrastructure/OperatorLoggingTest.java[tags=set-logger,indent=0] +---- + +[TIP] +==== + +Note that this is only useful to do when embedding Mutiny in your own stack, some frameworks like https://quarkus.io[Quarkus] will already have defined the correct logging strategy. + +==== diff --git a/content/docs/smallrye-mutiny/latest/guides/merging-and-concatenating-streams.adoc b/content/docs/smallrye-mutiny/latest/guides/merging-and-concatenating-streams.adoc new file mode 100644 index 000000000..43d1277b3 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/guides/merging-and-concatenating-streams.adoc @@ -0,0 +1,87 @@ += Merging and Concatenating Streams +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,intermediate + +Merging or concatenating streams is a frequent operation which consists in taking multiple streams and creating a new `Multi` out of them. +Such an operation observes the items emitted by the different streams and produces a new `Multi` emitting the events. + +All the streams merged or concatenated this way should emit the same type of items. + +== The difference between merge and concatenate + +Understanding the difference between _merge_ and _concatenate_ is essential. + +When _merging_ streams, it observes the different upstreams and emits the items as they come. +If the streams emit their items concurrently, the items from the different streams are interleaved. + +image::images/diagrams/mermaid-74e1ac453bf9.svg[mermaid diagram] + +When using _merge_, failures are also propagated to the merged stream, and no more items are emitted after that failure. +The _completion_ event is only emitted by the merged stream when all the observed streams are completed. + +But if we want to keep the order of the observed stream, we need to _concatenate_. + +When _concatenating_, it waits for the first stream to complete before subscribing to the second one. Thus, it ensures that all the items from the first stream have been emitted before emitting the second stream items. It preserves an order corresponding to the source: + +image::images/diagrams/mermaid-c6f9029821c4.svg[mermaid diagram] + +When the first stream emits the completion event, it switches to the second stream, and so on. +When the last stream completes, the concatenated stream sends the completion event. +As for _merge_, if a stream fails then there won't be further events. + +== Merging Multis + +To create a new `Multi` from the _merge_ of multiple `Multi` streams use: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/MergeConcatTest.java[tags=merge,indent=0] +---- + +For example, we can merge multiple streams emitting periodical events: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/MergeConcatTest.java[tags=merge-ticks,indent=0] +---- + +and the output would be similar to: + +[source,text] +---- +Got item: Stream 1 - 0 +Got item: Stream 2 - 0 +Got item: Stream 3 - 0 +Got item: Stream 3 - 1 +Got item: Stream 1 - 1 +Got item: Stream 3 - 2 +Got item: Stream 2 - 1 +Got item: Stream 3 - 3 +Got item: Stream 1 - 2 +Got item: Stream 3 - 4 +Got item: Stream 3 - 5 +---- + +== Concatenating Multis + +To create a new `Multi` from the _concatenation_ of multiple `Multi` streams use: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/MergeConcatTest.java[tags=concat,indent=0] +---- + +Don't forget that the streams order matters in this case, as `(streamA, streamB)` does not provide the same result as `(streamB, streamA)`: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/MergeConcatTest.java[tags=concatenate-strings,indent=0] +---- + +[IMPORTANT] +==== + +If one of the concatenated streams is unbounded (infinite), the next streams in the list won't be consumed! + +==== diff --git a/content/docs/smallrye-mutiny/latest/guides/multi-split.adoc b/content/docs/smallrye-mutiny/latest/guides/multi-split.adoc new file mode 100644 index 000000000..d8427562a --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/guides/multi-split.adoc @@ -0,0 +1,51 @@ += Splitting a Multi into several Multi +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,intermediate + +It is possible to split a `Multi` into several `Multi` streams. + +== Using the split operator + +Suppose that we have a stream of strings that represent _signals_, and that we want a `Multi` for each kind of signal: + +* `?foo`, `?bar` are _input_ signals, +* `!foo`, `!bar` are _output_ signals, +* `foo`, `bar` are _other_ signals. + +To do that, we need a function that maps each item of the stream to its target stream. +The splitter API needs a Java enumeration to define keys, as in: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/SplitTest.java[tags=enum,indent=0] +---- + +Now we can use the `split` operator that provides a splitter object, and fetch individual `Multi` for each split stream using the `get` method: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/SplitTest.java[tags=splits,indent=0] +---- + +This prints the following console output: + +.... +output - a +input - b +output - c +output - d +other - 123 +input - e +.... + +== Notes on using splits + +* Items flow when all splits have a subscriber. +* The flow stops when either of the subscribers cancels, or when any subscriber has a no outstanding demand. +* The flow resumes when all splits have a subscriber again, and when all subscribers have outstanding demand. +* Only one subscriber can be active for a given split. Other subscription attempts will receive an error. +* When a subscriber cancels, then a new subscription attempt on its corresponding split can succeed. +* Subscribing to an already completed or errored split results in receiving the terminal signal (`onComplete()` or `onFailure(err)`). +* The upstream `Multi` gets subscribed to when the first split subscription happens, no matter which split it is. +* The first split subscription passes its context, if any, to the upstream `Multi`. It is expected that all split subscribers share the same context object, or the behavior of your code will most likely be incorrect. diff --git a/content/docs/smallrye-mutiny/latest/guides/pagination.adoc b/content/docs/smallrye-mutiny/latest/guides/pagination.adoc new file mode 100644 index 000000000..006afadb3 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/guides/pagination.adoc @@ -0,0 +1,45 @@ += How to use paginated APIs? +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,intermediate + +There are many REST / HTTP APIs using pagination, _i.e._ return only a subset of the results and you need to request the next _page_ to get the next batch. +Each batch contains a list of item(s). + +To use this kind of API and generate a continuous stream of items, you need to use the `Multi.createBy().repeating()` function. +However, we need to pass a cursor / state to advance and avoid requesting again and again the same page. +Fortunately, `repeating` provides methods to pass a shared state. +So by combining these methods and `disjoint` you can generate streams from these pages: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/PaginationTest.java[tags=code,indent=0] +---- + +First, you create a `Multi` containing the items emitted by the `CompletionStage` supplier and pass a state supplier to progress among the pages. + +Then, use `until` to call the paginated API until we have all the items. +At the point we have a stream of list of item such as `++[++"a", "b", "c"++]++, ++[++"d", "e"++]++, ++[]++`. +However, we want the following stream: `"a", "b", "c", "d", "e"`. +The `disjoint` method does exactly this. +It gets the items from the lists and passes them downstream: + +image::images/diagrams/mermaid-df98fc1dd069.svg[mermaid diagram] + +`Multi.createBy().repeating()` lets you choose the number of iterations using: + +* `atMost` - exact number of repetitions (or failure happens before reaching that number) +* `until` - the repetition is stopped if the item emitted by the `Uni` *passes* a test (predicate). +It does not propagate the item that did pass the check, and it stops the repetition. +The check verifies if the current item does not contain valid data. +* `whilst` - the repetition is stopped if the item emitted by the `Uni` *does not pass* a test (predicate). +It does propagate the item downstream even if the check does not pass. +However, it stops the repetition. +The test verifies if there is a _next_ batch to be retrieved. + +The following code illustrates the usage of `whilst`: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/PaginationTest.java[tags=code2,indent=0] +---- diff --git a/content/docs/smallrye-mutiny/latest/guides/polling.adoc b/content/docs/smallrye-mutiny/latest/guides/polling.adoc new file mode 100644 index 000000000..88aa7115e --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/guides/polling.adoc @@ -0,0 +1,21 @@ += How to use polling? +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,advanced + +There are many poll-based API around us. +Sometimes you need to use these APIs to generate a stream from the polled values. + +To do this, use the `repeat()` feature: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/PollableSourceTest.java[tags=code,indent=0] +---- + +You can also stop the repetition using the `repeat().until()` method which will continue the repetition until the given predicate returns `true`, and/or directly create a `Multi` using `Multi.createBy().repeating()`: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/PollableSourceTest.java[tags=code2,indent=0] +---- diff --git a/content/docs/smallrye-mutiny/latest/guides/reactive-to-imperative.adoc b/content/docs/smallrye-mutiny/latest/guides/reactive-to-imperative.adoc new file mode 100644 index 000000000..b1ede52c1 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/guides/reactive-to-imperative.adoc @@ -0,0 +1,54 @@ += From reactive to imperative +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,advanced + +There are use cases where you need the items in an imperative manner instead of asynchronous. +Typically, when you serve an HTTP request from a worker thread, you can block. + +Mutiny provides the ability to block until you get the items. + +== Awaiting on Uni's item + +When dealing with a `Uni,` you can block and await the item using: + +[source,java] +---- +include::{examples-dir}/java/guides/integration/ReactiveToImperativeTest.java[tags=await,indent=0] +---- + +This method blocks the caller thread until the observed `uni` emits the item. +Note that the returned item can be `null` if the `uni` emits `null.` +If the `uni` fails, it throws the exception, wrapped in the `CompletionException` for _checked_ exception. + +Blocking forever may not be a great idea. +You can use `uni.await().atMost(Duration)` to pass a deadline. +When the deadline is reached, a `TimeoutException` is thrown: + +[source,java] +---- +include::{examples-dir}/java/guides/integration/ReactiveToImperativeTest.java[tags=atMost,indent=0] +---- + +== Iterating over Multi's items + +When dealing with a `Multi,` you may want to iterate over the items using a simple "foreach." +You can achieve this using `multi.subscribe().asIterable()`: + +[source,java] +---- +include::{examples-dir}/java/guides/integration/ReactiveToImperativeTest.java[tags=iterable,indent=0] +---- + +The returned `iterable` is blocking. +It waits for the next items, and during that time, blocks the caller thread. + +The iteration ends once the last item is consumed. +If the `multi` emits a failure, an exception is thrown. + +Similar to `asIterable()`, the `asStream` method lets you retrieve a `java.util.stream.Stream`: + +[source,java] +---- +include::{examples-dir}/java/guides/integration/ReactiveToImperativeTest.java[tags=stream,indent=0] +---- diff --git a/content/docs/smallrye-mutiny/latest/guides/replaying-multis.adoc b/content/docs/smallrye-mutiny/latest/guides/replaying-multis.adoc new file mode 100644 index 000000000..d2cc64c0e --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/guides/replaying-multis.adoc @@ -0,0 +1,94 @@ += Replaying Multis +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,advanced + +A `Multi` is a _cold-source_: no processing happens until you subscribe. + +While the `broadcast` operator can be used so that multiple subscribers consume a `Multi` events _at the same time_, it does not support replaying items for _late subscribers_: when a subscriber joins after the `Multi` has completed (or failed), then it won't receive any item. + +This is where _replaying_ can be useful. + +== Replaying all events + +Replaying all events from an upstream `Multi` works as follows: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/ReplayTest.java[tags=replay-all,indent=0] +---- + +Both `item++_++1` and `item++_++2` trigger new subscriptions, and both lists contain the following elements: + +.... +[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] +.... + +Replaying works by turning `upstream` into a _hot-stream_, meaning that it gets requested `Long.MAX++_++VALUE` elements. +This is done when the first subscription happens. + +The replay operator stores the items in an internal _replay log_, and then each subscriber gets to replay them. + +[IMPORTANT] +==== + +Subscribers demand and cancellation requests are honored while replaying, but `upstream` cannot be cancelled. + +Be careful with unbounded streams as you can exhaust memory! + +In such cases or when you need to replay large amounts of data, you might opt to use some eventing middleware rather than Mutiny replays. + +==== + +== Replaying the last 'n' events + +You can limit the number of elements to replay by using the `upTo` method: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/ReplayTest.java[tags=replay-last,indent=0] +---- + +Each new subscriber gets to replay the last `n` elements from where the replay log is at subscription time. +For instance the first subscriber can observe all events, while a subscriber that joins 2 seconds later might not observe the earlier events. + +Since `Multi.createFrom().range(0, 10)` is an _immediate_ stream, both `item++_++1` and `item++_++2` lists contain the last items: + +.... +[7, 8, 9] +.... + +== Prepending with seed data + +In some cases you might want to prepend some _seed_ data that will be available for replay before the upstream starts emitting. + +You can do so using an `Iterable` to provide such seed data: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/ReplayTest.java[tags=replay-seed,indent=0] +---- + +In which case subscribers can observe the following events: + +.... +[-10, -5, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9] +.... + +== Replay of failures and completions + +Subscribers get to observe not just items but also the failure and completion events: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/ReplayTest.java[tags=replay-errors,indent=0] +---- + +Running this code yields the following output for any subscriber: + +.... +-> 7 +-> 8 +-> 9 +Failed: boom +.... diff --git a/content/docs/smallrye-mutiny/latest/guides/rx.adoc b/content/docs/smallrye-mutiny/latest/guides/rx.adoc new file mode 100644 index 000000000..b41ec4f0f --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/guides/rx.adoc @@ -0,0 +1,31 @@ += Using map, flatMap and concatMap +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,advanced + +If you are a seasoned reactive developer, you may miss the `map`, `flatMap`, `concatMap` methods. + +The Mutiny API is quite different from the _standard_ reactive eXtensions API. + +There are multiple reasons for this choice. +Typically, _flatMap_ is not necessarily well understood by every developer, leading to potentially catastrophic consequences. + +That being said, Mutiny provides the _map_, _flatMap_ and _concatMap_ methods, implementing the most common variant for each: + +[source,java] +---- +include::{examples-dir}/java/guides/RxTest.java[tags=rx,indent=0] +---- + +The Mutiny equivalents are: + +* `map -++>++ onItem().transform()` +* `flatMap -++>++ onItem().transformToUniAndMerge` and `onItem().transformToMultiAndMerge` +* `concatMap -++>++ onItem().transformToUniAndConcatenate` and `onItem().transformToMultiAndConcatenate` + +The following snippet demonstrates how to uses these methods: + +[source,java] +---- +include::{examples-dir}/java/guides/RxTest.java[tags=mutiny,indent=0] +---- diff --git a/content/docs/smallrye-mutiny/latest/guides/shortcut-methods.adoc b/content/docs/smallrye-mutiny/latest/guides/shortcut-methods.adoc new file mode 100644 index 000000000..219dcdcb1 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/guides/shortcut-methods.adoc @@ -0,0 +1,44 @@ += Shortcut methods +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,beginner + +The Mutiny API is decomposed around the idea of groups, each group handling a specific event. +However, to avoid verbosity, Mutiny also exposes _shortcuts_ for the most used methods. +Be aware that these shorts, while making the code shorter, may harm the readability and understandability. + +To _peek_ at items, you can use the `invoke` method: + +[source,java] +---- +include::{examples-dir}/java/guides/ShortcutsTest.java[tags=invoke,indent=0] +---- + +`invoke` is a shortcut for `onItem().invoke(...)`. + +Mutiny also provides the `call` method for executing an action returning a `Uni`. +This is useful to execute an asynchronous action without modifying incoming item: + +[source,java] +---- +include::{examples-dir}/java/guides/ShortcutsTest.java[tags=call,indent=0] +---- + +`call` is a shortcut for `onItem().call(...)`. + +The following table lists the available shortcuts available by the `Uni` class: + +[width="100%",cols="41%,59%",options="header",] +|=== +|Shortcut |Equivalent +|`uni.map(x -++>++ y)` |`uni.onItem().transform(x -++>++ y)` +|`uni.flatMap(x -++>++ uni2)` |`uni.onItem().transformToUni(x -++>++ uni2)` +|`uni.chain(x -++>++ uni2)` |`uni.onItem().transformToUni(x -++>++ uni2)` +|`uni.invoke(x -++>++ System.out.println(x))` |`uni.onItem().invoke(x -++>++ System.out.println(x))` +|`uni.call(x -++>++ uni2)` |`uni.onItem().call(x -++>++ uni2)` +|`uni.eventually(() -++>++ System.out.println("eventually"))` |`uni.onTermination().invoke(() -++>++ System.out.println("eventually"))` +|`uni.eventually(() -++>++ uni2)` |`uni.onTermination().call((ignoredItem, ignoredError, ignoredCancellation) -++>++ uni2)` +|`uni.replaceWith(x)` |`uni.onItem().transform(ignored -++>++ x)` +|`uni.replaceWith(uni2)` |`uni.onItem().transformToUni(ignored -++>++ uni2)` +|`uni.replaceIfNullWith(x)` |`uni.onItem().ifNull().continueWith(x)` +|=== diff --git a/content/docs/smallrye-mutiny/latest/guides/spies.adoc b/content/docs/smallrye-mutiny/latest/guides/spies.adoc new file mode 100644 index 000000000..2a87e08e8 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/guides/spies.adoc @@ -0,0 +1,71 @@ += Spying on events +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,advanced + +Spies are useful when you need to track which _events_ flow into a `Uni` or a `Multi`. +Spies can track events from groups such as `onItem()`, `onFailure()`, `onSubscribe()`, etc. + +The `io.smallrye.mutiny.helpers.spies.Spy` interface offers factory methods to spy on selected groups, or even on all groups. + +== Spying selected groups + +The following example spies on requests and completion group events: + +[source,java] +---- +include::{examples-dir}/java/guides/SpiesTest.java[tags=selected,indent=0] +---- + +The standard output stream shall display the following text: + +.... +1 +2 +3 +Number of requests: 9223372036854775807 +Completed? true +.... + +The number of requests corresponds to `Long.MAX++_++VALUE`, and a completion event was sent. + +[IMPORTANT] +==== + +It is important to note that spies observe and report events for all subscribers, not just one in particular. + +You should call the `.reset()` method on a given spy to resets its statistics such as the invocation count. + +==== + +== Spying all groups + +You can take advantage of a _global spy_ if you are interested in all event groups: + +[source,java] +---- +include::{examples-dir}/java/guides/SpiesTest.java[tags=global,indent=0] +---- + +Running the snippet above gives the following output: + +.... +1 +2 +3 +Number of requests: 9223372036854775807 +Cancelled? false +Failure? null +Items: [1, 2, 3] +.... + +[WARNING] +==== + +Tracking `onItem()` events on a `Multi` requires storing all items into a list, which can yield an out-of-memory +exception with large streams. + +In such cases consider using `Spy.onItem(multi, false)` to obtain a spy that does not store items, but that can +still report data such as the number of received events (see `spy.invocationCount()`). + +==== diff --git a/content/docs/smallrye-mutiny/latest/guides/take-skip-items.adoc b/content/docs/smallrye-mutiny/latest/guides/take-skip-items.adoc new file mode 100644 index 000000000..f466d1062 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/guides/take-skip-items.adoc @@ -0,0 +1,116 @@ += Take/Skip the first or last items +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,beginner + +Multi provides the ability to: + +* only forward items from the beginning of the observed multi, +* only forward the last items (and discard all the other ones), +* skip items from the beginning of the multi, +* skip the last items. + +These actions are available from the `multi.select()` and `multi.skip()` groups, allowing to, respectively, select and skip +items from upstream. + +== Selecting items + +The `multi.select().first` method forwards on the _n_ *first* items from the multi. +It forwards that amount of items and then sends the completion signal. +It also cancels the upstream subscription. + +[source,java] +---- +include::{examples-dir}/java/guides/operators/SelectAndSkipTest.java[tags=take-first,indent=0] +---- + +[NOTE] +==== + +The `select().first()` method selects only the first item. + +==== + +If the observed multi emits fewer items, it sends the completion event when the upstream completes. + +Similarly, The `multi.select().last` operator forwards on the _n_ *last* items from the multi. +It discards all the items emitted beforehand. + +[source,java] +---- +include::{examples-dir}/java/guides/operators/SelectAndSkipTest.java[tags=take-last,indent=0] +---- + +[NOTE] +==== + +The `select().last()` method selects only the last item. + +==== + +The `multi.select().first(Predicate)` operator forwards the items while the passed predicate returns `true`: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/SelectAndSkipTest.java[tags=take-while,indent=0] +---- + +It calls the predicate for each item. +Once the predicate returns `false`, it stops forwarding the items downstream. +It also sends the completion event and cancels the upstream subscription. + +Finally, `multi.select().first(Duration)` operator picks the first items emitted during a given period. +Once the passed duration expires, it sends the completion event and cancels the upstream subscription. +If the observed multi completes before the passed duration, it sends the completion event. + +[source,java] +---- +include::{examples-dir}/java/guides/operators/SelectAndSkipTest.java[tags=take-for,indent=0] +---- + +== Skipping items + +You can also skip items using `multi.skip()`. + +The `multi.skip().first(n)` method skips the _n_ *first* items from the multi. +It forwards all the remaining items and sends the completion event when the upstream multi completes. + +[source,java] +---- +include::{examples-dir}/java/guides/operators/SelectAndSkipTest.java[tags=skip-first,indent=0] +---- + +If the observed multi emits fewer items, it sends the completion event without emitting any items. + +[NOTE] +==== + +`skip().last()` drops only the very last item. + +==== + +Similarly, The `multi.skip().last(n)` operator skips on the _n_ *last* items from the multi: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/SelectAndSkipTest.java[tags=skip-last,indent=0] +---- + +The `multi.skip().first(Predicate)` operator skips the items while the passed predicate returns `true`: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/SelectAndSkipTest.java[tags=skip-while,indent=0] +---- + +It calls the predicate for each item. +Once the predicate returns `false`, it stops discarding the items and starts forwarding downstream. + +Finally, `multi.skip().first(Duration)` operator skips the first items for a given period. +Once the passed duration expires, it sends the items emitted after the deadline downstream. +If the observed multi completes before the passed duration, it sends the completion event. + +[source,java] +---- +include::{examples-dir}/java/guides/operators/SelectAndSkipTest.java[tags=skip-for,indent=0] +---- diff --git a/content/docs/smallrye-mutiny/latest/guides/testing.adoc b/content/docs/smallrye-mutiny/latest/guides/testing.adoc new file mode 100644 index 000000000..6722c54dd --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/guides/testing.adoc @@ -0,0 +1,72 @@ += How can I write unit / integration tests? +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,beginner + +Mutiny provides subscribers for `Uni` and `Multi` offering helpful assertion methods. +You can use them to test pipelines. + +== Testing a Uni + +Here is an example to test a `Uni`: + +[source,java] +---- +include::{examples-dir}/java/guides/TestSubscribersTest.java[tags=uni,indent=0] +---- + +You can also use predicate-based assertions and item inspection: + +[source,java] +---- +include::{examples-dir}/java/guides/TestSubscribersTest.java[tags=uni-predicate,indent=0] +---- + +== Testing a Multi + +Testing a `Multi` pipeline is similar: + +[source,java] +---- +include::{examples-dir}/java/guides/TestSubscribersTest.java[tags=multi,indent=0] +---- + +Predicate-based assertions work on `Multi` too: + +[source,java] +---- +include::{examples-dir}/java/guides/TestSubscribersTest.java[tags=multi-predicate,indent=0] +---- + +== Declarative verification with AssertMulti + +For multi-item streams, `AssertMulti` provides a declarative step-by-step verifier. +Build a sequence of expectations, then call `verify()`: + +[source,java] +---- +include::{examples-dir}/java/guides/TestSubscribersTest.java[tags=assert-multi,indent=0] +---- + +You can control demand explicitly for backpressure testing: + +[source,java] +---- +include::{examples-dir}/java/guides/TestSubscribersTest.java[tags=assert-multi-demand,indent=0] +---- + +== Testing failures + +The assertions do not just focus on _good_ outcomes, you can also test failures as in: + +[source,java] +---- +include::{examples-dir}/java/guides/TestSubscribersTest.java[tags=failing,indent=0] +---- + +With `AssertMulti`: + +[source,java] +---- +include::{examples-dir}/java/guides/TestSubscribersTest.java[tags=assert-multi-failure,indent=0] +---- diff --git a/content/docs/smallrye-mutiny/latest/guides/unchecked-exceptions.adoc b/content/docs/smallrye-mutiny/latest/guides/unchecked-exceptions.adoc new file mode 100644 index 000000000..8b0f4206d --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/guides/unchecked-exceptions.adoc @@ -0,0 +1,42 @@ += Dealing with checked exceptions +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,intermediate + +When implementing your reactive pipeline, you write lots of functions (`java.util.function.Function`), consumers (`java.util.function.Consumer`), suppliers (`java.util.function.Supplier`) and so on. + +By default, you cannot throw checked exceptions. + +When integrating libraries throwing checked exceptions (like `IOException`) it's not very convenient to add a `try/catch` block and wrap the thrown exception into a runtime exception: + +[source,java] +---- +include::{examples-dir}/java/guides/UncheckedTest.java[tags=rethrow,indent=0] +---- + +Mutiny provides utilities to avoid having to do this manually. + +If your operation throws a _checked exception_, you can use the https://javadoc.io/doc/io.smallrye.reactive/mutiny/latest/io/smallrye/mutiny/unchecked/Unchecked.html[`io.smallrye.mutiny.unchecked.Unchecked`] wrappers. + +For example, if your synchronous transformation uses a method throwing a checked exception, wrap it using `Unchecked.function`: + +[source,java] +---- +include::{examples-dir}/java/guides/UncheckedTest.java[tags=transform,indent=0] +---- + +You can also wrap consumers such as in: + +[source,java] +---- +include::{examples-dir}/java/guides/UncheckedTest.java[tags=invoke,indent=0] +---- + +[TIP] +==== + +You can add the following import statement to simplify the usage of the provided methods: + +`import static io.smallrye.mutiny.unchecked.Unchecked.++*++;` + +==== diff --git a/content/docs/smallrye-mutiny/latest/images/diagrams/mermaid-0277dcc2b534.svg b/content/docs/smallrye-mutiny/latest/images/diagrams/mermaid-0277dcc2b534.svg new file mode 100644 index 000000000..d390d8d76 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/images/diagrams/mermaid-0277dcc2b534.svg @@ -0,0 +1 @@ +SubscriberProcessorPublisherSubscriberProcessorPublishersubscribesubscribesubscriptionsubscriptionrequest(5)request(5)onItem("a")onItem("A")onItem("b")onItem("B")cancel()cancel() \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/latest/images/diagrams/mermaid-18f9a50c0627.svg b/content/docs/smallrye-mutiny/latest/images/diagrams/mermaid-18f9a50c0627.svg new file mode 100644 index 000000000..684d7dc80 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/images/diagrams/mermaid-18f9a50c0627.svg @@ -0,0 +1 @@ +SubscriberemitOn(executor)MultiSubscriberemitOn(executor)MultiOn caller threadOn executor threadonItem(1)1onItem(1)2 \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/latest/images/diagrams/mermaid-3b8d4c58438a.svg b/content/docs/smallrye-mutiny/latest/images/diagrams/mermaid-3b8d4c58438a.svg new file mode 100644 index 000000000..bfccc4178 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/images/diagrams/mermaid-3b8d4c58438a.svg @@ -0,0 +1 @@ +SubscriberMulti(item)TransformerUniSubscriberMulti(item)TransformerUnionItem(item)1subscribe(...)2onSubscribe(sub)3onItem(item)4onItem(item)5onItem(item)6onItem(item)7 \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/latest/images/diagrams/mermaid-3d4459e3c6a6.svg b/content/docs/smallrye-mutiny/latest/images/diagrams/mermaid-3d4459e3c6a6.svg new file mode 100644 index 000000000..2e8d7dc42 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/images/diagrams/mermaid-3d4459e3c6a6.svg @@ -0,0 +1 @@ +SubscriberCombined streamStream BStream ASubscriberCombined streamStream BStream AonItem(1)1onItem(2)2onItem(a)3onItem([1,a])4onItem(3)5onItem(b)6onItem([2,b])7onItem(c)8 \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/latest/images/diagrams/mermaid-491ff27a51a5.svg b/content/docs/smallrye-mutiny/latest/images/diagrams/mermaid-491ff27a51a5.svg new file mode 100644 index 000000000..767fb50f9 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/images/diagrams/mermaid-491ff27a51a5.svg @@ -0,0 +1 @@ +ObserverStreamObserverStreamObserverStreamObserverStreamonItem("a")onItem("a")onItem("b")onItem("b")onItem("c")onFailure(err)onCompletion() \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/latest/images/diagrams/mermaid-69aa56febc93.svg b/content/docs/smallrye-mutiny/latest/images/diagrams/mermaid-69aa56febc93.svg new file mode 100644 index 000000000..47b820123 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/images/diagrams/mermaid-69aa56febc93.svg @@ -0,0 +1 @@ +SubscriberCollect operatorMultiSubscriberCollect operatorMultionItem(1)1onItem(2)2onItem(3)3onItem([1, 2, 3])4 \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/latest/images/diagrams/mermaid-74e1ac453bf9.svg b/content/docs/smallrye-mutiny/latest/images/diagrams/mermaid-74e1ac453bf9.svg new file mode 100644 index 000000000..d0afbd3b0 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/images/diagrams/mermaid-74e1ac453bf9.svg @@ -0,0 +1 @@ +Merged streamStream BStream AMerged streamStream BStream Asubscribe1subscribe2onSubscribe(s)3onItem(1)4onSubscribe(s)5onItem(2)6onItem(a)7onItem(3)8onItem(b)9onItem(c)10 \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/latest/images/diagrams/mermaid-76df06b35153.svg b/content/docs/smallrye-mutiny/latest/images/diagrams/mermaid-76df06b35153.svg new file mode 100644 index 000000000..137defedd --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/images/diagrams/mermaid-76df06b35153.svg @@ -0,0 +1 @@ +SubscriberrunSubscriptionOn(executor)MultiSubscriberrunSubscriptionOn(executor)Multion Caller threadOn executor threadsubscribe1subscribe2 \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/latest/images/diagrams/mermaid-bc092005fd3f.svg b/content/docs/smallrye-mutiny/latest/images/diagrams/mermaid-bc092005fd3f.svg new file mode 100644 index 000000000..2f934d514 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/images/diagrams/mermaid-bc092005fd3f.svg @@ -0,0 +1 @@ +DownstreamReturned UnisonItem().call(...)MultiDownstreamReturned UnisonItem().call(...)Multiitem11item12result13result14item25item26result27result28item39item310result311result312 \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/latest/images/diagrams/mermaid-c54f60b30f42.svg b/content/docs/smallrye-mutiny/latest/images/diagrams/mermaid-c54f60b30f42.svg new file mode 100644 index 000000000..07645de13 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/images/diagrams/mermaid-c54f60b30f42.svg @@ -0,0 +1 @@ +SubscriberCombined streamStream BStream ASubscriberCombined streamStream BStream AonItem(1)1onItem(a)2onItem([1,a])3onItem(2)4onItem(b)5onItem([2,b])6 \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/latest/images/diagrams/mermaid-c6f9029821c4.svg b/content/docs/smallrye-mutiny/latest/images/diagrams/mermaid-c6f9029821c4.svg new file mode 100644 index 000000000..64a7d800c --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/images/diagrams/mermaid-c6f9029821c4.svg @@ -0,0 +1 @@ +Concatenated streamStream BStream AConcatenated streamStream BStream Asubscribe1onSubscribe(s)2onItem(1)3onItem(2)4onItem(3)5onCompletion()6subscribe7onSubscribe(s)8onItem(a)9onItem(b)10onItem(c)11 \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/latest/images/diagrams/mermaid-cbd13fb6d19b.svg b/content/docs/smallrye-mutiny/latest/images/diagrams/mermaid-cbd13fb6d19b.svg new file mode 100644 index 000000000..4d0a969d8 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/images/diagrams/mermaid-cbd13fb6d19b.svg @@ -0,0 +1 @@ +

a, b, c

a, b, c

a, b, c

a, b, c

a, b, c

a, b, c

Events source

Events source

Events source

Publisher

Publisher

Publisher

Subscriber 1

Subscriber 2

Subscriber 3

\ No newline at end of file diff --git a/content/docs/smallrye-mutiny/latest/images/diagrams/mermaid-df98fc1dd069.svg b/content/docs/smallrye-mutiny/latest/images/diagrams/mermaid-df98fc1dd069.svg new file mode 100644 index 000000000..876990fe8 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/images/diagrams/mermaid-df98fc1dd069.svg @@ -0,0 +1 @@ +SubscriberDisjoint operatorMultiSubscriberDisjoint operatorMultionItem([a,b,c])1onItem(a)2onItem(b)3onItem(c)4onItem([d,e])5onItem(d)6onItem(e)7 \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/latest/images/diagrams/mermaid-e6dd95e32a3f.svg b/content/docs/smallrye-mutiny/latest/images/diagrams/mermaid-e6dd95e32a3f.svg new file mode 100644 index 000000000..deda34e8f --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/images/diagrams/mermaid-e6dd95e32a3f.svg @@ -0,0 +1 @@ +SubscriberTransformerMultiSubscriberTransformerMultionItem(a1)1onItem(a2)2onItem(b1)3onItem(b2)4onItem(c1)5onItem(c2)6 \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/latest/images/diagrams/mermaid-e88e1918cd26.svg b/content/docs/smallrye-mutiny/latest/images/diagrams/mermaid-e88e1918cd26.svg new file mode 100644 index 000000000..8f33f491e --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/images/diagrams/mermaid-e88e1918cd26.svg @@ -0,0 +1 @@ +DownstreamonItem().invoke(...)MultiDownstreamonItem().invoke(...)Multicallback executionitem11item12item23item24item35item36 \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/latest/images/diagrams/mermaid-f0001b5cde9f.svg b/content/docs/smallrye-mutiny/latest/images/diagrams/mermaid-f0001b5cde9f.svg new file mode 100644 index 000000000..5818402e1 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/images/diagrams/mermaid-f0001b5cde9f.svg @@ -0,0 +1 @@ +SubscriberCombined streamStream BStream ASubscriberCombined streamStream BStream AonItem(1)1onItem(2)2onItem(a)3onItem([2,a])4onItem(3)5onItem([3,a])6onItem(b)7onItem([3,b])8onItem(c)9onItem([3,c])10 \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/latest/images/diagrams/mermaid-f67adb6018b3.svg b/content/docs/smallrye-mutiny/latest/images/diagrams/mermaid-f67adb6018b3.svg new file mode 100644 index 000000000..3fbcf4900 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/images/diagrams/mermaid-f67adb6018b3.svg @@ -0,0 +1 @@ +SubscriberUni(item)TransformerUniSubscriberUni(item)TransformerUnionItem(item)1subscribe(...)2onSubscribe(sub)3onItem(result)4onItem(result)5 \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/latest/images/diagrams/mermaid-f71496c9eae5.svg b/content/docs/smallrye-mutiny/latest/images/diagrams/mermaid-f71496c9eae5.svg new file mode 100644 index 000000000..fc83f5b8c --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/images/diagrams/mermaid-f71496c9eae5.svg @@ -0,0 +1 @@ +SubscriberProcessor 2Processor 1PublisherSubscriberProcessor 2Processor 1PublisheronItemonItemonItemonItemonItemonItemonCompletiononCompletiononCompletion \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/latest/images/diagrams/mermaid-f74f7917dcf7.svg b/content/docs/smallrye-mutiny/latest/images/diagrams/mermaid-f74f7917dcf7.svg new file mode 100644 index 000000000..b92c0941f --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/images/diagrams/mermaid-f74f7917dcf7.svg @@ -0,0 +1 @@ +

a, b, c

a, b, c

a, b, c

a, b, c

Events source

Publisher

Subscriber 1

Subscriber 2

Subscriber 3

\ No newline at end of file diff --git a/content/docs/smallrye-mutiny/latest/index.adoc b/content/docs/smallrye-mutiny/latest/index.adoc new file mode 100644 index 000000000..74b38c67d --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/index.adoc @@ -0,0 +1,88 @@ += Mutiny -- Intuitive Event-Driven Reactive Programming Library for Java +include::_attributes.adoc[] + +[source,java] +---- +Uni request = makeSomeNetworkRequest(params); + +request.ifNoItem().after(ofMillis(100)) + .failWith(() -> new TooSlowException("💥")) + .onFailure(IOException.class).recoverWithItem(fail -> "📦") + .subscribe().with( + item -> log("👍 " + item), + err -> log(err.getMessage()) + ); +---- + +xref:tutorials/getting-mutiny.adoc[Get Mutiny {versions-mutiny},role=button] + +[.cards] +-- + +**** + +[discrete] +== Event-Driven + +Mutiny places _events_ at the core of its design. + +With Mutiny, you observe _events_, _react_ to them, and create elegant and readable processing pipelines. + +A PhD in functional programming is not required. + +**** + +**** + +[discrete] +== Navigable + +Even with smart code completion, classes with hundreds of methods are confusing. + +Mutiny provides a navigable and explicit API driving you towards the operator you need. + +**** + +**** + +[discrete] +== Non-blocking I/O + +Mutiny is the perfect companion to tame the asynchronous nature of applications with non-blocking I/O. + +Compose operations in a declarative fashion, transform data, enforce progress, recover from failures and more. + +**** + +**** + +[discrete] +== Quarkus and Vert.x native + +Mutiny is integrated in https://quarkus.io[Quarkus] where every reactive API uses Mutiny, and https://vertx.io[Eclipse Vert.x] clients are made available using Mutiny bindings. + +Mutiny is however an independent library that can ultimately be used in any Java application. + +**** + +**** + +[discrete] +== Made for an asynchronous world + +Mutiny can be used in any asynchronous application such as event-driven microservices, message-based applications, network utilities, data stream processing, and of course... reactive applications! + +**** + +**** + +[discrete] +== Reactive Converters Built-In + +Mutiny is based on the https://www.reactive-streams.org/[Reactive Streams protocol] and https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/concurrent/Flow.html[Java Flow], and so it can be integrated with any other reactive programming library. + +In addition, it proposes _converters_ to interact with other popular libraries. + +**** + +-- diff --git a/content/docs/smallrye-mutiny/latest/reference/distributed_systems_are_asynchronous.png b/content/docs/smallrye-mutiny/latest/reference/distributed_systems_are_asynchronous.png new file mode 100644 index 000000000..0d00a8c9e Binary files /dev/null and b/content/docs/smallrye-mutiny/latest/reference/distributed_systems_are_asynchronous.png differ diff --git a/content/docs/smallrye-mutiny/latest/reference/going-reactive-a-few-pitfalls.adoc b/content/docs/smallrye-mutiny/latest/reference/going-reactive-a-few-pitfalls.adoc new file mode 100644 index 000000000..ac6f0e7b2 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/reference/going-reactive-a-few-pitfalls.adoc @@ -0,0 +1,74 @@ += Going reactive: a few pitfalls +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: reference,beginner + +Don't get us wrong, reactive programming is a fantastic way to write resource-efficient code! + +That being said, reactive programming has a learning curve that should not be taken lightly, and in some cases it is safer to write imperative code that you fully comprehend over reactive code that you don't fully grok. + +We have assembled a few considerations that we think new users should know before they embark into writing complex reactive business logic. + +== Mutiny doesn't auto-magically make your code asynchronous + +This is a common source of confusion for new reactive programmers. +Mutiny itself *does not perform any scheduling work*, except for the xref:guides/emit-on-vs-run-subscription-on.adoc[`emitOn` and `runSubscriptionOn` operators]. + +Consider the following code where we _join_ results from multiple asynchronous operations, materialised by the `Uni++<++String++>++`-returning `fetch` method: + +[source,java] +---- +include::{examples-dir}/java/guides/reference/PitfallsTest.java[tags=noMagicJoin,indent=0] +---- + +You might think that the `join` operator schedules the calls to `fetch` to be run concurrently, and then collects the results into a list. +This is not how it works! + +The `join` operator does subscribe to each `Uni++<++String++>++` returned by each call to `fetch`. +When it receives a value, it puts it into a list, and when all values have been received, that list is emitted. +The threads involved here are the ones that emit values in `fetch`. +If `fetch` uses async I/O underneath then you should observe true concurrency, but if `fetch` just emits a value right when the subscription happens then you will merely observe a sequential execution of each call to `fetch`, in order. + +== When to prefer `Uni++<++List++<++T++>>++` over `Multi++<++T++>++` + +The reason why `Multi` exists is to model streams over back-pressured sources. +By conforming to the https://www.reactive-streams.org/[Reactive Streams protocol], a `Multi` respects the control flow requests from its subscribers, avoiding classic problems such as a fast producer and a slow consumer that can yield to memory exhaustion problems. + +That being said, not everything is a stream. +Take the example of relational databases: *databases don't stream!* (for the most parts) + +When you do a query such as `SELECT ++*++ FROM ABC WHERE INDEX ++<++ 123`, you get result rows. +While you might wrap the results in a `Multi++<++Row++>++` as a convenience, the network protocol of the database still sends you all `Row` values and is very unlikely to support any notion of back-pressure on a SQL query result. + +This is why `Uni++<++List++<++Row++>>++` is in this case a better representation of an asynchronous operation than `Multi++<++Row++>++`, because the underlying networked service protocol does not provide you with any back-pressured stream. + +== Creating `Uni` and `Multi` from in-memory data might be suspicious + +You will find lots of occurrences of creating `Uni` and `Multi` from in-memory data in this documentation, as in: + +[source,java] +---- +include::{examples-dir}/java/guides/reference/PitfallsTest.java[tags=inMemoryData,indent=0] +---- + +This is convenient and expected when creating tests and examples, but this should be a strong warning in production. +Indeed, if we have a method such as the following: + +[source,java] +---- +include::{examples-dir}/java/guides/reference/PitfallsTest.java[tags=suspiciousPublisher,indent=0] +---- + +then it is clear that there is nothing _"reactive"_ in this code _(sadly, you can find such idioms in some well-known "reactive" client libraries, but we digress)_. + +As a rule of thumb, if your *initial* publisher does not make any I/O operation and it already has the data available in memory, then it is suspicious: + +* if it is a `Uni`, then it does not really model an asynchronous I/O operation because the data is already here, and +* if it is a `Multi` then not only there is no asynchronous I/O operation involved, but there is no need for a back-pressure protocol either (see the previous section). + +What is not suspicious however is to create, say, a `Multi` to perform a transformation operation: + +[source,java] +---- +include::{examples-dir}/java/guides/reference/PitfallsTest.java[tags=flatmap-ism,indent=0] +---- diff --git a/content/docs/smallrye-mutiny/latest/reference/migrating-to-mutiny-2.adoc b/content/docs/smallrye-mutiny/latest/reference/migrating-to-mutiny-2.adoc new file mode 100644 index 000000000..c036345db --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/reference/migrating-to-mutiny-2.adoc @@ -0,0 +1,52 @@ += Migrating to Mutiny 2 +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: reference,beginner + +The upgrade is transparent for most code bases that _use_ Mutiny in applications (e.g., Quarkus applications). + +== Highlights + +* Mutiny 2 is a major release with source and binary incompatible changes to the Mutiny `0.x` and `1.x` series. +* The main highlight of Mutiny 2 is that it is now based on top of the `java.util.concurrent.Flow` APIs instead of the legacy _Reactive Streams APIs_. +* The `Flow` APIs have been part of the JDK since Java 9, and they are the modern _Reactive Streams APIs_. +* Mutiny remains a faithful implementation of the _Reactive Streams_ specification and passes the `Flow` variant of the _Reactive Streams TCK_. +* Deprecated APIs in Mutiny `1.x` have been removed, and experimental APIs have been promoted. + +== Impact of the switch from legacy Reactive Streams APIs to JDK Flow + +* The `Flow` types are isomorphic to the legacy _Reactive Streams API_ types. +* We recommend that you migrate to `Flow` in your own code bases. +* You should encourage third-party libraries to migrate to `Flow`. +* You can always use _adapters_ to go back and forth between `Flow` and legacy _Reactive Streams_ types. + +=== General guidelines + +* If your code _only uses_ `Uni` and `Multi` (i.e., not `org.reactivestreams.Publisher`), then you will be source-compatible with Mutiny 2. You should still recompile and check that your test suites pass. +* If you expose `Multi` as a `org.reactivestreams.Publisher` then you will either need an _adapter_ (see below) or migrate to `java.util.concurrent.Flow.Publisher`. +* If you interact with `org.reactivestreams.Publisher` publishers and you can't migrate them to `java.util.concurrent.Flow.Publisher` (e.g., because it is a third-party library), then you will need an _adapter_. Please encourage third-party libraries to migrate to `Flow`. + +=== Adapters between Flow and legacy Reactive Streams APIs + +* We recommend using the adapters from the https://smallrye.io/smallrye-mutiny-zero[Mutiny Zero project]. +** The Maven coordinates are `groupId: io.smallrye.reactive`, `artifactId: mutiny-zero-flow-adapters` +** Use `AdaptersToFlow` to convert from _Reactive Streams_ types to `Flow` types, and +** Use `AdaptersToReactiveStreams` to convert `Flow` types to _Reactive Streams_ types. +* The Mutiny Zero adapters have virtually zero overhead. + +== Other API changes + +=== Deprecated API removals + +* `Uni` and `Multi` `onSubscribe()` group is now `onSubscription()`. +* `AssertSubscriber.await()` has been replaced by event-specific methods (items, failure, completion, etc). +* The _RxJava 2_ integration module has been discarded (only RxJava 3 is now supported). + +=== Experimental API promotions + +* `Uni` and `Multi` subscription-bound contexts. +* `Uni.join()` publisher. +* `.ifNoItem()` timeout operators. +* `Uni` and `Multi` spies. +* `capDemandsUsing()` and `paceDemand()` request management operators. +* `Multi` `replay()` operator. diff --git a/content/docs/smallrye-mutiny/latest/reference/publications.adoc b/content/docs/smallrye-mutiny/latest/reference/publications.adoc new file mode 100644 index 000000000..05db24027 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/reference/publications.adoc @@ -0,0 +1,18 @@ += Publications +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: reference,advanced + +Julien Ponge, Arthur Navarro, Clément Escoffier, and Frédéric Le Mouël. 2021. +*https://doi.org/10.1145/3486605.3486788[Analysing the Performance and Costs of Reactive Programming Libraries in Java].* +_In Proceedings of the 8th ACM SIGPLAN International Workshop on Reactive and Event-Based Languages and Systems (REBLS ’21)_, October 18, 2021, Chicago, IL, USA. ACM, New York, NY, USA, 10 pages. +https://hal.inria.fr/hal-03409277/document[(PDF)] + +____ +Modern services running in cloud and edge environments need to be resource-efficient to increase deployment density and reduce operating costs. +Asynchronous I/O combined with asynchronous programming provides a solid technical foundation to reach these goals. +Reactive programming and reactive streams are gaining traction in the Java ecosystem. +However, reactive streams implementations tend to be complex to work with and maintain. +This paper discusses the performance of the three major reactive streams compliant libraries used in Java applications: RxJava, Project Reactor, and SmallRye Mutiny. +As we will show, advanced optimization techniques such as operator fusion do not yield better performance on realistic I/O-bound workloads, and they significantly increase development and maintenance costs. +____ diff --git a/content/docs/smallrye-mutiny/latest/reference/uni-and-multi.adoc b/content/docs/smallrye-mutiny/latest/reference/uni-and-multi.adoc new file mode 100644 index 000000000..bc8181723 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/reference/uni-and-multi.adoc @@ -0,0 +1,44 @@ += Uni and Multi +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: reference,beginner + +Mutiny defines two _reactive_ types: + +* `Multi` - represents streams of ++_++0..++*_++ items (potentially unbounded) +* `Uni` - represents streams receiving either an item or a failure + +[TIP] +==== + +The Mutiny name comes from the contraction of `Multi` and `Uni` names + +==== + +Both `Uni` and `Multi` are asynchronous types. +They receive and fire events at any time. + +You may wonder why we make the distinction between `Uni` and `Multi.` +Conceptually, a `Uni` is a `Multi,` right? + +In practice, you don't use `Unis` and `Multis` the same way. +The use cases and operations are different. + +* `Uni` does not need the complete ceremony presented above as the _request_ does not make sense. +* The `subscribe` event expresses the interest and triggers the computation, no need for an additional _request_. +* `Uni` can handle items having a `null` value (and has specific methods to handle this case). +* `Multi` does not allow it (because the Reactive Streams specification forbids it). +* Having a `Uni` implementing `Publisher` would be a bit like having `Optional` implementing `Iterable`. + +In other words, `Uni`: + +* can receive at most 1 `item` event, or a `failure` event +* cannot receive a `completion` event (`null` in the case of 0 items) +* cannot receive a `request` event + +The following snippet shows how you can use `Uni` and `Multi`: + +[source,java] +---- +include::{examples-dir}/java/guides/UniMultiComparisonTest.java[tags=code,indent=0] +---- diff --git a/content/docs/smallrye-mutiny/latest/reference/what-is-reactive-programming.adoc b/content/docs/smallrye-mutiny/latest/reference/what-is-reactive-programming.adoc new file mode 100644 index 000000000..9065de5f3 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/reference/what-is-reactive-programming.adoc @@ -0,0 +1,31 @@ += What is Reactive Programming? +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: reference,beginner + +Mutiny is a reactive programming library. +If you look on Wikipedia for reactive programming, you will find the following definition: + +____ +Reactive Programming combines functional programming, the observer pattern, and the iterable pattern. +____ + +While correct, we never found this definition very helpful. +It does not convey clearly what's reactive programming is all about. +So, let's make another definition, much more straightforward: + +____ +Reactive programming is about programming with data streams. +____ + +That's it. +Reactive programming is about streams and especially, observing them. +It pushes that idea to its limit: with reactive programming, everything is a data stream. + +With reactive programming, you observe streams and implement side effects when _something_ flows in the stream: + +image::images/diagrams/mermaid-491ff27a51a5.svg[mermaid diagram] + +It's asynchronous by nature as you don't know when the _data_ is going to be seen. +Yet, reactive programming goes beyond this. +It provides a toolbox to compose streams and process events. diff --git a/content/docs/smallrye-mutiny/latest/reference/what-makes-mutiny-different.adoc b/content/docs/smallrye-mutiny/latest/reference/what-makes-mutiny-different.adoc new file mode 100644 index 000000000..aec919089 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/reference/what-makes-mutiny-different.adoc @@ -0,0 +1,108 @@ += What makes Mutiny different? +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: reference,beginner + +There are other reactive programming libraries out there. +In the Java world, we can mention Project Reactor and Rx Java. + +So, what makes Mutiny different from these two well-known libraries? +*The API!* + +Asynchronous is hard to grasp for most developers, and for good reasons. +Thus, the API must not require advanced knowledge or add cognitive overload. +It should help you design your logic and still be intelligible when you return to the code 6 months later. + +To achieve this, Mutiny is built on three pillars: + +* *Event-Driven* - with Mutiny, you listen for _events_ and handle them, +* *API Navigability* - based on the event-driven nature, the API is built around the type of events and drive the navigation based on the kind of event you want to handle, +* *Simplicity* - Mutiny provides only two types (`Multi` and `Uni`), which can handle any kind of asynchronous interactions. + +== Events? + +When you use Mutiny, you design a pipeline in which the events flow. +Your code observes these events and react. + +Each processing stage is a new pipe you append to the pipeline. +This pipe may change the events, create new ones, drops, buffers, whatever you need. + +In general, events flow from upstream to downstream, from source to sinks. +Some events can _swim_ upstream from the sinks to the source. + +Events going from upstream to downstream are published by `Publishers` and consumed by (downstream) `Subscribers,` which may also produce events for their own downstream, as illustrated by the following diagram: + +image::images/diagrams/mermaid-f71496c9eae5.svg[mermaid diagram] + +Four types of events can flow in this direction: + +* *Subscribed* - indicates that the upstream has taken into account the subscription - more on this later, +* *Items* - events containing some (business) _value_, +* *Completion* - event indicating that the source won't emit any more items, +* *Failure* - event telling that something terrible happened upstream and that the source cannot continue to emit items. + +`Failure` and `Completion` are terminal events. +Once they are sent, no more items will flow. + +Three types of events flow in the opposite direction, _i.e._ from downstream to upstream: + +* *Subscription* - event sent by a _subscriber_ to indicate its interest for the events (such as items) emitted by upstream +* *Requests* - event sent by a _subscriber_ indicating how many items event it can handle - this is related to back-pressure +* *Cancellation* - event sent by a _subscriber_ to stop the reception of events. + +In a typical scenario: + +[arabic] +. A subscriber _subscribes_ to the upstream - the upstream receive the `subscription request`, and when initialized sends the `subscribed` event to the subscriber +. The subscriber gets the `subscribed` event with a _subscription_ used to emit the `requests` and `cancellation` events +. The subscriber sends a `request` event indicating how many items it can handle at this moment; it can request 1, _n_, or infinite. +. The publisher receiving the `request` event starts emitting at most _n_ item events to the subscriber +. The subscriber can decide at any time to request more events or to cancel the subscription + +image::images/diagrams/mermaid-0277dcc2b534.svg[mermaid diagram] + +The `request` event is the cornerstone of the back-pressure protocol. +A subscriber should not request more than what it can handle, and a publisher should not emit more items than the amount of request received. + +[NOTE] +==== + +Mutiny uses the https://www.reactive-streams.org/[Reactive Streams] protocol for back-pressure management and the https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/concurrent/Flow.html[Java Flow APIs]. + +==== + +[IMPORTANT] +==== + +Don't forget to subscribe! + +If no subscriber _subscribes_, no items will be emitted. +More importantly, nothing will ever happen. + +If your program does not do anything, check that it subscribes, it's a very common error. + +==== + +== An event-driven API? + +Mutiny is an event-driven API. + +For each type of event, there is an `on` associated method that lets you handle this specific event. +For example: + +[source,java] +---- +include::{examples-dir}/java/guides/EventsTest.java[tags=code,indent=0] +---- + +Of course, the methods presented in this snippet are not very interesting, although they are quite useful to trace what's going on. + +You can see a common pattern emerging: + +[source,java] +---- +.onEvent().invoke(event -> ...); +---- + +`invoke` is just one of the methods available. +Each _group_ proposes methods specific to the type of event. For example, `onFailure().recover`, `onCompletion().continueWith` and so on. diff --git a/content/docs/smallrye-mutiny/latest/reference/why-is-asynchronous-important.adoc b/content/docs/smallrye-mutiny/latest/reference/why-is-asynchronous-important.adoc new file mode 100644 index 000000000..b06a0d807 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/reference/why-is-asynchronous-important.adoc @@ -0,0 +1,45 @@ += Why is asynchronous important? +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: reference,beginner + +We are living in a distributed world. + +Most of the applications built nowadays are distributed systems. +The Cloud, IoT, microservices, mobile application, even simple CRUD applications are distributed applications. + +Still, developing distributed systems is hard! + +.Distributed systems are asynchronous +image::reference/distributed_systems_are_asynchronous.png[Distributed systems are asynchronous] + +Communications in distributed systems are inherently asynchronous and unreliable. +Anything can go wrong, anytime, and often with no prior notice. + +Network disruptions, unavailable services, software, or hardware failures are just a tiny subset of the wide variety of failures that can happen in distributed systems. + +_Correctly_ building distributed applications is a considerable challenge, as it requires re-assessing almost everything we know from traditional software development. + +Most classic applications use a synchronous development model. +Synchronous code is easy to reason about, more comfortable to write and read than asynchronous code, but it has some hidden cost. +This cost emerges when building I/O intensive applications, quite common in distributed applications. + +In general, these traditional applications assign one thread per request, and so they handle multiple concurrent requests with multiple threads. +When the request processing needs to interact over the network, it uses that _worker_ thread, which blocks the thread until the response has been received. +This response may never come, so you need to add watchdogs handling timeouts and other resilience patterns. +And, to handle more requests concurrently, you need to create more threads. + +Threads come at a cost. +Each thread requires memory, and the more threads you have, the more CPU cycles are used to handle the context switches. +Thus, this model ends up being costly, limits the deployment density, and on the Cloud means that you pay bigger bills. + +Fortunately, there is another way, and it relies on non-blocking I/O, an efficient way to handle I/O interactions that do not require additional threads. +While applications using non-blocking I/O are more efficient and better suited for the Cloud's distributed nature, they come with a considerable constraint: you must never block the I/O thread. + +Thus, you need to implement your business logic using an asynchronous development model. + +I/O is not the only reason why asynchronous is essential in Today's systems. +Most of the interactions in the real world are asynchronous and event-driven. +Representing these interactions using synchronous processes is not only wrong; it also introduces fragility in your application. + +Asynchronous is a significant shift. +Mutiny helps you to take the plunge. diff --git a/content/docs/smallrye-mutiny/latest/tags-index.adoc b/content/docs/smallrye-mutiny/latest/tags-index.adoc new file mode 100644 index 000000000..6f43a9375 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/tags-index.adoc @@ -0,0 +1,24 @@ += Index +include::_attributes.adoc[] + +== Document kinds + +[width="100%",cols="18%,82%",options="header",] +|=== +|Tag |Description +|`tutorial` |Tutorials to get started with Mutiny. +|`guides` |Topic-centric guides on how to use Mutiny. +|`reference` |Reference documents to understand core concepts in Mutiny. +|=== + +== Audience level + +[cols=",",options="header",] +|=== +|Tag |Description +|`beginner` |Reading materials if you are new to Mutiny +|`intermediate` |Intermediate operations beyond the basics +|`advanced` |Advanced operations beyond classic usage of Mutiny +|=== + +== Tags diff --git a/content/docs/smallrye-mutiny/latest/tutorials/creating-multi-pipelines.adoc b/content/docs/smallrye-mutiny/latest/tutorials/creating-multi-pipelines.adoc new file mode 100644 index 000000000..8d551c63f --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/tutorials/creating-multi-pipelines.adoc @@ -0,0 +1,152 @@ += Creating `Multi` pipelines +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: tutorial,beginner + +A `Multi` represents a _stream_ of data. +A stream can emit 0, 1, n, or an infinite number of items. + +You will rarely create instances of `Multi` yourself but instead use a reactive client that exposes a Mutiny API. +Still, just like `Uni` there exists a rich API for creating `Multi` objects. + +== The Multi type + +A `Multi++<++T++>++` is a data stream that: + +* emits `0..n` item events +* emits a failure event +* emits a completion event for bounded streams + +[WARNING] +==== + +Failures are terminal events: after having received a failure no further item will be emitted. + +==== + +`Multi++<++T++>++` provides many operators that create, transform, and orchestrate `Multi` sequences. +The operators can be used to define a processing pipeline. +The events flow in this pipeline, and each operator can process or transform the events. + +`Multis` are lazy by nature. +To trigger the computation, you must subscribe. + +The following snippet provides a simple example of pipeline using `Multi`: + +[source,java] +---- +include::{examples-dir}/java/tutorials/CreatingMultiTest.java[tags=pipeline,indent=0] +---- + +== Subscribing to a Multi + +Remember, if you don't subscribe, nothing is going to happen. +Also, the pipeline is materialized for each _subscription_. + +When subscribing to a `Multi,` you can pass an item callback (invoked when the item is emitted), or pass two callbacks, one receiving the item and one receiving the failure, or three callbacks to handle respectively the item, failure and completion events. + +[source,java] +---- +include::{examples-dir}/java/tutorials/CreatingMultiTest.java[tags=subscription,indent=0] +---- + +Note the returned `Cancellable`: this object allows canceling the stream if need be. + +== Creating Multi from items + +There are many ways to create `Multi` instances. +See `Multi.createFrom()` to see all the possibilities. + +For instance, you can create a `Multi` from known items or from an `Iterable`: + +[source,java] +---- +include::{examples-dir}/java/tutorials/CreatingMultiTest.java[tags=simple,indent=0] +---- + +Every subscriber receives the same set of items (`1`, `2`... `5`) just after the subscription. + +You can also use `Suppliers`: + +[source,java] +---- +include::{examples-dir}/java/tutorials/CreatingMultiTest.java[tags=supplier,indent=0] +---- + +The `Supplier` is called for every subscriber, so each of them will get different values. + +[TIP] +==== + +You can create ranges using `Multi.createFrom().range(start, end)`. + +==== + +== Creating failing Multis + +Streams can also fail. + +Failures are used to indicate to the downstream subscribers that the source encountered a terrible error and cannot continue emitting items. +Create failed `Multi` instances with: + +[source,java] +---- +include::{examples-dir}/java/tutorials/CreatingMultiTest.java[tags=failed,indent=0] +---- + +== Creating empty Multis + +Unlike `Uni,` `Multi` streams don't send `null` items (this is forbidden in _reactive streams_). + +Instead `Multi` streams send completion events indicating that there are no more items to consume. +Of course, the completion event can happen even if there are no items, creating an empty stream. + +You can create such a stream using: + +[source,java] +---- +include::{examples-dir}/java/tutorials/CreatingMultiTest.java[tags=empty,indent=0] +---- + +== Creating Multis using an emitter (_advanced_) + +You can create a `Multi` using an emitter. +This approach is useful when integrating callback-based APIs: + +[source,java] +---- +include::{examples-dir}/java/tutorials/CreatingMultiTest.java[tags=emitter,indent=0] +---- + +The emitter can also send a failure. +It can also get notified of cancellation to, for example, stop the work in progress. + +== Creating Multis from _ticks_ (_advanced_) + +You can create a stream that emit a _ticks_ periodically: + +[source,java] +---- +include::{examples-dir}/java/tutorials/CreatingMultiTest.java[tags=ticks,indent=0] +---- + +The downstream receives a `long,` which is a counter. +For the first tick, it's 0, then 1, then 2, and so on. + +== Creating Multis from a generator (_advanced_) + +You can create a stream from some _initial state_, and a _generator function_: + +[source,java] +---- +include::{examples-dir}/java/tutorials/CreatingMultiTest.java[tags=generator,indent=0] +---- + +The initial state is given through a supplier (here `() -++>++ 1`). +The generator function accepts 2 arguments: + +* the current state, +* an emitter that can emit a new item, emit a failure, or emit a completion. + +The generator function return value is the next _current state_. +Running the previous example gives the following number suite: `++{++2, 4, 7, 11, 17, 26, 40, 61}`. diff --git a/content/docs/smallrye-mutiny/latest/tutorials/creating-uni-pipelines.adoc b/content/docs/smallrye-mutiny/latest/tutorials/creating-uni-pipelines.adoc new file mode 100644 index 000000000..6ffa03df1 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/tutorials/creating-uni-pipelines.adoc @@ -0,0 +1,128 @@ += Creating `Uni` pipelines +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: tutorial,beginner + +A `Uni` represents a _stream_ that can only emit either an item or a failure event. + +You rarely create instances of `Uni` yourself, but, instead, use a reactive client exposing a Mutiny API that provides `Uni` objects. +That being said, it can be handy at times. + +== The Uni type + +A `Uni++<++T++>++` is a specialized stream that emits only an item or a failure. +Typically, `Uni++<++T++>++` are great to represent asynchronous actions such as a remote procedure call, an HTTP request, or an operation producing a single result. + +`Uni++<++T++>++` provides many operators that create, transform, and orchestrate `Uni` sequences. + +As said, `Uni++<++T++>++` emits either an item or a failure. +Note that the item can be `null,` and the `Uni` API has specific methods for this case. + +Typically, a `Uni++<++Void++>++` always emits `null` as item event or a failure if the represented operation fails. +You can consider the item event as a completion signal indicating the success of the operation. + +The offered operators can be used to define a processing pipeline. +The event, either the item or failure, flows in this pipeline, and each operator can process or transform the event. +`Unis` are lazy by nature. + +To trigger the computation, you must have a final subscriber indicating your interest. +The following snippet provides a simple example of pipeline using `Uni`: + +[source,java] +---- +include::{examples-dir}/java/tutorials/CreatingUniTest.java[tags=pipeline,indent=0] +---- + +== Subscribing to a Uni + +[IMPORTANT] +==== + +Remember: if you don't subscribe, nothing is going to happen. +What's more, the pipeline is materialized for each _subscription_. + +==== + +When subscribing to a `Uni`, you can pass an item callback (invoked when the item is emitted), or two callbacks (one receiving the item and one receiving the failure): + +[source,java] +---- +include::{examples-dir}/java/tutorials/CreatingUniTest.java[tags=subscription,indent=0] +---- + +Note the returned `Cancellable`: this object allows canceling the operation if need be. + +== Creating Unis from items + +There are many ways to create `Uni` instances. +Use `Uni.createFrom()` to see all the possibilities. + +You can, for instance, create a `Uni` from a known value: + +[source,java] +---- +include::{examples-dir}/java/tutorials/CreatingUniTest.java[tags=simple,indent=0] +---- + +Every subscriber receives the item `1` just after the subscription. + +You can also pass a `Supplier`: + +[source,java] +---- +include::{examples-dir}/java/tutorials/CreatingUniTest.java[tags=supplier,indent=0] +---- + +The `Supplier` is called for every subscriber. +So, each of them will get a different value. + +== Creating failing Unis + +Operations represented by `Unis` can also emit a failure event, indicating that the operation failed. + +You can create failed `Uni` instances with: + +[source,java] +---- +include::{examples-dir}/java/tutorials/CreatingUniTest.java[tags=failed,indent=0] +---- + +== Creating `Uni++<++Void++>++` + +When the represented operation to not produce a result, you still need a way to indicate the operation's completion. +For this, you need to emit a `null` item: + +[source,java] +---- +include::{examples-dir}/java/tutorials/CreatingUniTest.java[tags=null,indent=0] +---- + +== Creating Unis using an emitter (_advanced_) + +You can create a `Uni` using an emitter. +This approach is useful when integrating callback-based APIs: + +[source,java] +---- +include::{examples-dir}/java/tutorials/CreatingUniTest.java[tags=emitter,indent=0] +---- + +The emitter can also send a failure. +It can also get notified of cancellation to, for example, stop the work in progress. + +== Creating Unis from a CompletionStage (_advanced_) + +You can also `Uni` objects from `CompletionStage` / `CompletableFuture`. +This is useful when integrating with APIs that are based on these types: + +[source,java] +---- +include::{examples-dir}/java/tutorials/CreatingUniTest.java[tags=cs,indent=0] +---- + +[TIP] +==== + +You can also create a `CompletionStage` from a `Uni` using `uni.subscribe().asCompletionStage()` + +==== diff --git a/content/docs/smallrye-mutiny/latest/tutorials/getting-mutiny.adoc b/content/docs/smallrye-mutiny/latest/tutorials/getting-mutiny.adoc new file mode 100644 index 000000000..651586995 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/tutorials/getting-mutiny.adoc @@ -0,0 +1,142 @@ += Getting started with Mutiny +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: tutorial,beginner + +== Using Mutiny in a Java application + +Add the _dependency_ to your project using your preferred build tool: + +[.tabs] +==== + +Apache Maven:: ++ +-- + +[source,xml,subs="attributes+"] +---- + + io.smallrye.reactive + mutiny + {versions-mutiny} + +---- + +-- + +Gradle (Groovy):: ++ +-- + +[source,groovy,subs="attributes+"] +---- +implementation 'io.smallrye.reactive:mutiny:{versions-mutiny}' +---- + +-- + +Gradle (Kotlin):: ++ +-- + +[source,kotlin,subs="attributes+"] +---- +implementation("io.smallrye.reactive:mutiny:{versions-mutiny}") +---- + +-- + +JBang:: ++ +-- + +[source,java,subs="attributes+"] +---- +//DEPS io.smallrye.reactive:mutiny:{versions-mutiny} +---- + +-- + +==== + +== Using Mutiny with Quarkus + +Most of the https://quarkus.io[Quarkus] extensions with reactive capabilities already depend on Mutiny. + +You can also add the `quarkus-mutiny` dependency explicitly from the command-line: + +[source,bash] +---- +mvn quarkus:add-extension -Dextensions=mutiny +---- + +or by editing the `pom.xml` file and adding: + +[source,xml] +---- + + io.quarkus + quarkus-mutiny + +---- + +== Using Mutiny with Vert.x + +Most of the https://vertx.io[Eclipse Vert.x] stack modules are available through the https://smallrye.io/smallrye-mutiny-vertx-bindings/[SmallRye Mutiny Vert.x Bindings] project. + +Bindings for Vert.x modules are named by prepending `smallrye-mutiny-`. +As an example here's how to add a dependency to the `vertx-core` Mutiny bindings: + +[.tabs] +==== + +Apache Maven:: ++ +-- + +[source,xml,subs="attributes+"] +---- + + io.smallrye.reactive + smallrye-mutiny-vertx-core + {versions-vertx-bindings} + +---- + +-- + +Gradle (Groovy):: ++ +-- + +[source,groovy,subs="attributes+"] +---- +implementation 'io.smallrye.reactive:smallrye-mutiny-vertx-core:{versions-vertx-bindings}' +---- + +-- + +Gradle (Kotlin):: ++ +-- + +[source,kotlin,subs="attributes+"] +---- +implementation("io.smallrye.reactive:smallrye-mutiny-vertx-core:{versions-vertx-bindings}") +---- + +-- + +JBang:: ++ +-- + +[source,java,subs="attributes+"] +---- +//DEPS io.smallrye.reactive:smallrye-mutiny-vertx-core:{versions-vertx-bindings} +---- + +-- + +==== diff --git a/content/docs/smallrye-mutiny/latest/tutorials/handling-failures.adoc b/content/docs/smallrye-mutiny/latest/tutorials/handling-failures.adoc new file mode 100644 index 000000000..5dab71250 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/tutorials/handling-failures.adoc @@ -0,0 +1,96 @@ += Handling failures +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: tutorial,beginner + +Mutiny provides several operators to handle failures. + +Remember, failures are terminal events sent by the observed stream, indicating that something _bad_ happened. +After a failure, no more items are being received. + +When such an event is received, you can: + +* propagate the failure downstream (default), or +* transform the failure into another failure, or +* recover from it by switching to another stream, passing a fallback item, or completing, or +* retrying (covered in the next guide) + +If you don't handle the failure event, it is propagated downstream until a stage handles the failure or reaches the final subscriber. + +[IMPORTANT] +==== + +on `Multi`, a failure cancels the subscription, meaning you will not receive any more items. +The `retry` operator lets you re-subscribe and continue the reception. + +==== + +== Observing failures + +It can be useful to execute some custom action when a failure happens. +For example, you can log the failure: + +[source,java] +---- +include::{examples-dir}/java/tutorials/HandlingFailuresTest.java[tags=invoke,indent=0] +---- + +[TIP] +==== + +You can also execute an asynchronous action using `onFailure().call(Function++<++Throwable, Uni++<++?++>++)`. +The received failure will be propagated downstream when the `Uni` produced by the passed function emits its item. + +==== + +== Transforming failures + +Another useful action on failure is to transform the failure into a _more meaningful_ failure. + +Typically, you can wrap a low-level failure (like an `IOException`) into a business failure (`ServiceUnavailableException`): + +[source,java] +---- +include::{examples-dir}/java/tutorials/HandlingFailuresTest.java[tags=transform,indent=0] +---- + +== Recovering using fallback item(s) + +In general, upon failure, you want to recover. +The first approach is to recover by replacing the failure with an item: + +[source,java] +---- +include::{examples-dir}/java/tutorials/HandlingFailuresTest.java[tags=recover-item,indent=0] +---- + +The second approach receives a `Supplier` to compute the fallback item. +For the downstream, it didn't fail; it gets the fallback item instead. + +However, don't forget that failures are terminal! +So for `Multi`, the downstream receives the fallback item followed by the completion signal, as no more items can be produced. + +== Completing on failure + +When observing a `Multi` you can replace the failure with the completion signal: + +[source,java] +---- +include::{examples-dir}/java/tutorials/HandlingFailuresTest.java[tags=recover-completion,indent=0] +---- + +The downstream won't see the failure, just the completion event. + +== Switching to another stream + +On failure, you may want to switch to an alternate stream. +When the failure is received, it subscribes to this other stream and propagates the items from this stream instead: + +[source,java] +---- +include::{examples-dir}/java/tutorials/HandlingFailuresTest.java[tags=recover-switch,indent=0] +---- + +The `recoverWithUni` and `recoverWithMulti` methods replace the failed upstream with the returned stream. + +The fallback streams must produce the same type of event as the original upstream. diff --git a/content/docs/smallrye-mutiny/latest/tutorials/hello-mutiny.adoc b/content/docs/smallrye-mutiny/latest/tutorials/hello-mutiny.adoc new file mode 100644 index 000000000..80fab7b4e --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/tutorials/hello-mutiny.adoc @@ -0,0 +1,75 @@ += Hello Mutiny! +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: tutorial,beginner + +Once you made Mutiny available to your classpath, you can start writing code. +Let's start with this simple program: + +[source,java] +---- +include::{examples-dir}/java/FirstProgram.java[indent=0] +---- + +This program prints: + +.... +>> HELLO MUTINY +.... + +== Dissecting the pipeline + +What's interesting is how this message is _built_. +We described a processing pipeline taking an item, processing it and finally consuming it. + +First, we create a `Uni`, one of the two types with `Multi` that Mutiny provides. +A `Uni` is a stream emitting either a single item or a failure. + +Here, we create a `Uni` emitting the `"hello"` item. +This is the input of our pipeline. +Then we process this item: + +* we append `" mutiny"`, then +* we make it an uppercase string. + +This forms the processing part of our pipeline, and then we finally *subscribe* to the pipeline. + +This last part is essential. +If you don't have a final subscriber, nothing is going to happen. +Mutiny types are lazy, meaning that you need to express your interest. +If you don't, the computation won't even start. + +[IMPORTANT] +==== + +If your program doesn't do anything, verify that you didn't forget to subscribe! + +==== + +== Mutiny uses a builder API! + +Another important aspect is the pipeline construction. +Appending a new _stage_ to a pipeline returns a new `Uni.` + +The previous program is equivalent to: + +[source,java] +---- +include::{examples-dir}/java/FirstProgramTest.java[tags=uni,indent=0] +---- + +It is fundamental to understand that this program is not equivalent to: + +[source,java] +---- +include::{examples-dir}/java/FirstProgramTest.java[tags=uni2,indent=0] +---- + +This program just prints `"++>>++ hello"`, as it does not use the appended stages and the final subscriber consumes the first `Uni.` + +[WARNING] +==== + +Mutiny APIs are not fluent and each computation stage returns a new object. + +==== diff --git a/content/docs/smallrye-mutiny/latest/tutorials/mutiny-workshop.adoc b/content/docs/smallrye-mutiny/latest/tutorials/mutiny-workshop.adoc new file mode 100644 index 000000000..99fcf5a81 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/tutorials/mutiny-workshop.adoc @@ -0,0 +1,15 @@ += Go further with the Mutiny workshop! +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: tutorial,beginner + +One great option to teach yourself Mutiny is to go through the https://github.com/smallrye/smallrye-mutiny/tree/main/workshop-examples[Mutiny workshop examples]. + +These self-contained https://jbang.dev/[JBang] scripts cover the main parts of the Mutiny APIs. + +It's a fun and easy way to discover Mutiny! + +Check out https://github.com/smallrye/smallrye-mutiny/tree/main/workshop-examples to learn more. + +.Running a workshop sample +image::tutorials/running-workshop-sample.png[Running a workshop sample,width=400] diff --git a/content/docs/smallrye-mutiny/latest/tutorials/observing-events.adoc b/content/docs/smallrye-mutiny/latest/tutorials/observing-events.adoc new file mode 100644 index 000000000..3d9e1ca28 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/tutorials/observing-events.adoc @@ -0,0 +1,111 @@ += Observing events +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: tutorial,beginner + +Learn how to observe the events emitted by `Uni` and `Multi` instances. + +== Events + +`Uni` and `Multi` emit _events_. +Your code is going to observe and process these events. + +Most of the time, your code is only interested in item and failure events. +But there are other kinds of events such as cancellation, request, completion, and so on: + +[width="100%",cols=">12%,9%,<15%,<64%",options="header",] +|=== +|Event |Uni / Multi |Direction |Note +|*item* |Uni {plus} Multi |upstream -++>++ downstream |The upstream sent an item. +|*failure* |Uni {plus} Multi |upstream -++>++ downstream |The upstream failed. +|*completion* |Multi |upstream -++>++ downstream |The upstream completed. +|*subscribe* |Uni and Multi |downstream -++>++ upstream |A downstream subscriber is interested in the data. +|*subscription* |Uni and Multi |upstream -++>++ downstream |Event happening after a `subscribe` event to indicate that the upstream acknowledged the subscription. +|*cancellation* |Uni and Multi |downstream -++>++ upstream |A downstream subscriber does not want any more events. +|*overflow* |Multi |upstream -++>++ downstream |The upstream has emitted more than the downstream can handle. +|*request* |Multi |downstream -++>++ upstream |The downstream indicates its capacity to handle `n` items. +|=== + +It’s not rare that you need to look at these various events to understand better what’s going on or implement specific side effects. +For example, you may need to close a resource after a completion event or log a message on failure or cancellation. + +For each kind of event, there is an associated group providing the methods to handle that specific event: `onItem()`, `onFailure()`, `onCompletion()` and so on. +These groups provide two methods to _peek_ at the various events without impacting its distribution: `invoke(...)` and `call(...)`. +It does not transform the received event; it notifies you that something happened and let you react. +Once this _reaction_ completes, the event is propagated downstream or upstream depending on the direction of the event. + +== The `invoke` method + +The invoke method is synchronous and the passed callback does not return anything. +Mutiny invokes the configured callback when the observed stream dispatches the event: + +[source,java] +---- +include::{examples-dir}/java/tutorials/ObserveTest.java[tags=invoke,indent=0] +---- + +As said above, `invoke` is synchronous. +Mutiny invokes the callback and propagates the event downstream when the callback returns. +It blocks the dispatching. + +image::images/diagrams/mermaid-e88e1918cd26.svg[mermaid diagram] + +Of course, we highly recommend you not to block. + +The following snippets show how you can log the different types of events. + +[source,java] +---- +include::{examples-dir}/java/tutorials/ObserveTest.java[tags=invoke-all,indent=0] +---- + +The arrows from the previous code snippet indicate if the event comes from the upstream (source) or downstream (consumer) (see the table above for more details). +The `invoke` method does not change the event, except in one case. +If the callback throws an exception, the downstream does not get the actual event but get a failure event instead. + +When observing the failure event, if the callback throws an exception, Mutiny propagates a `CompositeException` aggregating the original failure and the callback failure. + +== The `call` method + +Unlike `invoke`, `call` is asynchronous, and the callback returns a `Uni++<++?++>++` object. + +`call` is often used when you need to implement asynchronous side-effects, such as closing resources. + +image::images/diagrams/mermaid-bc092005fd3f.svg[mermaid diagram] + +Mutiny does not dispatch the original event downstream until the Uni returned by the callback emits an item: + +[source,java] +---- +include::{examples-dir}/java/tutorials/ObserveTest.java[tags=call,indent=0] +---- + +As shown in the previous snippet, you can use this approach to delay items. +But, the primary use case is about completing asynchronous actions such as calling an asynchronous `close` method on a resource: + +[source,java] +---- +include::{examples-dir}/java/tutorials/ObserveTest.java[tags=close,indent=0] +---- + +Under the hood, Mutiny gets the `Uni` (by invoking the callback) and subscribes to it. +It observes the item or failure event from that Uni. +It discards the item value as only the emission matters in this case. + +If the callback throws an exception or the produced `Uni` produces a failure, Mutiny propagates that failure (or a `CompositeException`) downstream, replacing the original event. + +== Summary + +* The `invoke` and `call` methods are handy when you need to observe a `Uni` or a `Multi` without changing the transiting events. +* Use `invoke` for implementing synchronous side-effects or logging events. +* The asynchronous nature of `call` makes it perfect for implementing asynchronous side-effects, such as closing resources, flushing data, delay items, and so on. + +The following table highlights the key differences: + +[width="100%",cols=">20%,<33%,<47%",options="header",] +|=== +| |`invoke` |`call` +|*Nature* |synchronous |asynchronous +|*Return type* |`void` |`Uni++<++T++>++` +|*Main use cases* |logging, synchronous side-effects |I/O operations, closing resources, flushing data +|=== diff --git a/content/docs/smallrye-mutiny/latest/tutorials/retrying.adoc b/content/docs/smallrye-mutiny/latest/tutorials/retrying.adoc new file mode 100644 index 000000000..c2ec63ecc --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/tutorials/retrying.adoc @@ -0,0 +1,67 @@ += Retrying on failures +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: tutorial,beginner + +It is common to want to retry if something terrible happened. + +You can retry upon failure. +The https://quarkus.io/blog/uni-retry/[How does retry... retries] blog post provides a more detailed overview of the retry mechanism. + +[NOTE] +==== + +If despite multiple attempts, it still fails, the failure is propagated downstream. + +==== + +== Retry multiple times + +To retry on failure, use `onFailure().retry()`: + +[source,java] +---- +include::{examples-dir}/java/tutorials/RetryTest.java[tags=retry-at-most,indent=0] +---- + +You pass the number of retries as a parameter. + +[IMPORTANT] +==== + +While `.onFailure().retry().indefinitely()` is available, it may never terminate, so use it with caution. + +==== + +== Introducing delays + +By default, `retry` retries immediately. +When using remote services, it is often better to delay a bit the attempts. + +Mutiny provides a method to configure an exponential backoff: a growing delay between retries. +Configure the exponential backoff as follows: + +[source,java] +---- +include::{examples-dir}/java/tutorials/RetryTest.java[tags=retry-backoff,indent=0] +---- + +The backoff is configured with the initial and max delay. +Optionally, you can also configure a jitter to add a pinch of randomness to the delay. + +When using exponential backoff, you may not want to configure the max number of attempts (`atMost`), but a deadline. +To do so, use either `expireIn` or `expireAt`. + +== Deciding to retry + +As an alternative to `atMost`, you can also use `until`. +This method accepts a predicate called after every failure. +When used, a backoff should not be used. + +If the predicate returned `true,` it retries. +Otherwise, it stops retrying and propagates the last failure downstream: + +[source,java] +---- +include::{examples-dir}/java/tutorials/RetryTest.java[tags=retry-until,indent=0] +---- diff --git a/content/docs/smallrye-mutiny/latest/tutorials/running-workshop-sample.png b/content/docs/smallrye-mutiny/latest/tutorials/running-workshop-sample.png new file mode 100644 index 000000000..6eec15382 Binary files /dev/null and b/content/docs/smallrye-mutiny/latest/tutorials/running-workshop-sample.png differ diff --git a/content/docs/smallrye-mutiny/latest/tutorials/transforming-items-asynchronously.adoc b/content/docs/smallrye-mutiny/latest/tutorials/transforming-items-asynchronously.adoc new file mode 100644 index 000000000..a84902200 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/tutorials/transforming-items-asynchronously.adoc @@ -0,0 +1,133 @@ += Transforming items asynchronously +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: tutorial,beginner + +The previous examples illustrated how to transform each item from a stream into another item. +Yet, there are cases where we need to go beyond this, for example: + +* Transforming an item into a `Uni` -- any asynchronous processing such as calling a remote service, interacting with a database, etc +* Transforming an item into a `Multi` -- producing a multi-items stream based on the incoming item, filtering out items, etc + +Having the possibility to transform an item into a stream gives us many opportunities. +To implement such transformations, we use `onItem().transformToUni(Function++<++T, Uni++<++O++>>++)` and `onItem().transformToMulti(Function++<++T, Multi++<++O++>>++)` + +== Uni - Transforming an item into a Uni + +image::images/diagrams/mermaid-f67adb6018b3.svg[mermaid diagram] + +Imagine that you have a `Uni++<++String++>++`, and you want to call a remote service. + +Calling a remote service is an asynchronous action represented by a `Uni`, as in: + +[source,java] +---- +include::{examples-dir}/java/tutorials/TransformItemsAsyncTest.java[tags=service,indent=0] +---- + +To call this service, you need to transform the item received from the first `Uni` into the `Uni` returned by the service: + +[source,java] +---- +include::{examples-dir}/java/tutorials/TransformItemsAsyncTest.java[tags=call,indent=0] +---- + +This snippet _chains_ the first `Uni` with another one. +The returned `Uni` (`result`) emits the result from the remote service or a failure if anything wrong happened: + +[source,java] +---- +include::{examples-dir}/java/tutorials/TransformItemsAsyncTest.java[tags=chain,indent=0] +---- + +== Uni - Transforming an item into a Multi + +The previous example produced a single item. +You may want to transform the received item into a stream which is... a `Multi`! + +[source,java] +---- +include::{examples-dir}/java/tutorials/TransformItemsAsyncTest.java[tags=uni-to-multi,indent=0] +---- + +This code creates a stream of two elements, duplicating the received item. + +[source,java] +---- +include::{examples-dir}/java/tutorials/TransformItemsAsyncTest.java[tags=uni-to-multi-2,indent=0] +---- + +image::images/diagrams/mermaid-3b8d4c58438a.svg[mermaid diagram] + +The produced `Multi` objects can of course be more complicated than that and emit items in an asynchronous fashion. + +== Transforming items from Multi - the _merge_ vs _concatenate_ dilemma + +When transforming items emitted by an upstream `Multi,` we need to answer the following question: how are we going to _merge_ the produced items back? + +Let's take an example. +Imagine a `Multi` emitting the `Cameron` and `Donna` items (in order), and you want to call the `invokeRemoteGreetingService` from above. +It thus calls `invokeRemoteGreetingService("Cameron")` then `invokeRemoteGreetingService("Donna")`. + +The service does not have a constant response time (because of network latency or the load), which means that responses can be interleaved. +Indeed, you may receive `"Hello Donna"` before `"Hello Cameron"`. + +Now, how do you want to handle this case? +Do you need to preserve the order and ensure that the downstream subscriber will always get `"Hello Cameron"` first, or do you accept interleaved responses? + +When transforming items from `Multi` into streams, you need to decide in which order the items emitted by the produced stream are going to be received by the downstream subscriber. +Mutiny offers two possibilities: + +[arabic] +. *Merging* -- it does not preserve the order and emits the items from the produced streams as they come, or +. *Concatenating* -- it maintains and concatenates the streams produced for each item. + +== Multi - Transforming an item into a Uni + +To implement the scenario from the last section, you will use `onItem().transformToUniAndMerge` or `onItem().transformToUniAndConcatenate()` depending on your ordering choice: + +[source,java] +---- +include::{examples-dir}/java/tutorials/TransformItemsAsyncTest.java[tags=merge-concat,indent=0] +---- + +[IMPORTANT] +==== + +* When merging: items from the source `Multi` _may_ be processed *concurrently* depending on the concurrency level that has been set, if any. +* When concatenating: items from the source `Multi` are processed *in order*, waiting for each `Uni` to complete before moving on to the next item. + +==== + +=== Controlling concurrency with merge + +The `merge` method accepts an optional `concurrency` parameter that limits how many inner streams can be subscribed to concurrently: + +[source,java] +---- +include::{examples-dir}/java/tutorials/TransformItemsAsyncTest.java[tags=merge-concurrency,indent=0] +---- + +When not provided, the default concurrency is configured using `Infrastructure.getBufferSizeS()`. + +[WARNING] +.Merge concurrency +==== + +When using merge with limited concurrency, be aware of potential backpressure issues. +*Setting concurrency too low* can cause upstream request starvation if the number of subscribed but not emitting inner streams surpasses the level of concurrency. + +*Unbounded concurrency* eliminates the request starvation issue by removing the limit on the number of subscribed inner streams to merge. + +==== + +== Multi - Transforming an item into a Multi + +`onItem().transformToMultiAndMerge` and `onItem().transformToMultiAndConcatenate` transform incoming items into `Multi` streams. +The produced `Multi` objects are either _merged_ or _concatenated_: + +[source,java] +---- +include::{examples-dir}/java/tutorials/TransformItemsAsyncTest.java[tags=merge-concat-multi,indent=0] +---- + +Like after `transformToUni`, the `merge` method after `transformToMulti` also accepts an optional concurrency parameter with the same considerations regarding backpressure and request starvation when used with infinite streams. diff --git a/content/docs/smallrye-mutiny/latest/tutorials/transforming-items.adoc b/content/docs/smallrye-mutiny/latest/tutorials/transforming-items.adoc new file mode 100644 index 000000000..f13a92f76 --- /dev/null +++ b/content/docs/smallrye-mutiny/latest/tutorials/transforming-items.adoc @@ -0,0 +1,53 @@ += Transforming items +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: tutorial,beginner + +Both `Unis` and `Multis` emit _items_. + +One of the most common operations you will do is transforming these items using a _synchronous_ 1-to-1 function. + +To achieve this, you use `onItem().transform(Function++<++T, U++>++)`. +It calls the passed function for each item and produces the result as an item which is propagated downstream. + +image::images/diagrams/mermaid-e6dd95e32a3f.svg[mermaid diagram] + +== Transforming items produced by a Uni + +Let's imagine you have a `Uni++<++String++>++,` and you want to capitalize the received `String`. +Implementing this transformation is done as follows: + +[source,java] +---- +include::{examples-dir}/java/tutorials/TransformItemsTest.java[tags=uni-transform,indent=0] +---- + +== Transforming items produced by a Multi + +The only difference for `Multi` is that the function is called for each item: + +[source,java] +---- +include::{examples-dir}/java/tutorials/TransformItemsTest.java[tags=multi-transform,indent=0] +---- + +The produced items are passed to the downstream subscriber: + +[source,java] +---- +include::{examples-dir}/java/tutorials/TransformItemsTest.java[tags=multi-transform-2,indent=0] +---- + +== What if the transformation failed? + +If the transformation throws an exception, that exception is caught and passed to the downstream subscriber as a _failure_ event. +It also means that the subscriber won't get further item after that failure. + +== Chaining multiple transformations + +You can chain multiple transformations: + +[source,java] +---- +include::{examples-dir}/java/tutorials/TransformItemsTest.java[tags=chain,indent=0] +---- diff --git a/content/docs/smallrye-mutiny/main/_attributes.adoc b/content/docs/smallrye-mutiny/main/_attributes.adoc new file mode 100644 index 000000000..f1640defb --- /dev/null +++ b/content/docs/smallrye-mutiny/main/_attributes.adoc @@ -0,0 +1,22 @@ +:project-version: 3.3.0 +// Attributes shared by every page. smallrye.io overrides the ones it needs when it publishes. +// Pages in subdirectories set :version-root: (../ per level) before including this file. +ifndef::version-root[:version-root:] +:idprefix: +:idseparator: - +:sectanchors: +:examples-dir: {version-root}_examples +:partialsdir: {version-root}_partials +:project-name: SmallRye Mutiny +:javadoc-version: latest +:versions-mutiny: 3.3.0 +:versions-vertx-bindings: 4.0.0-beta2 +:generated-dir: {version-root}_generated + +// Added by smallrye.io (tools/docs/Docs.java sync) +:javadoc-version: 3.3.0 +:relfileprefix: /docs/smallrye-mutiny/main/ +:imagesdir: /docs/smallrye-mutiny/main +:docs-project: smallrye-mutiny +:docs-version: main +:docs-ref: main diff --git a/content/docs/smallrye-mutiny/main/_examples/java/FirstProgram.java b/content/docs/smallrye-mutiny/main/_examples/java/FirstProgram.java new file mode 100644 index 000000000..87d40de5d --- /dev/null +++ b/content/docs/smallrye-mutiny/main/_examples/java/FirstProgram.java @@ -0,0 +1,11 @@ +import io.smallrye.mutiny.Uni; + +public class FirstProgram { + + public static void main(String[] args) { + Uni.createFrom().item("hello") + .onItem().transform(item -> item + " mutiny") + .onItem().transform(String::toUpperCase) + .subscribe().with(item -> System.out.println(">> " + item)); + } +} \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/main/_examples/java/FirstProgramTest.java b/content/docs/smallrye-mutiny/main/_examples/java/FirstProgramTest.java new file mode 100644 index 000000000..d5c464230 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/_examples/java/FirstProgramTest.java @@ -0,0 +1,44 @@ +import guides.extension.SystemOut; +import guides.extension.SystemOutCaptureExtension; +import io.smallrye.mutiny.Uni; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(SystemOutCaptureExtension.class) +public class FirstProgramTest { + + @Test + public void testFirstProgram(SystemOut out) { + FirstProgram.main(new String[0]); + assertThat(out.get()).contains(">> HELLO MUTINY"); + } + + @Test + public void testUniBuilder(SystemOut out) { + // tag::uni[] + Uni uni1 = Uni.createFrom().item("hello"); + Uni uni2 = uni1.onItem().transform(item -> item + " mutiny"); + Uni uni3 = uni2.onItem().transform(String::toUpperCase); + + uni3.subscribe().with(item -> System.out.println(">> " + item)); + // end::uni[] + assertThat(out.get()).contains("HELLO MUTINY"); + assertThat(uni3.await().indefinitely()).isEqualTo("HELLO MUTINY"); + } + + @Test + public void testUniBuilder2(SystemOut out) { + // tag::uni2[] + Uni uni = Uni.createFrom().item("hello"); + + uni.onItem().transform(item -> item + " mutiny"); + uni.onItem().transform(String::toUpperCase); + + uni.subscribe().with(item -> System.out.println(">> " + item)); + // end::uni2[] + assertThat(out.get()).contains("hello"); + assertThat(uni.await().indefinitely()).isEqualTo("hello"); + } +} diff --git a/content/docs/smallrye-mutiny/main/_examples/java/guides/BranchingTest.java b/content/docs/smallrye-mutiny/main/_examples/java/guides/BranchingTest.java new file mode 100644 index 000000000..320469593 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/_examples/java/guides/BranchingTest.java @@ -0,0 +1,64 @@ +package guides; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import org.junit.jupiter.api.Test; + +import java.util.Random; + +public class BranchingTest { + + @Test + public void uni() { + // tag::pipeline[] + Random random = new Random(); + Uni.createFrom().item(() -> random.nextInt(100)) + .onItem().transformToUni(n -> { + if (n % 2 == 0) { + return evenOperation(n); + } else { + return oddOperation(n); + } + }) + .subscribe().with(System.out::println); + // end::pipeline[] + } + + // tag::branches[] + Uni evenOperation(int n) { + return Uni.createFrom().item("Even number: " + n) + .onItem().invoke(() -> System.out.println("(even branch)")); + } + + Uni oddOperation(int n) { + return Uni.createFrom().item("Odd number: " + n) + .onItem().invoke(() -> System.out.println("(odd branch)")); + } + // end::branches[] + + @Test + public void multi() { + // tag::multi-pipeline[] + Random random = new Random(); + Multi.createBy().repeating().supplier(random::nextInt).atMost(20) + .onItem().transformToUniAndMerge(n -> { + System.out.println("----"); + if (n < 0) { + return drop(); + } else if (n % 2 == 0) { + return evenOperation(n); + } else { + return oddOperation(n); + } + }) + .subscribe().with(str -> System.out.println("=> " + str)); + // end::multi-pipeline[] + } + + // tag::drop[] + Uni drop() { + return Uni.createFrom().nullItem() + .onItem().invoke(() -> System.out.println("(dropping negative value)")); + } + // end::drop[] +} diff --git a/content/docs/smallrye-mutiny/main/_examples/java/guides/CompletionStageTest.java b/content/docs/smallrye-mutiny/main/_examples/java/guides/CompletionStageTest.java new file mode 100644 index 000000000..27b0fd6ba --- /dev/null +++ b/content/docs/smallrye-mutiny/main/_examples/java/guides/CompletionStageTest.java @@ -0,0 +1,77 @@ +package guides; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.assertj.core.api.Assertions.assertThat; + +public class CompletionStageTest { + + @Test + public void testSubscribeAsCompletionStage() { + AtomicInteger counter = new AtomicInteger(); + Uni uni = Uni.createFrom().item(() -> "hello-" + counter.getAndIncrement()); + // tag::uni-subscribe-cs[] + CompletionStage cs = uni.subscribeAsCompletionStage(); + // end::uni-subscribe-cs[] + + assertThat(cs.toCompletableFuture().join()).isEqualTo("hello-0"); + + // tag::uni-subscribe-cs-twice[] + // Trigger the underlying operation twice: + CompletionStage cs1 = uni.subscribeAsCompletionStage(); + CompletionStage cs2 = uni.subscribeAsCompletionStage(); + // end::uni-subscribe-cs-twice[] + assertThat(cs1.toCompletableFuture().join()).isEqualTo("hello-1"); + assertThat(cs2.toCompletableFuture().join()).isEqualTo("hello-2"); + } + + + @Test + public void test() { + Executor executor = Runnable::run; + // tag::create-uni[] + Uni uni1 = Uni + // Create from a Completion Stage + .createFrom().completionStage( + CompletableFuture.supplyAsync(() -> "hello", executor) + ) + .onItem().transform(String::toUpperCase); + + Uni uni2 = Uni + // Create from a Completion Stage supplier (recommended) + .createFrom().completionStage( + () -> CompletableFuture.supplyAsync(() -> "hello", executor) + ) + .onItem().transform(String::toUpperCase); + // end::create-uni[] + + assertThat(uni1.await().indefinitely()).isEqualTo("HELLO"); + assertThat(uni2.await().indefinitely()).isEqualTo("HELLO"); + + // tag::create-multi[] + Multi multi1 = Multi + .createFrom().completionStage( + CompletableFuture.supplyAsync(() -> "hello", executor) + ) + .onItem().transform(String::toUpperCase); + + Multi multi2 = Multi + .createFrom().completionStage(() -> + CompletableFuture.supplyAsync(() -> "hello", executor) + ) + .onItem().transform(String::toUpperCase); + // end::create-multi[] + + assertThat(multi1.collect().asList().await().indefinitely()).containsExactly("HELLO"); + assertThat(multi2.collect().asList().await().indefinitely()).containsExactly("HELLO"); + + } +} diff --git a/content/docs/smallrye-mutiny/main/_examples/java/guides/ContextPassingTest.java b/content/docs/smallrye-mutiny/main/_examples/java/guides/ContextPassingTest.java new file mode 100644 index 000000000..00897d788 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/_examples/java/guides/ContextPassingTest.java @@ -0,0 +1,99 @@ +package guides; + +import io.smallrye.mutiny.Context; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class ContextPassingTest { + + void contextManipulation() { + + // tag::contextManipulation[] + // Create a context using key / value pairs + Context context = Context.of( + "X-CUSTOMER-ID", "1234", + "X-SPAN-ID", "foo-bar-baz" + ); + + // Get an entry + System.out.println( + context.get("X-SPAN-ID")); + + // Get an entry, use a supplier for a default value if the key is not present + System.out.println( + context.getOrElse("X-SPAN-ID", () -> "")); + + // Add an entry + context.put("foo", "bar"); + + // Remove an entry + context.delete("foo"); + // end::contextManipulation[] + } + + @Test + void sampleUsage() { + Multi pipeline = Multi.createFrom().range(1, 10); + String customerId = "1234"; + + // tag::contextSampleUsage[] + Context context = Context.of("X-CUSTOMER-ID", customerId); + + pipeline.withContext((multi, ctx) -> multi.onItem().transformToUniAndMerge(item -> makeRequest(item, ctx.get("X-CUSTOMER-ID")))) + .subscribe().with(context, item -> handleResponse(item), err -> handleFailure(err)); + // end::contextSampleUsage[] + } + + @Test + void sampleUsageAttachedContext() { + Multi pipeline = Multi.createFrom().range(1, 10); + String customerId = "1234"; + + // tag::contextAttachedSampleUsage[] + Context context = Context.of("X-CUSTOMER-ID", customerId); + + pipeline.attachContext() + .onItem().transformToUniAndMerge(item -> makeRequest(item.get(), item.context().get("X-CUSTOMER-ID"))) + .subscribe().with(context, item -> handleResponse(item), err -> handleFailure(err)); + // end::contextAttachedSampleUsage[] + } + + @Test + void builderUsage() { + Context context = Context.of("X-SPAN-ID", "1234"); + + // tag::builderUsage[] + Uni.createFrom().context(ctx -> makeRequest("db1", ctx.get("X-SPAN-ID"))) + .subscribe().with(context, item -> handleResponse(item), err -> handleFailure(err)); + // end::builderUsage[] + } + + @Test + void emitterUsage() { + Context context = Context.of("X-SPAN-ID", "1234"); + + // tag::emitterUsage[] + Multi.createFrom().emitter(emitter -> { + String customerId = emitter.context().get("X-SPAN-ID"); + for (int i = 0; i < 10; i++) { + emitter.emit("@" + i + " [" + customerId + "]"); + } + emitter.complete(); + }); + // end::emitterUsage[] + } + + private void handleFailure(Throwable err) { + Assertions.fail(err); + } + + private void handleResponse(String item) { + Assertions.assertTrue(item.endsWith("::1234")); + } + + private Uni makeRequest(Object item, Object o) { + return Uni.createFrom().item(item + "::" + o); + } +} diff --git a/content/docs/smallrye-mutiny/main/_examples/java/guides/EventsTest.java b/content/docs/smallrye-mutiny/main/_examples/java/guides/EventsTest.java new file mode 100644 index 000000000..85fd1d709 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/_examples/java/guides/EventsTest.java @@ -0,0 +1,60 @@ +package guides; + +import guides.extension.SystemOut; +import guides.extension.SystemOutCaptureExtension; +import io.smallrye.mutiny.Uni; +import org.junit.jupiter.api.Test; + +import io.smallrye.mutiny.Multi; +import org.junit.jupiter.api.extension.ExtendWith; + +import static org.awaitility.Awaitility.await; + +@ExtendWith(SystemOutCaptureExtension.class) +public class EventsTest { + + private void log(String s) { + System.out.println(s); + } + + @SuppressWarnings("Convert2MethodRef") + @Test + public void test(SystemOut out) { + // tag::code[] + Multi source = Multi.createFrom().items("a", "b", "c"); + source + .onItem() // Called for every item + .invoke(item -> log("Received item " + item)) + .onFailure() // Called on failure + .invoke(failure -> log("Failed with " + failure)) + .onCompletion() // Called when the stream completes + .invoke(() -> log("Completed")) + .onSubscription() // Called when the upstream is ready + .invoke(subscription -> log("We are subscribed!")) + .onCancellation() // Called when the downstream cancels + .invoke(() -> log("Cancelled :-(")) + .onRequest() // Called on downstream requests + .invoke(n -> log("Downstream requested " + n + " items")) + .subscribe() + .with(item -> log("Subscriber received " + item)); + // end::code[] + + // + Multi multi = Multi.createFrom().items("a", "b", "c"); + multi.invoke(item -> System.out.println("Received item " + item)); + // + + // + multi.call(item -> executeAnAsyncAction(item)); + // + + await().until(() -> out.get().contains("We are subscribed!")); + await().until(() -> out.get().contains("Downstream requested")); + await().until(() -> out.get().contains("Received item c")); + await().until(() -> out.get().contains("Completed")); + } + + private Uni executeAnAsyncAction(String item) { + return Uni.createFrom().nullItem(); + } +} diff --git a/content/docs/smallrye-mutiny/main/_examples/java/guides/PlugTest.java b/content/docs/smallrye-mutiny/main/_examples/java/guides/PlugTest.java new file mode 100644 index 000000000..c696891a3 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/_examples/java/guides/PlugTest.java @@ -0,0 +1,58 @@ +package guides; + +import guides.extension.SystemOut; +import guides.extension.SystemOutCaptureExtension; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.operators.multi.AbstractMultiOperator; +import io.smallrye.mutiny.operators.multi.MultiOperatorProcessor; +import io.smallrye.mutiny.subscription.MultiSubscriber; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.util.concurrent.ThreadLocalRandom; + +import static org.assertj.core.api.Assertions.assertThat; + +@SuppressWarnings("InnerClassMayBeStatic") +@ExtendWith(SystemOutCaptureExtension.class) +public class PlugTest { + + @Test + public void plug(SystemOut out) { + // tag::plug[] + Multi.createFrom() + .range(1, 101) + .plug(RandomDrop::new) + .subscribe().with(System.out::println); + // end::plug[] + + assertThat(out.get()).isNotBlank(); + } + + // tag::custom-operator[] + public class RandomDrop extends AbstractMultiOperator { + public RandomDrop(Multi upstream) { + super(upstream); + } + + @Override + public void subscribe(MultiSubscriber downstream) { + upstream.subscribe().withSubscriber(new DropProcessor(downstream)); + } + + private class DropProcessor extends MultiOperatorProcessor { + DropProcessor(MultiSubscriber downstream) { + super(downstream); + } + + @Override + public void onItem(T item) { + if (ThreadLocalRandom.current().nextBoolean()) { + super.onItem(item); + } + } + } + } + // end::custom-operator[] +} diff --git a/content/docs/smallrye-mutiny/main/_examples/java/guides/RxTest.java b/content/docs/smallrye-mutiny/main/_examples/java/guides/RxTest.java new file mode 100644 index 000000000..ba439fe5c --- /dev/null +++ b/content/docs/smallrye-mutiny/main/_examples/java/guides/RxTest.java @@ -0,0 +1,111 @@ +package guides; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class RxTest { + + @Test + public void rx() { + Multi multi = Multi.createFrom().range(1, 3); + Uni uni = Uni.createFrom().item(1); + // tag::rx[] + + int result = uni + .map(i -> i + 1) + .await().indefinitely(); + + int result2 = uni + .flatMap(i -> Uni.createFrom().item(i + 1)) + .await().indefinitely(); + + List list = multi + .map(i -> i + 1) + .collect().asList() + .await().indefinitely(); + + List list2 = multi + .flatMap(i -> Multi.createFrom().items(i, i)) + .collect().asList() + .await().indefinitely(); + + List list3 = multi + .concatMap(i -> Multi.createFrom().items(i, i)) + .collect().asList() + .await().indefinitely(); + + // end::rx[] + assertThat(result).isEqualTo(2); + assertThat(result2).isEqualTo(2); + assertThat(list).containsExactly(2, 3); + assertThat(list2).containsExactly(1, 1, 2, 2); + assertThat(list3).containsExactly(1, 1, 2, 2); + } + + @Test + public void mutiny() { + Multi multi = Multi.createFrom().range(1, 3); + Uni uni = Uni.createFrom().item(1); + // tag::mutiny[] + + int result = uni + .onItem().transform(i -> i + 1) + .await().indefinitely(); + + int result2 = uni + .onItem().transformToUni(i -> Uni.createFrom().item(i + 1)) + .await().indefinitely(); + + // Shortcut for .onItem().transformToUni + int result3 = uni + .chain(i -> Uni.createFrom().item(i + 1)) + .await().indefinitely(); + + List list = multi + .onItem().transform(i -> i + 1) + .collect().asList() + .await().indefinitely(); + + List list2 = multi + .onItem().transformToMultiAndMerge(i -> Multi.createFrom().items(i, i)) + .collect().asList() + .await().indefinitely(); + + // Equivalent to transformToMultiAndMerge but let you configure the flattening process, + // failure management, concurrency... + List list3 = multi + .onItem().transformToMulti(i -> Multi.createFrom().items(i, i)).merge() + .collect().asList() + .await().indefinitely(); + + List list4 = multi + .onItem().transformToMultiAndConcatenate(i -> Multi.createFrom().items(i, i)) + .collect().asList() + .await().indefinitely(); + + // Equivalent to transformToMultiAndConcatenate but let you configure the flattening process, + // failure management... + List list5 = multi + .onItem().transformToMulti(i -> Multi.createFrom().items(i, i)).concatenate() + .collect().asList() + .await().indefinitely(); + + + // end::mutiny[] + assertThat(result).isEqualTo(2); + assertThat(result2).isEqualTo(2); + assertThat(result3).isEqualTo(2); + assertThat(list).containsExactly(2, 3); + assertThat(list2).containsExactly(1, 1, 2, 2); + assertThat(list3).containsExactly(1, 1, 2, 2); + assertThat(list4).containsExactly(1, 1, 2, 2); + assertThat(list5).containsExactly(1, 1, 2, 2); + } + + +} diff --git a/content/docs/smallrye-mutiny/main/_examples/java/guides/ShortcutsTest.java b/content/docs/smallrye-mutiny/main/_examples/java/guides/ShortcutsTest.java new file mode 100644 index 000000000..1d08c9982 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/_examples/java/guides/ShortcutsTest.java @@ -0,0 +1,29 @@ +package guides; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import org.junit.jupiter.api.Test; + +public class ShortcutsTest { + + + @Test + public void test() { + + // tag::invoke[] + Multi multi = Multi.createFrom().items("a", "b", "c"); + multi.invoke(item -> System.out.println("Received item " + item)); + // end::invoke[] + + // tag::call[] + multi.call(item -> executeAnAsyncAction(item)); + // end::call[] + + multi.subscribe().with(x -> {}); + } + + private Uni executeAnAsyncAction(String item) { + return Uni.createFrom().nullItem(); + } + +} diff --git a/content/docs/smallrye-mutiny/main/_examples/java/guides/SpiesTest.java b/content/docs/smallrye-mutiny/main/_examples/java/guides/SpiesTest.java new file mode 100644 index 000000000..bc2c297dc --- /dev/null +++ b/content/docs/smallrye-mutiny/main/_examples/java/guides/SpiesTest.java @@ -0,0 +1,40 @@ +package guides; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.helpers.spies.MultiGlobalSpy; +import io.smallrye.mutiny.helpers.spies.MultiOnCompletionSpy; +import io.smallrye.mutiny.helpers.spies.MultiOnRequestSpy; +import io.smallrye.mutiny.helpers.spies.Spy; +import org.junit.jupiter.api.Test; + +public class SpiesTest { + + @Test + public void trackItems() { + // tag::selected[] + Multi multi = Multi.createFrom().items(1, 2, 3); + MultiOnRequestSpy requestSpy = Spy.onRequest(multi); + MultiOnCompletionSpy completionSpy = Spy.onCompletion(requestSpy); + + completionSpy.subscribe().with(System.out::println); + + System.out.println("Number of requests: " + requestSpy.requestedCount()); + System.out.println("Completed? " + completionSpy.invoked()); + // end::selected[] + } + + @Test + public void trackEverything() { + // tag::global[] + Multi multi = Multi.createFrom().items(1, 2, 3); + MultiGlobalSpy spy = Spy.globally(multi); + + spy.subscribe().with(System.out::println); + + System.out.println("Number of requests: " + spy.onRequestSpy().requestedCount()); + System.out.println("Cancelled? " + spy.onCancellationSpy().isCancelled()); + System.out.println("Failure? " + spy.onFailureSpy().lastFailure()); + System.out.println("Items: " + spy.onItemSpy().items()); + // end::global[] + } +} \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/main/_examples/java/guides/TestSubscribersTest.java b/content/docs/smallrye-mutiny/main/_examples/java/guides/TestSubscribersTest.java new file mode 100644 index 000000000..467bd8602 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/_examples/java/guides/TestSubscribersTest.java @@ -0,0 +1,131 @@ +package guides; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.helpers.test.AssertMulti; +import io.smallrye.mutiny.helpers.test.AssertSubscriber; +import io.smallrye.mutiny.helpers.test.UniAssertSubscriber; +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +public class TestSubscribersTest { + + @Test + void uni() { + // tag::uni[] + Uni uni = Uni.createFrom().item(63); + + UniAssertSubscriber subscriber = uni + .subscribe().withSubscriber(UniAssertSubscriber.create()); + + subscriber + .awaitItem() + .assertItem(63); + // end::uni[] + } + + @Test + void multi() { + // tag::multi[] + Multi multi = Multi.createFrom().range(1, 5) + .onItem().transform(n -> n * 10); + + AssertSubscriber subscriber = multi.subscribe().withSubscriber(AssertSubscriber.create(10)); + + subscriber + .awaitCompletion() + .assertItems(10, 20, 30, 40); + // end::multi[] + } + + @Test + void failing() { + // tag::failing[] + Multi multi = Multi.createFrom().failure(() -> new IOException("Boom")); + + AssertSubscriber subscriber = multi + .subscribe().withSubscriber(AssertSubscriber.create(10)); + + subscriber + .awaitFailure() + .assertFailedWith(IOException.class, "Boom"); + // end::failing[] + } + + @Test + void uniPredicate() { + // tag::uni-predicate[] + Uni uni = Uni.createFrom().item(63); + + UniAssertSubscriber subscriber = uni + .subscribe().withSubscriber(UniAssertSubscriber.create()); + + subscriber + .awaitItem() + .assertItem(i -> i > 0, "positive number") + .inspectItem(item -> { + // use any assertion library here + assert item == 63; + }); + // end::uni-predicate[] + } + + @Test + void multiPredicate() { + // tag::multi-predicate[] + Multi multi = Multi.createFrom().range(1, 5) + .onItem().transform(n -> n * 10); + + AssertSubscriber subscriber = multi + .subscribe().withSubscriber(AssertSubscriber.create(10)); + + subscriber + .awaitCompletion() + .assertLastItem(i -> i == 40, "last item is 40") + .assertItemCount(4) + .inspectItems(items -> { + // use any assertion library here + assert items.size() == 4; + }); + // end::multi-predicate[] + } + + @Test + void assertMulti() { + // tag::assert-multi[] + Multi multi = Multi.createFrom().range(1, 6); + + AssertMulti.create(multi) + .expectNext(1, 2, 3) + .expectNextMatches(i -> i > 3, "greater than 3") + .expectNext(5) + .expectComplete() + .verify(); + // end::assert-multi[] + } + + @Test + void assertMultiFailure() { + // tag::assert-multi-failure[] + Multi multi = Multi.createFrom().failure(new IOException("boom")); + + AssertMulti.create(multi) + .expectFailure(IOException.class, "boom") + .verify(); + // end::assert-multi-failure[] + } + + @Test + void assertMultiDemand() { + // tag::assert-multi-demand[] + Multi multi = Multi.createFrom().range(1, 4); + + AssertMulti.create(multi) + .thenRequest(10) + .expectNext(1, 2, 3) + .expectComplete() + .verify(); + // end::assert-multi-demand[] + } +} diff --git a/content/docs/smallrye-mutiny/main/_examples/java/guides/UncheckedTest.java b/content/docs/smallrye-mutiny/main/_examples/java/guides/UncheckedTest.java new file mode 100644 index 000000000..1d15e5bd4 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/_examples/java/guides/UncheckedTest.java @@ -0,0 +1,69 @@ +package guides; + +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.unchecked.Unchecked; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.io.UncheckedIOException; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@SuppressWarnings("Convert2MethodRef") +public class UncheckedTest { + + @Test + public void rethrow() { + Uni item = Uni.createFrom().item(1); + + // tag::rethrow[] + Uni uni = item.onItem().transform(i -> { + try { + return methodThrowingIoException(i); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + // end::rethrow[] + + assertThatThrownBy(() -> uni.await().indefinitely()) + .isInstanceOf(RuntimeException.class).hasCauseInstanceOf(IOException.class); + + } + + @Test + public void transformExample() { + Uni item = Uni.createFrom().item(1); + + // tag::transform[] + Uni uni = item.onItem().transform(Unchecked.function(i -> { + // Can throw checked exception + return methodThrowingIoException(i); + })); + // end::transform[] + + assertThatThrownBy(() -> uni.await().indefinitely()) + .isInstanceOf(RuntimeException.class).hasCauseInstanceOf(IOException.class); + + } + + @Test + public void invokeExample() { + Uni item = Uni.createFrom().item(1); + + // tag::invoke[] + Uni uni = item.onItem().invoke(Unchecked.consumer(i -> { + // Can throw checked exception + throw new IOException("boom"); + })); + // end::invoke[] + + assertThatThrownBy(() -> uni.await().indefinitely()) + .isInstanceOf(RuntimeException.class).hasCauseInstanceOf(IOException.class); + + } + + private int methodThrowingIoException(Integer ignored) throws IOException { + throw new IOException("boom"); + } +} diff --git a/content/docs/smallrye-mutiny/main/_examples/java/guides/UniMultiComparisonTest.java b/content/docs/smallrye-mutiny/main/_examples/java/guides/UniMultiComparisonTest.java new file mode 100644 index 000000000..d1df9017c --- /dev/null +++ b/content/docs/smallrye-mutiny/main/_examples/java/guides/UniMultiComparisonTest.java @@ -0,0 +1,63 @@ +package guides; + +import guides.extension.SystemOut; +import guides.extension.SystemOutCaptureExtension; +import org.junit.jupiter.api.Test; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import org.junit.jupiter.api.extension.ExtendWith; + +import static org.awaitility.Awaitility.await; + +@ExtendWith(SystemOutCaptureExtension.class) +public class UniMultiComparisonTest { + + @Test + public void comparison(SystemOut out) { + //tag::code[] + Multi.createFrom().items("a", "b", "c") + .onItem().transform(String::toUpperCase) + .subscribe().with( + item -> System.out.println("Received: " + item), + failure -> System.out.println("Failed with " + failure) + ); + + Uni.createFrom().item("a") + .onItem().transform(String::toUpperCase) + .subscribe().with( + item -> System.out.println("Received: " + item), + failure -> System.out.println("Failed with " + failure) + ); + //end::code[] + + await().until(() -> out.get().contains("Received: C")); + } + + @Test + public void conversion(SystemOut out) { + // + Multi.createFrom().items("a", "b", "c") + .onItem().transform(String::toUpperCase) + // Convert the multi to uni + // It only emits the first item ("a") + .toUni() + .subscribe().with( + item -> System.out.println("Received: " + item), + failure -> System.out.println("Failed with " + failure)); + + Uni.createFrom().item("a") + .onItem().transform(String::toUpperCase) + // Convert the uni to a multi, + // the completion event will be fired after "a": + .toMulti() + .subscribe().with( + item -> System.out.println("Received: " + item), + failure -> System.out.println("Failed with " + failure) + ); + + // + + await().until(() -> out.get().contains("Received: A")); + } +} diff --git a/content/docs/smallrye-mutiny/main/_examples/java/guides/UniNullTest.java b/content/docs/smallrye-mutiny/main/_examples/java/guides/UniNullTest.java new file mode 100644 index 000000000..53ddbeb02 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/_examples/java/guides/UniNullTest.java @@ -0,0 +1,48 @@ +package guides; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class UniNullTest { + + @Test + public void uni() { + Uni uni = Uni.createFrom().item(() -> null); + // tag::code[] + uni.onItem().ifNull().continueWith("hello"); + uni.onItem().ifNull().switchTo(() -> Uni.createFrom().item("hello")); + uni.onItem().ifNull().failWith(() -> new Exception("Boom!")); + // end::code[] + + assertThat(uni.onItem().ifNull().continueWith("hello").await().indefinitely()).isEqualTo("hello"); + } + + @Test + public void uniNotNull() { + Uni uni = Uni.createFrom().item(() -> null); + // tag::code-not-null[] + uni + .onItem().ifNotNull().transform(String::toUpperCase) + .onItem().ifNull().continueWith("yolo!"); + // end::code-not-null[] + + String r = uni + .onItem().ifNotNull().transform(String::toUpperCase) + .onItem().ifNull().continueWith("yolo!") + .await().indefinitely(); + assertThat(r).isEqualTo("yolo!"); + } + + @Test + public void accumulate() { + Multi multi = Multi.createFrom().range(1, 3); + // + Multi added = multi.onItem().scan(() -> 0, (item, acc) -> acc + item); + // + assertThat(added.subscribe().asIterable()).containsExactly(0, 1, 3); + + } +} diff --git a/content/docs/smallrye-mutiny/main/_examples/java/guides/UniTimeoutTest.java b/content/docs/smallrye-mutiny/main/_examples/java/guides/UniTimeoutTest.java new file mode 100644 index 000000000..3dcd602ca --- /dev/null +++ b/content/docs/smallrye-mutiny/main/_examples/java/guides/UniTimeoutTest.java @@ -0,0 +1,102 @@ +package guides; + +import io.smallrye.mutiny.TimeoutException; +import io.smallrye.mutiny.Uni; +import org.junit.jupiter.api.Test; + +import javax.naming.ServiceUnavailableException; +import java.time.Duration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@SuppressWarnings("Convert2MethodRef") +public class UniTimeoutTest { + + @Test + public void test() { + Uni uni = Uni.createFrom().nothing(); + // tag::code[] + Uni uniWithTimeout = uni + .ifNoItem().after(Duration.ofMillis(100)) + .recoverWithItem("some fallback item"); + // end::code[] + String item = uniWithTimeout + .await().indefinitely(); + assertThat(item).isEqualTo("some fallback item"); + } + + @Test + public void testFail() { + Uni uni = Uni.createFrom().nothing(); + // tag::fail[] + Uni uniWithTimeout = uni + .ifNoItem().after(Duration.ofMillis(100)).fail(); + // end::fail[] + assertThatThrownBy(() -> + uniWithTimeout + .await().indefinitely() + ).isInstanceOf(TimeoutException.class); + } + + @Test + public void testFailRecover() { + Uni uni = Uni.createFrom().nothing(); + // tag::fail-recover[] + Uni uniWithTimeout = uni + .ifNoItem().after(Duration.ofMillis(100)).fail() + .onFailure(TimeoutException.class).recoverWithItem("we got a timeout"); + // end::fail-recover[] + String item = uniWithTimeout + .await().indefinitely(); + assertThat(item).isEqualTo("we got a timeout"); + } + + @Test + public void testFailWithSpecificException() { + Uni uni = Uni.createFrom().nothing(); + // tag::fail-with[] + Uni uniWithTimeout = uni + .ifNoItem().after(Duration.ofMillis(100)).failWith(() -> new ServiceUnavailableException()); + // end::fail-with[] + assertThatThrownBy(() -> + uniWithTimeout + .await().indefinitely() + ).isInstanceOf(ServiceUnavailableException.class); + } + + @Test + public void testFallback() { + Uni uni = Uni.createFrom().nothing(); + // tag::fallback[] + Uni uniWithTimeout = uni + .ifNoItem().after(Duration.ofMillis(100)).recoverWithItem(() -> "fallback"); + // end::fallback[] + String item = uniWithTimeout + .await().indefinitely(); + assertThat(item).isEqualTo("fallback"); + } + + @Test + public void testFallbackUni() { + Uni uni = Uni.createFrom().nothing(); + // tag::fallback-uni[] + Uni uniWithTimeout = uni + .ifNoItem().after(Duration.ofMillis(100)).recoverWithUni(() -> someFallbackUni()); + // end::fallback-uni[] + String item = uniWithTimeout + .await().indefinitely(); + assertThat(item).isEqualTo("fallback"); + } + + private Uni someFallbackUni() { + return Uni.createFrom().item("fallback"); + } + + + private static class ServiceUnavailableException extends RuntimeException { + + } + + +} diff --git a/content/docs/smallrye-mutiny/main/_examples/java/guides/infrastructure/DroppedExceptionTest.java b/content/docs/smallrye-mutiny/main/_examples/java/guides/infrastructure/DroppedExceptionTest.java new file mode 100644 index 000000000..980ecd9b7 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/_examples/java/guides/infrastructure/DroppedExceptionTest.java @@ -0,0 +1,54 @@ +package guides.infrastructure; + +import guides.extension.SystemOut; +import guides.extension.SystemOutCaptureExtension; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.infrastructure.Infrastructure; +import io.smallrye.mutiny.subscription.Cancellable; +import io.smallrye.mutiny.subscription.UniEmitter; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; + +@ExtendWith(SystemOutCaptureExtension.class) +public class DroppedExceptionTest { + + @Test + public void droppedExceptionTest(SystemOut out) { + // tag::override-handler[] + Infrastructure.setDroppedExceptionHandler(err -> + log(Level.SEVERE, "Mutiny dropped exception", err) + ); + // end::override-handler[] + + // tag::code[] + Cancellable cancellable = Uni.createFrom() + .emitter(this::emitter) + .onCancellation().call(() -> Uni.createFrom().failure(new IOException("boom"))) + .subscribe().with(this::onItem, this::onFailure); + + cancellable.cancel(); + // end::code[] + Assertions.assertThat(out.get()).contains("Mutiny dropped exception"); + } + + private void log(Level severe, String message, Throwable err) { + System.out.println(severe.getName() + ":" + message + " " + err.getMessage()); + } + + private void onFailure(Throwable t) { + // Nothing, the snippet is for pure documentation purposes + } + + private void onItem(Object o) { + // Nothing, the snippet is for pure documentation purposes + } + + private void emitter(UniEmitter uniEmitter) { + // Nothing, the snippet is for pure documentation purposes + } +} diff --git a/content/docs/smallrye-mutiny/main/_examples/java/guides/infrastructure/InfrastructureTest.java b/content/docs/smallrye-mutiny/main/_examples/java/guides/infrastructure/InfrastructureTest.java new file mode 100644 index 000000000..61da6e036 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/_examples/java/guides/infrastructure/InfrastructureTest.java @@ -0,0 +1,39 @@ +package guides.infrastructure; + +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.infrastructure.Infrastructure; +import io.smallrye.mutiny.infrastructure.MutinyScheduler; +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.util.concurrent.Executor; + +import static org.assertj.core.api.Assertions.assertThat; + +public class InfrastructureTest { + + @Test + public void test() { + // tag::infra[] + Uni uni1 = Uni.createFrom().item(1) + .emitOn(Infrastructure.getDefaultExecutor()); + + Uni uni2 = Uni.createFrom().item(2) + .onItem().delayIt() + .onExecutor(Infrastructure.getDefaultWorkerPool()) + .by(Duration.ofMillis(10)); + // end::infra[] + + assertThat(uni1.await().indefinitely()).isEqualTo(1); + assertThat(uni2.await().indefinitely()).isEqualTo(2); + + + Executor executor = Runnable::run; + // tag::set-infra[] + Infrastructure.setDefaultExecutor(executor); + // end::set-infra[] + + assertThat(Infrastructure.getDefaultExecutor()).isEqualTo(executor); + assertThat(Infrastructure.getDefaultWorkerPool()).isInstanceOf(MutinyScheduler.class); + } +} diff --git a/content/docs/smallrye-mutiny/main/_examples/java/guides/infrastructure/OperatorLoggingTest.java b/content/docs/smallrye-mutiny/main/_examples/java/guides/infrastructure/OperatorLoggingTest.java new file mode 100644 index 000000000..32746b70a --- /dev/null +++ b/content/docs/smallrye-mutiny/main/_examples/java/guides/infrastructure/OperatorLoggingTest.java @@ -0,0 +1,36 @@ +package guides.infrastructure; + +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.infrastructure.Infrastructure; + +public class OperatorLoggingTest { + + @Test + public void test() { + // tag::set-logger[] + Infrastructure.setOperatorLogger((id, event, value, err) -> { + Logger logger = LoggerFactory.getLogger(id); + if (err != null) { + logger.info(event + "(" + err.getClass() + "(" + err.getMessage() + "))"); + } else { + if (value != null) { + logger.info(event + "(" + value + ")"); + } else { + logger.info(event + "()"); + } + } + }); + // end::set-logger[] + + // tag::log[] + Multi.createFrom().items(1, 2, 3) + .onItem().transform(n -> n * 10) + .log() + .subscribe().with(item -> System.out.println(">>> " + item)); + // end::log[] + } +} diff --git a/content/docs/smallrye-mutiny/main/_examples/java/guides/integration/ImperativeToReactiveTest.java b/content/docs/smallrye-mutiny/main/_examples/java/guides/integration/ImperativeToReactiveTest.java new file mode 100644 index 000000000..cc4484456 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/_examples/java/guides/integration/ImperativeToReactiveTest.java @@ -0,0 +1,60 @@ +package guides.integration; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.infrastructure.Infrastructure; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ImperativeToReactiveTest { + + + @Test + public void uniRunSubscriptionOn() { + // tag::uni-runSubscriptionOn[] + Uni uni = Uni.createFrom() + .item(this::invokeRemoteServiceUsingBlockingIO) + .runSubscriptionOn(Infrastructure.getDefaultWorkerPool()); + // end::uni-runSubscriptionOn[] + + String res = uni.await().indefinitely(); + assertThat(res).isEqualTo("Hello"); + } + + @Test + public void multiEmitOn() { + // tag::multi-emitOn[] + Multi multi = Multi.createFrom().items("john", "jack", "sue") + .emitOn(Infrastructure.getDefaultWorkerPool()) + .onItem().transform(this::invokeRemoteServiceUsingBlockingIO); + // end::multi-emitOn[] + + List strings = multi.subscribe().asStream().collect(Collectors.toList()); + assertThat(strings).containsExactly("JOHN", "JACK", "SUE"); + } + + public String invokeRemoteServiceUsingBlockingIO() { + assertThat(Thread.currentThread().getName()).doesNotContain("main"); + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + return "Hello"; + } + + public String invokeRemoteServiceUsingBlockingIO(String s) { + assertThat(Thread.currentThread().getName()).doesNotContain("main"); + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + return s.toUpperCase(); + } + +} diff --git a/content/docs/smallrye-mutiny/main/_examples/java/guides/integration/ReactiveToImperativeTest.java b/content/docs/smallrye-mutiny/main/_examples/java/guides/integration/ReactiveToImperativeTest.java new file mode 100644 index 000000000..d4cb2397e --- /dev/null +++ b/content/docs/smallrye-mutiny/main/_examples/java/guides/integration/ReactiveToImperativeTest.java @@ -0,0 +1,57 @@ +package guides.integration; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; + +@SuppressWarnings("unchecked") +public class ReactiveToImperativeTest { + + @Test + public void testAwait() { + Uni uni = (Uni) Uni.createFrom().item(() -> 1); + + // tag::await[] + T t = uni.await().indefinitely(); + // end::await[] + + assertThat(t).isEqualTo(1); + } + + @Test + public void testAwaitAtMost() { + Uni uni = (Uni) Uni.createFrom().item(() -> 1); + + // tag::atMost[] + T t = uni.await().atMost(Duration.ofSeconds(1)); + // end::atMost[] + + assertThat(t).isEqualTo(1); + } + + @Test + public void test() { + Multi multi = (Multi) Multi.createFrom().items(1, 2, 3); + + // tag::iterable[] + Iterable iterable = multi.subscribe().asIterable(); + for (T item : iterable) { + doSomethingWithItem(item); + } + // end::iterable[] + + // tag::stream[] + Stream stream = multi.subscribe().asStream(); + stream.forEach(this::doSomethingWithItem); + // end::stream[] + } + + private void doSomethingWithItem(T item) { + assertThat(item).isIn(1, 2, 3); + } +} diff --git a/content/docs/smallrye-mutiny/main/_examples/java/guides/integration/ReactorTest.java b/content/docs/smallrye-mutiny/main/_examples/java/guides/integration/ReactorTest.java new file mode 100644 index 000000000..83f629379 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/_examples/java/guides/integration/ReactorTest.java @@ -0,0 +1,158 @@ +package guides.integration; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.converters.multi.MultiReactorConverters; +import io.smallrye.mutiny.converters.uni.UniReactorConverters; +import mutiny.zero.flow.adapters.AdaptersToFlow; +import mutiny.zero.flow.adapters.AdaptersToReactiveStreams; +import org.junit.jupiter.api.Test; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.List; +import java.util.concurrent.Flow; + +import static org.assertj.core.api.Assertions.assertThat; + +@SuppressWarnings("unchecked") +public class ReactorTest { + + @Test + public void testMultiCreation() { + Flux flux = getFlux(); + Mono mono = getMono(); + + // tag::reactor-multi-create[] + Flow.Publisher fluxAsPublisher = AdaptersToFlow.publisher(flux); + Multi multiFromFlux = Multi.createFrom().publisher(fluxAsPublisher); + + Flow.Publisher monoAsPublisher = AdaptersToFlow.publisher(mono); + Multi multiFromMono = Multi.createFrom().publisher(monoAsPublisher); + // end::reactor-multi-create[] + + List list = multiFromFlux + .onItem().transform(Object::toString) + .collect().asList().await().indefinitely(); + assertThat(list).containsExactly("a", "b", "c"); + + list = multiFromMono + .onItem().transform(Object::toString) + .collect().asList().await().indefinitely(); + assertThat(list).containsExactly("a"); + + } + + @Test + public void testUniCreation() { + Flux flux = getFlux(); + Mono mono = getMono(); + + // tag::reactor-uni-create[] + Flow.Publisher fluxAsPublisher = AdaptersToFlow.publisher(flux); + Uni uniFromFlux = Uni.createFrom().publisher(fluxAsPublisher); + + Flow.Publisher monoAsPublisher = AdaptersToFlow.publisher(mono); + Uni uniFromMono = Uni.createFrom().publisher(monoAsPublisher); + // end::reactor-uni-create[] + + String s = uniFromFlux + .onItem().transform(Object::toString) + .await().indefinitely(); + assertThat(s).isEqualTo("a"); + + s = uniFromMono + .onItem().transform(Object::toString) + .await().indefinitely(); + assertThat(s).isEqualTo("a"); + } + + @Test + public void testCreationFromMulti() { + Multi multi = getMulti(); + + // tag::reactor-create-multi[] + Publisher multiAsLegacyPublisher = AdaptersToReactiveStreams.publisher(multi); + + Flux fluxFromMulti = Flux.from(multiAsLegacyPublisher); + Mono monoFromMulti = Mono.from(multiAsLegacyPublisher); + // end::reactor-create-multi[] + + List list = fluxFromMulti + .map(Object::toString) + .collectList() + .block(); + assertThat(list).containsExactly("a", "b", "c"); + + String s = monoFromMulti + .map(Object::toString) + .block(); + assertThat(s).isEqualTo("a"); + } + + @Test + public void testCreationFromUni() { + Uni uni = getUni(); + + // tag::reactor-create-uni[] + Flux fluxFromUni = uni.convert().with(UniReactorConverters.toFlux()); + Mono monoFromUni = uni.convert().with(UniReactorConverters.toMono()); + // end::reactor-create-uni[] + + List list = fluxFromUni + .map(Object::toString) + .collectList() + .block(); + assertThat(list).containsExactly("a"); + + String s = monoFromUni + .map(Object::toString) + .block(); + assertThat(s).isEqualTo("a"); + } + + private Flux getFlux() { + return Flux.just("a", "b", "c") + .map(s -> (T) s); + } + + private Mono getMono() { + return Mono.just("a") + .map(s -> (T) s); + } + + private Multi getMulti() { + return Multi.createFrom().items("a", "b", "c") + .map(s -> (T) s); + } + + private Uni getUni() { + return Uni.createFrom().item("a") + .map(s -> (T) s); + } + + @Test + public void uniExportToReactor() { + Uni uni = Uni.createFrom().item("hello"); + // tag::uni-export[] + Mono mono = uni.convert().with(UniReactorConverters.toMono()); + Flux flux = uni.convert().with(UniReactorConverters.toFlux()); + // end::uni-export[] + + assertThat(mono.block()).isEqualTo("hello"); + assertThat(flux.blockFirst()).isEqualTo("hello"); + } + + @Test + public void multiExportToReactor() { + Multi multi = Multi.createFrom().items("hello", "bonjour"); + // tag::multi-export[] + Mono mono = multi.convert().with(MultiReactorConverters.toMono()); + Flux flux = multi.convert().with(MultiReactorConverters.toFlux()); + // end::multi-export[] + + assertThat(mono.block()).isEqualTo("hello"); + assertThat(flux.toIterable()).containsExactly("hello", "bonjour"); + } +} diff --git a/content/docs/smallrye-mutiny/main/_examples/java/guides/integration/RxJavaTest.java b/content/docs/smallrye-mutiny/main/_examples/java/guides/integration/RxJavaTest.java new file mode 100644 index 000000000..dd1ae117c --- /dev/null +++ b/content/docs/smallrye-mutiny/main/_examples/java/guides/integration/RxJavaTest.java @@ -0,0 +1,277 @@ +package guides.integration; + +import io.reactivex.rxjava3.core.*; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.converters.multi.MultiRx3Converters; +import io.smallrye.mutiny.converters.uni.UniRx3Converters; +import mutiny.zero.flow.adapters.AdaptersToFlow; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Optional; +import java.util.concurrent.Flow; + +import static org.assertj.core.api.Assertions.assertThat; + +@SuppressWarnings("unchecked") +public class RxJavaTest { + + @Test + public void testMultiCreation() { + Observable observable = getObservable(); + Flowable flowable = getFlowable(); + + // tag::rx-multi-create-observable[] + Multi multiFromObservable = Multi.createFrom() + .converter(MultiRx3Converters.fromObservable(), observable); + // end::rx-multi-create-observable[] + + // tag::rx-multi-create-flowable[] + Flow.Publisher flowableAsPublisher = AdaptersToFlow.publisher(flowable); + + Multi multiFromFlowable = Multi.createFrom().publisher(flowableAsPublisher); + // end::rx-multi-create-flowable[] + + List list = multiFromObservable + .onItem().transform(Object::toString) + .collect().asList().await().indefinitely(); + assertThat(list).containsExactly("a", "b", "c"); + + list = multiFromFlowable + .onItem().transform(Object::toString) + .collect().asList().await().indefinitely(); + assertThat(list).containsExactly("a", "b", "c"); + + Completable completable = getCompletable(); + Single single = getSingle(); + Maybe maybe = getMaybe(); + Maybe emptyMaybe = getEmptyMaybe(); + + // tag::rx-multi-create-single[] + Multi multiFromCompletable = Multi.createFrom() + .converter(MultiRx3Converters.fromCompletable(), completable); + Multi multiFromSingle = Multi.createFrom() + .converter(MultiRx3Converters.fromSingle(), single); + Multi multiFromMaybe = Multi.createFrom() + .converter(MultiRx3Converters.fromMaybe(), maybe); + Multi multiFromEmptyMaybe = Multi.createFrom() + .converter(MultiRx3Converters.fromMaybe(), emptyMaybe); + // end::rx-multi-create-single[] + + list = multiFromCompletable + .onItem().transform(Object::toString) + .collect().asList().await().indefinitely(); + assertThat(list).isEmpty(); + + list = multiFromSingle + .onItem().transform(Object::toString) + .collect().asList().await().indefinitely(); + assertThat(list).containsExactly("a"); + + list = multiFromMaybe + .onItem().transform(Object::toString) + .collect().asList().await().indefinitely(); + assertThat(list).containsExactly("a"); + + list = multiFromEmptyMaybe + .onItem().transform(Object::toString) + .collect().asList().await().indefinitely(); + assertThat(list).isEmpty(); + } + + @Test + public void testUniCreation() { + Observable observable = getObservable(); + Flowable flowable = getFlowable(); + + // tag::rx-uni-create-observable[] + Uni uniFromObservable = Uni.createFrom().converter( + UniRx3Converters.fromObservable(), observable); + // end::rx-uni-create-observable[] + + // tag::rx-uni-create-flowable[] + Flow.Publisher flowableAsPublisher = AdaptersToFlow.publisher(flowable); + + Uni uniFromFlowable = Uni.createFrom().publisher(flowableAsPublisher); + // end::rx-uni-create-flowable[] + + String s = uniFromFlowable + .onItem().transform(Object::toString) + .await().indefinitely(); + assertThat(s).isEqualTo("a"); + + s = uniFromObservable + .onItem().transform(Object::toString) + .await().indefinitely(); + assertThat(s).isEqualTo("a"); + + Completable completable = getCompletable(); + Single single = getSingle(); + Maybe maybe = getMaybe(); + Maybe emptyMaybe = getEmptyMaybe(); + + // tag::rx-uni-create-single[] + Uni multiFromCompletable = Uni.createFrom() + .converter(UniRx3Converters.fromCompletable(), completable); + Uni multiFromSingle = Uni.createFrom() + .converter(UniRx3Converters.fromSingle(), single); + Uni multiFromMaybe = Uni.createFrom() + .converter(UniRx3Converters.fromMaybe(), maybe); + Uni multiFromEmptyMaybe = Uni.createFrom() + .converter(UniRx3Converters.fromMaybe(), emptyMaybe); + // end::rx-uni-create-single[] + + Void v = multiFromCompletable + .await().indefinitely(); + assertThat(v).isNull(); + + s = multiFromSingle + .onItem().transform(Object::toString) + .await().indefinitely(); + assertThat(s).isEqualTo("a"); + + s = multiFromMaybe + .onItem().transform(Object::toString) + .await().indefinitely(); + assertThat(s).isEqualTo("a"); + + s = multiFromEmptyMaybe + .onItem().castTo(String.class) + .await().indefinitely(); + assertThat(s).isNull(); + } + + @Test + public void testCreatingRxFromMulti() { + Multi multi = getMulti(); + + // tag::create-rx-from-multi[] + Completable completable = multi.convert() + .with(MultiRx3Converters.toCompletable()); + Single> single = multi.convert() + .with(MultiRx3Converters.toSingle()); + Single single2 = multi.convert() + .with(MultiRx3Converters + .toSingle().onEmptyThrow(() -> new Exception("D'oh!"))); + Maybe maybe = multi.convert() + .with(MultiRx3Converters.toMaybe()); + Observable observable = multi.convert() + .with(MultiRx3Converters.toObservable()); + Flowable flowable = multi.convert() + .with(MultiRx3Converters.toFlowable()); + // end::create-rx-from-multi[] + + completable.blockingAwait(); + assertThat(single.blockingGet()).contains((T) "a"); + assertThat(single2.blockingGet()).isEqualTo("a"); + assertThat(maybe.blockingGet()).isEqualTo("a"); + assertThat(observable.blockingIterable()).containsExactly((T) "a", (T) "b", (T) "c"); + assertThat(flowable.blockingIterable()).containsExactly((T) "a", (T) "b", (T) "c"); + } + + @Test + public void testCreatingRxFromUni() { + Uni uni = getUni(); + + // tag::create-rx-from-uni[] + Completable completable = uni.convert().with(UniRx3Converters.toCompletable()); + Single> single = uni.convert().with(UniRx3Converters.toSingle()); + Single single2 = uni.convert().with(UniRx3Converters.toSingle().failOnNull()); + Maybe maybe = uni.convert().with(UniRx3Converters.toMaybe()); + Observable observable = uni.convert().with(UniRx3Converters.toObservable()); + Flowable flowable = uni.convert().with(UniRx3Converters.toFlowable()); + // end::create-rx-from-uni[] + + completable.blockingAwait(); + assertThat(single.blockingGet()).contains((T) "a"); + assertThat(single2.blockingGet()).isEqualTo("a"); + assertThat(maybe.blockingGet()).isEqualTo("a"); + assertThat(observable.blockingIterable()).containsExactly((T) "a"); + assertThat(flowable.blockingIterable()).containsExactly((T) "a"); + } + + @Test + public void uniExportToRx() { + Uni uni = Uni.createFrom().item("hello"); + // + Completable completable = uni.convert().with(UniRx3Converters.toCompletable()); + Single> single = uni.convert().with(UniRx3Converters.toSingle()); + Single single2 = uni.convert().with(UniRx3Converters.toSingle().failOnNull()); + Maybe maybe = uni.convert().with(UniRx3Converters.toMaybe()); + Observable observable = uni.convert().with(UniRx3Converters.toObservable()); + Flowable flowable = uni.convert().with(UniRx3Converters.toFlowable()); + // + + completable.test().assertComplete(); + single.test().assertValue(o -> o.isPresent() && o.get().equals("hello")); + single2.test().assertValue("hello"); + maybe.test().assertValue("hello"); + observable.test().assertValue("hello").assertComplete(); + flowable.test().assertValue("hello").assertComplete(); + } + + @Test + public void multiExportToRx() { + Multi multi = Multi.createFrom().items("hello", "bonjour"); + // + Completable completable = multi.convert() + .with(MultiRx3Converters.toCompletable()); + Single> single = multi.convert() + .with(MultiRx3Converters.toSingle()); + Single single2 = multi.convert() + .with(MultiRx3Converters.toSingle().onEmptyThrow(() -> new Exception("D'oh!"))); + Maybe maybe = multi.convert() + .with(MultiRx3Converters.toMaybe()); + Observable observable = multi.convert() + .with(MultiRx3Converters.toObservable()); + Flowable flowable = multi.convert() + .with(MultiRx3Converters.toFlowable()); + // + + completable.test().assertComplete(); + single.test().assertValue(o -> o.isPresent() && o.get().equals("hello")); + single2.test().assertValue("hello"); + maybe.test().assertValue("hello"); + observable.test().assertValues("hello", "bonjour").assertComplete(); + flowable.test().assertValues("hello", "bonjour").assertComplete(); + } + + private Observable getObservable() { + return Observable.just("a", "b", "c") + .map(s -> (T) s); + } + + private Flowable getFlowable() { + return Flowable.just("a", "b", "c") + .map(s -> (T) s); + } + + private Single getSingle() { + return Single.just("a") + .map(s -> (T) s); + } + + private Maybe getMaybe() { + return Maybe.just("a") + .map(s -> (T) s); + } + + private Maybe getEmptyMaybe() { + return Maybe.empty(); + } + + private Completable getCompletable() { + return Completable.complete(); + } + + private Multi getMulti() { + return Multi.createFrom().items("a", "b", "c") + .map(s -> (T) s); + } + + private Uni getUni() { + return Uni.createFrom().item("a") + .map(s -> (T) s); + } +} diff --git a/content/docs/smallrye-mutiny/main/_examples/java/guides/operators/BroadcastProcessorTest.java b/content/docs/smallrye-mutiny/main/_examples/java/guides/operators/BroadcastProcessorTest.java new file mode 100644 index 000000000..36172511c --- /dev/null +++ b/content/docs/smallrye-mutiny/main/_examples/java/guides/operators/BroadcastProcessorTest.java @@ -0,0 +1,35 @@ +package guides.operators; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.helpers.test.AssertSubscriber; +import io.smallrye.mutiny.operators.multi.processors.BroadcastProcessor; +import org.junit.jupiter.api.Test; + +public class BroadcastProcessorTest { + + @Test + public void test() { + // tag::code[] + BroadcastProcessor processor = BroadcastProcessor.create(); + Multi multi = processor + .onItem().transform(String::toUpperCase) + .onFailure().recoverWithItem("d'oh"); + + new Thread(() -> { + for (int i = 0; i < 1000; i++) { + processor.onNext(Integer.toString(i)); + } + processor.onComplete(); + }).start(); + + // Subscribers can subscribe at any time. + // They will only receive items emitted after their subscription. + // If the source is already terminated (by a completion or a failure signal) + // the subscriber receives this signal. + + // end::code[] + AssertSubscriber subscriber = AssertSubscriber.create(Long.MAX_VALUE); + multi.subscribe().withSubscriber(subscriber) + .awaitCompletion(); + } +} diff --git a/content/docs/smallrye-mutiny/main/_examples/java/guides/operators/CollectingItemsTest.java b/content/docs/smallrye-mutiny/main/_examples/java/guides/operators/CollectingItemsTest.java new file mode 100644 index 000000000..b48d62c53 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/_examples/java/guides/operators/CollectingItemsTest.java @@ -0,0 +1,99 @@ +package guides.operators; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.tuples.Tuple2; +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.util.*; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.*; + +@SuppressWarnings("Convert2MethodRef") +public class CollectingItemsTest { + + @Test + public void testList() { + // tag::list[] + Multi multi = getMulti(); + Uni> uni = multi.collect().asList(); + // end::list[] + + assertThat(uni.await().indefinitely()).containsExactly("a", "b", "c"); + + // tag::first[] + Uni first = multi.collect().first(); + Uni last = multi.collect().last(); + // end::first[] + + assertThat(first.await().indefinitely()).isEqualTo("a"); + assertThat(last.await().indefinitely()).isEqualTo("c"); + } + + @Test + public void testMap() { + // tag::map[] + Multi multi = getMulti(); + Uni> uni = + multi.collect() + .asMap(item -> getUniqueKeyForItem(item)); + // end::map[] + + assertThat(uni.await().indefinitely()).hasSize(3); + } + + @Test + public void testMultiMap() { + // tag::multimap[] + Multi multi = getMulti(); + Uni>> uni = + multi.collect() + .asMultiMap(item -> getKeyForItem(item)); + // end::multimap[] + + assertThat(uni.await().indefinitely()).hasSize(2); + } + + @Test + public void testCustomAccumulator() { + // tag::accumulator[] + Multi multi = getMulti(); + Uni uni = multi.collect() + .in(MyCollection::new, (col, item) -> col.add(item)); + // end::accumulator[] + + assertThat(uni.await().indefinitely()).hasSize(3); + + // tag::collector[] + Uni count = multi.collect() + .with(Collectors.counting()); + // end::collector[] + + assertThat(count.await().indefinitely()).isEqualTo(3); + } + + private String getKeyForItem(String item) { + if( item.equalsIgnoreCase("b")) { + return "b"; + } else { + return "a"; + } + } + + private String getUniqueKeyForItem(String item) { + return item; + } + + private Multi getMulti() { + return Multi.createFrom().items("a", "b", "c"); + } + + private static class MyCollection extends ArrayList { + + } +} diff --git a/content/docs/smallrye-mutiny/main/_examples/java/guides/operators/CombiningItemsTest.java b/content/docs/smallrye-mutiny/main/_examples/java/guides/operators/CombiningItemsTest.java new file mode 100644 index 000000000..1f02148fd --- /dev/null +++ b/content/docs/smallrye-mutiny/main/_examples/java/guides/operators/CombiningItemsTest.java @@ -0,0 +1,177 @@ +package guides.operators; + +import guides.extension.SystemOut; +import guides.extension.SystemOutCaptureExtension; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.tuples.Tuple2; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.time.Duration; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +@SuppressWarnings({ "unchecked", "Convert2MethodRef" }) +@ExtendWith(SystemOutCaptureExtension.class) +public class CombiningItemsTest { + + @Test + public void testWithUni(SystemOut out) throws InterruptedException { + // tag::invocations[] + Uni uniA = invokeHttpServiceA(); + Uni uniB = invokeHttpServiceB(); + // end::invocations[] + + // tag::combination[] + Uni> responses = Uni.combine() + .all().unis(uniA, uniB).asTuple(); + // end::combination[] + + // tag::subscription[] + Uni.combine().all().unis(uniA, uniB).asTuple() + .subscribe().with(tuple -> { + System.out.println("Response from A: " + tuple.getItem1()); + System.out.println("Response from B: " + tuple.getItem2()); + }); + // end::subscription[] + + await().until(() -> out.get().contains("Response from A: Response{content='A'}")); + await().until(() -> out.get().contains("Response from B: Response{content='B'}")); + + // tag::combined-with[] + Uni> uni = Uni.combine() + .all().unis(uniA, uniB).with( + listOfResponses -> { + Map map = new LinkedHashMap<>(); + map.put("A", (Response) listOfResponses.get(0)); + map.put("B", (Response) listOfResponses.get(1)); + return map; + } + ); + // end::combined-with[] + + assertThat(uni.await().indefinitely()).containsKeys("A", "B"); + + // + Uni> uni1 = Uni.combine() + .all().unis(uniA, uniB).withUni( + listOfResponses -> { + Map map = new LinkedHashMap<>(); + map.put("A", (Response) listOfResponses.get(0)); + map.put("B", (Response) listOfResponses.get(1)); + return Uni.createFrom().item(map); + } + ); + // + + assertThat(uni1.await().indefinitely()).containsKeys("A", "B"); + + + Thread.sleep(100); + } + + @Test + public void testWithMulti() throws InterruptedException { + Multi multiA = getMultiA(); + Multi multiB = getMultiB(); + + // tag::combine-multi[] + Multi> combined = Multi.createBy().combining() + .streams(multiA, multiB).asTuple(); + // end::combine-multi[] + + CountDownLatch latch = new CountDownLatch(1); + AtomicInteger i = new AtomicInteger(); + combined.subscribe().with( + tuple -> { + int v = i.incrementAndGet(); + assertThat(tuple.getItem1()).isEqualTo("a" + v); + assertThat(tuple.getItem2()).isEqualTo("b" + v); + }, + () -> latch.countDown() + ); + + assertThat(latch.await(10, TimeUnit.MILLISECONDS)).isTrue(); + + // tag::combine-multi-with[] + Multi.createBy().combining() + .streams(multiA, multiB).using(list -> combineItems(list)) + .subscribe().with(x -> { + // do something with the combined items + }); + // end::combine-multi-with[] + } + + @Test + public void testCombineLast() { + Multi multiA = getMultiA(); + Multi multiB = getMultiB(); + + // tag::combine-last[] + Multi> multi1 = Multi.createBy().combining() + .streams(multiA, multiB) + .latestItems().asTuple(); + + // or + + Multi multi2 = Multi.createBy().combining() + .streams(multiA, multiB) + .latestItems().using(list -> combineItems(list)); + // end::combine-last[] + + List> list = multi1.collect().asList().await().indefinitely(); + assertThat(list).hasSize(3); + assertThat(list.toString()).contains("a3", "b1", "b2", "b3").doesNotContain("a1", "a2"); + List strings = multi2.collect().asList().await().indefinitely(); + assertThat(strings).hasSize(3).containsExactly("a3b1", "a3b2", "a3b3"); + } + + private String combineItems(List list) { + return "" + list.get(0) + list.get(1); + } + + private Multi getMultiA() { + return Multi.createFrom().items("a1", "a2", "a3") + .onItem().transform(s -> (A) s); + } + + private Multi getMultiB() { + return Multi.createFrom().items("b1", "b2", "b3") + .onItem().transform(s -> (B) s); + } + + private Uni invokeHttpServiceA() { + return Uni.createFrom().item(new Response("A")) + .onItem().delayIt().by(Duration.ofMillis(10)); + } + + private Uni invokeHttpServiceB() { + return Uni.createFrom().item(new Response("B")) + .onItem().delayIt().by(Duration.ofMillis(30)); + } + + static class Response { + + private final String content; + + public Response(String content) { + this.content = content; + } + + @Override + public String toString() { + return "Response{" + + "content='" + content + '\'' + + '}'; + } + } + +} diff --git a/content/docs/smallrye-mutiny/main/_examples/java/guides/operators/ControlDemandTest.java b/content/docs/smallrye-mutiny/main/_examples/java/guides/operators/ControlDemandTest.java new file mode 100644 index 000000000..2ea2694e1 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/_examples/java/guides/operators/ControlDemandTest.java @@ -0,0 +1,93 @@ +package guides.operators; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.helpers.test.AssertSubscriber; +import io.smallrye.mutiny.infrastructure.Infrastructure; +import io.smallrye.mutiny.subscription.DemandPacer; +import io.smallrye.mutiny.subscription.FixedDemandPacer; +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; + +import static org.assertj.core.api.Assertions.assertThat; + +class ControlDemandTest { + + @Test + void pacing() { + // tag::pacing[] + FixedDemandPacer pacer = new FixedDemandPacer(25L, Duration.ofMillis(100L)); + + Multi multi = Multi.createFrom().range(0, 100) + .paceDemand().on(Infrastructure.getDefaultWorkerPool()).using(pacer); + // end::pacing[] + + AssertSubscriber sub = multi.subscribe().withSubscriber(AssertSubscriber.create()); + sub.awaitCompletion(); + assertThat(sub.getItems()).hasSize(100); + } + + @Test + void customPacer() { + // tag::custom-pacer[] + DemandPacer pacer = new DemandPacer() { + + @Override + public Request initial() { + return new Request(10L, Duration.ofMillis(100L)); + } + + @Override + public Request apply(Request previousRequest, long observedItemsCount) { + return new Request(previousRequest.demand() * 2, previousRequest.delay().plus(10, ChronoUnit.MILLIS)); + } + }; + // end::custom-pacer[] + } + + @Test + void capping() { + // tag::capConstant[] + AssertSubscriber sub = AssertSubscriber.create(); + + sub = Multi.createFrom().range(0, 100) + .capDemandsTo(50L) + .subscribe().withSubscriber(sub); + + // A first batch of 50 (capped), 25 remain outstanding + sub.request(75L).assertNotTerminated(); + assertThat(sub.getItems()).hasSize(50); + + // Second batch: 25 + 25 = 50 + sub.request(25L).assertCompleted(); + assertThat(sub.getItems()).hasSize(100); + // end::capConstant[] + } + + @Test + void cappingFunction() { + // tag::capFunction[] + AssertSubscriber sub = AssertSubscriber.create(); + + sub = Multi.createFrom().range(0, 100) + .capDemandsUsing(n -> { + if (n > 1) { + return (long) (((double) n) * 0.75d); + } else { + return n; + } + }) + .subscribe().withSubscriber(sub); + + sub.request(100L).assertNotTerminated(); + assertThat(sub.getItems()).hasSize(75); + + sub.request(1L).assertNotTerminated(); + assertThat(sub.getItems()).hasSize(94); + + sub.request(Long.MAX_VALUE).assertCompleted(); + assertThat(sub.getItems()).hasSize(100); + // end::capFunction[] + } +} diff --git a/content/docs/smallrye-mutiny/main/_examples/java/guides/operators/DelayTest.java b/content/docs/smallrye-mutiny/main/_examples/java/guides/operators/DelayTest.java new file mode 100644 index 000000000..052004e8b --- /dev/null +++ b/content/docs/smallrye-mutiny/main/_examples/java/guides/operators/DelayTest.java @@ -0,0 +1,87 @@ +package guides.operators; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.util.List; +import java.util.Random; + +import static org.assertj.core.api.Assertions.assertThat; + +public class DelayTest { + + @Test + public void testDelayBy() { + // tag::delay-by[] + Uni delayed = Uni.createFrom().item("hello") + .onItem().delayIt().by(Duration.ofMillis(10)); + // end::delay-by[] + String r = delayed.map(s -> "Delayed " + s) + .await().indefinitely(); + assertThat(r).isEqualTo("Delayed hello"); + } + + @Test + public void testDelayUntil() { + // tag::delay-until[] + Uni delayed = Uni.createFrom().item("hello") + // The write method returns a Uni completed + // when the operation is done. + .onItem().delayIt().until(this::write); + // end::delay-until[] + String r = delayed + .map(s -> "Written " + s) + .await().indefinitely(); + assertThat(r).isEqualTo("Written hello"); + } + + @Test + public void testDelayMulti() { + Multi multi = Multi.createFrom().items(1, 2, 3, 4, 5); + // tag::delay-multi[] + Multi delayed = multi + .onItem().call(i -> + // Delay the emission until the returned uni emits its item + Uni.createFrom().nullItem().onItem().delayIt().by(Duration.ofMillis(10)) + ); + // end::delay-multi[] + assertThat(delayed.collect().asList().await().indefinitely()).containsExactly(1, 2, 3, 4, 5); + } + + @Test + public void testThrottling() { + Multi multi = Multi.createFrom().items(1, 2, 3, 4, 5); + // tag::throttling-multi[] + // Introduce a one second delay between each item + Multi ticks = Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .onOverflow().drop(); + Multi delayed = Multi.createBy().combining().streams(ticks, multi) + .using((x, item) -> item); + // end::throttling-multi[] + assertThat(delayed.collect().asList().await().indefinitely()).containsExactly(1, 2, 3, 4, 5); + } + + @Test + public void testDelayMultiRandom() { + // tag::delay-multi-random[] + Random random = new Random(); + Multi delayed = Multi.createFrom().items(1, 2, 3, 4, 5) + .onItem().call(i -> { + Duration delay = Duration.ofMillis(random.nextInt(100) + 1); + return Uni.createFrom().nullItem().onItem().delayIt().by(delay); + }); + // end::delay-multi-random[] + assertThat(delayed.collect().asList() + .await().indefinitely()) + .containsExactlyInAnyOrder(1, 2, 3, 4, 5); + } + + private Uni write(String s) { + return Uni.createFrom().item(s) + .onItem().delayIt().by(Duration.ofMillis(20)) + .onItem().ignore().andContinueWithNull(); + } + +} diff --git a/content/docs/smallrye-mutiny/main/_examples/java/guides/operators/EmitOnTest.java b/content/docs/smallrye-mutiny/main/_examples/java/guides/operators/EmitOnTest.java new file mode 100644 index 000000000..c9398aa36 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/_examples/java/guides/operators/EmitOnTest.java @@ -0,0 +1,62 @@ +package guides.operators; + +import guides.extension.SystemOut; +import guides.extension.SystemOutCaptureExtension; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import static org.assertj.core.api.Assertions.assertThat; + +@SuppressWarnings("Convert2MethodRef") +@ExtendWith(SystemOutCaptureExtension.class) +public class EmitOnTest { + + @Test + public void testEmitOn() { + // tag::example[] + Uni uni = Uni.createFrom().emitter(emitter -> + new Thread(() -> + emitter.complete("hello from " + + Thread.currentThread().getName()) + ).start() + ) + .onItem().transform(item -> { + // Called on the emission thread. + return item.toUpperCase(); + }); + // end::example[] + + assertThat(uni.await().indefinitely()).startsWith("HELLO FROM").doesNotContain("MAIN"); + } + + @Test + public void test(SystemOut out) { + Uni uni = Uni.createFrom().item("hello"); + Multi multi = Multi.createFrom().items("a", "b", "c"); + + ExecutorService executor = Executors.newFixedThreadPool(4); + // tag::code[] + String res0 = uni.emitOn(executor) + .onItem() + .invoke(s -> System.out.println("Received item `" + s + "` on thread: " + + Thread.currentThread().getName())) + .await().indefinitely(); + + String res1 = multi.emitOn(executor) + .onItem() + .invoke(s -> System.out.println("Received item `" + s + "` on thread: " + + Thread.currentThread().getName())) + .collect().first() + .await().indefinitely(); + // end::code[] + + assertThat(out.get()).doesNotContain("main").contains("pool-").contains("hello", "a"); + assertThat(res0).isEqualTo("hello"); + assertThat(res1).isEqualTo("a"); + } +} diff --git a/content/docs/smallrye-mutiny/main/_examples/java/guides/operators/FilterTest.java b/content/docs/smallrye-mutiny/main/_examples/java/guides/operators/FilterTest.java new file mode 100644 index 000000000..39e72d024 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/_examples/java/guides/operators/FilterTest.java @@ -0,0 +1,101 @@ +package guides.operators; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class FilterTest { + + @Test + public void filter() { + Multi multi = Multi.createFrom().range(1, 11); + // tag::filter[] + List list = multi + .select().where(i -> i > 6) + .collect().asList() + .await().indefinitely(); + // end::filter[] + + // tag::test[] + List list2 = multi + .select().when(i -> Uni.createFrom().item(i > 6)) + .collect().asList() + .await().indefinitely(); + // end::test[] + + assertThat(list).containsExactly(7, 8, 9, 10); + assertThat(list2).containsExactly(7, 8, 9, 10); + } + + @Test + public void take() { + Multi multi = Multi.createFrom().range(1, 11); + // + List list = multi + .select().first(2) + .collect().asList() + .await().indefinitely(); + + List list2 = multi + .select().first(i -> i < 3) + .collect().asList() + .await().indefinitely(); + + List list3 = multi + .select().last(2) + .collect().asList() + .await().indefinitely(); + // + assertThat(list).containsExactly(1, 2); + assertThat(list2).containsExactly(1, 2); + assertThat(list3).containsExactly(9, 10); + } + + @Test + public void skip() { + Multi multi = Multi.createFrom().range(1, 11); + // + List list = multi + .skip().first(8) + .collect().asList() + .await().indefinitely(); + + List list2 = multi + .skip().first(i -> i < 9) + .collect().asList() + .await().indefinitely(); + + List list3 = multi + .skip().last(8) + .collect().asList() + .await().indefinitely(); + // + assertThat(list).containsExactly(9, 10); + assertThat(list2).containsExactly(9, 10); + assertThat(list3).containsExactly(1, 2); + } + + @Test + public void distinct() { + Multi multi = Multi.createFrom().items(1, 1, 2, 3, 4, 5, 5, 6); + // + List list = multi + .select().distinct() + .collect().asList() + .await().indefinitely(); + // + + // + List list2 = multi + .skip().repetitions() + .collect().asList() + .await().indefinitely(); + // + assertThat(list).containsExactly(1, 2, 3, 4, 5, 6); + assertThat(list2).containsExactly(1, 2, 3, 4, 5, 6); + } +} diff --git a/content/docs/smallrye-mutiny/main/_examples/java/guides/operators/GroupingItemsTest.java b/content/docs/smallrye-mutiny/main/_examples/java/guides/operators/GroupingItemsTest.java new file mode 100644 index 000000000..022c11219 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/_examples/java/guides/operators/GroupingItemsTest.java @@ -0,0 +1,188 @@ +package guides.operators; + +import io.smallrye.mutiny.GroupedMulti; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.groups.Gatherer; +import io.smallrye.mutiny.groups.Gatherers; +import io.smallrye.mutiny.helpers.test.AssertSubscriber; +import io.smallrye.mutiny.subscription.BackPressureFailure; +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collector; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class GroupingItemsTest { + + @Test + public void groupIntoLists() { + // tag::groupIntoLists[] + Multi> grouped = Multi.createFrom().range(0, 10) + .group().intoLists().of(3); // Group every 3 items into a list + + List> lists = grouped + .collect().asList() + .await().indefinitely(); + + assertThat(lists).containsExactly( + List.of(0, 1, 2), + List.of(3, 4, 5), + List.of(6, 7, 8), + List.of(9) + ); + // end::groupIntoLists[] + } + + @Test + public void timeBasedListGrouping() { + // tag::timeBasedListGrouping[] + Multi> timeWindows = Multi.createFrom() + .ticks().every(Duration.ofMillis(10)) + .group().intoLists().every(Duration.ofMillis(100)); + + // Each list contains items emitted within a 100ms window + // end::timeBasedListGrouping[] + } + + @Test + public void sizeAndTimeBasedListGrouping() { + // tag::sizeAndTimeBasedListGrouping[] + Multi> combined = Multi.createFrom() + .ticks().every(Duration.ofMillis(10)) + .group().intoLists().of(5, Duration.ofMillis(100)); + + // Emits a list when either 5 items are collected OR 100ms expires, + // whichever happens first + // end::sizeAndTimeBasedListGrouping[] + } + + @Test + public void groupIntoMultis() { + // tag::groupIntoMultis[] + Multi> grouped = Multi.createFrom().range(0, 7) + .group().intoMultis().of(2); // Group every 2 items into a Multi + + // Process each Multi independently - for example, join each group + Multi sums = grouped + .onItem().transformToUniAndConcatenate(multi -> + multi.collect().asList() + .onItem().transform(couple -> couple.stream() + .map(Objects::toString) + .collect(Collectors.joining("-")))); + + List results = sums.collect().asList().await().indefinitely(); + assertThat(results).containsExactly("0-1", "2-3", "4-5", "6"); + // end::groupIntoMultis[] + } + + @Test + public void timeBasedGrouping() { + // tag::timeBasedGrouping[] + Multi> timeWindows = Multi.createFrom() + .ticks().every(Duration.ofMillis(10)) + .group().intoMultis().every(Duration.ofMillis(100)); + + // Each Multi contains items emitted within a 100ms window + // end::timeBasedGrouping[] + } + + @Test + public void groupByKey() { + // tag::groupByKey[] + Multi> groups = Multi.createFrom().range(1, 10) + .group().by(i -> i % 3); // Group by modulo 3 + + // Process each group separately + Uni> result = groups + .onItem().transformToUniAndConcatenate(group -> + group.onItem().transform(Objects::toString) + .collect().asList() + .onItem().transform(l -> group.key() + "(" + String.join(",", l) + ")")) + .collect().asList(); + + List lists = result.await().indefinitely(); + assertThat(lists).containsExactly("1(1,4,7)", "2(2,5,8)", "0(3,6,9)"); + // end::groupByKey[] + } + + @Test + public void groupByKeyAndValue() { + // tag::groupByKeyAndValue[] + Multi> groups = Multi.createFrom().range(1, 10) + .group().by( + i -> i % 2 == 0, // Key: true for even, false for odd + i -> "Number: " + i // Value: transform to string + ); + + Uni>> result = groups + .onItem().transformToUniAndMerge(group -> + group.collect().asList() + ) + .collect().asList(); + + List> lists = result.await().indefinitely(); + assertThat(lists).hasSize(2); + // end::groupByKeyAndValue[] + } + + @Test + public void groupByWithMerge() { + // tag::groupByWithMerge[] + Multi results = Multi.createFrom().range(1, 100) + .group().by(i -> i % 10) // Create 10 groups (0-9) + .onItem().transformToMulti( + group -> group.onItem().transform(i -> "Group " + group.key() + ": " + i) + ) + .merge(10); // Set concurrency to at least the number of groups + + List items = results.collect().asList().await().indefinitely(); + assertThat(items).hasSize(99); + // end::groupByWithMerge[] + } + + @Test + public void groupByDeadlockExample() { + // tag::groupByDeadlock[] + // Creating 10 groups but limiting concurrency to only 2 + Multi dangerous = Multi.createFrom().ticks().every(Duration.ofMillis(10)) + .group().by(i -> i % 10) // Creates 10 groups + .onItem().transformToMulti( + group -> group.onItem().transform(i -> "Group " + group.key() + ": " + i) + ) + .merge(2); // Only 2 groups can be processed concurrently + + // This may hang because groups 3-10 cannot make progress + // while waiting for groups 1-2 to complete + AssertSubscriber sub = dangerous.subscribe() + .withSubscriber(AssertSubscriber.create(Long.MAX_VALUE)); + + sub.awaitFailure(t -> assertThat(t).isInstanceOf(BackPressureFailure.class)); + // end::groupByDeadlock[] + } + + @Test + public void groupByWithConcatenate() { + // tag::groupByWithConcatenate[] + // Safe alternative: use concatenate to process groups sequentially + Multi safe = Multi.createFrom().range(1, 100) + .invoke(i -> System.out.println("emit " + i)) + .group().by(i -> i % 10) + .onItem().transformToMultiAndConcatenate( + group -> group.onItem().transform(i -> "Group " + group.key() + ": " + i) + .invoke(i -> System.out.println("group " + i)) + ); + // Groups are processed one at a time, so no deadlock + + List items = safe.collect().asList().await().indefinitely(); + assertThat(items).hasSize(99); + // end::groupByWithConcatenate[] + } + +} diff --git a/content/docs/smallrye-mutiny/main/_examples/java/guides/operators/MergeConcatTest.java b/content/docs/smallrye-mutiny/main/_examples/java/guides/operators/MergeConcatTest.java new file mode 100644 index 000000000..580404e7e --- /dev/null +++ b/content/docs/smallrye-mutiny/main/_examples/java/guides/operators/MergeConcatTest.java @@ -0,0 +1,119 @@ +package guides.operators; + +import guides.extension.SystemOut; +import guides.extension.SystemOutCaptureExtension; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.subscription.Cancellable; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.time.Duration; +import java.util.List; +import java.util.Random; +import java.util.concurrent.CopyOnWriteArrayList; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +@SuppressWarnings({ "unchecked", "Convert2MethodRef" }) +@ExtendWith(SystemOutCaptureExtension.class) +public class MergeConcatTest { + + private final Random random = new Random(); + + @Test + public void testMerge() { + // tag::merge[] + Multi multi1 = getFirstMulti(); + Multi multi2 = getSecondMulti(); + + Multi merged = Multi.createBy().merging().streams(multi1, multi2); + // end::merge[] + + List received = new CopyOnWriteArrayList<>(); + merged.subscribe().with(received::add); + await().until(() -> received.size() == 9); + assertThat(received).contains(1, 2, 3, 4, 5, 6, 7, 8, 9); + } + + @Test + public void testConcat() { + // tag::concat[] + Multi multi1 = getFirstMulti(); + Multi multi2 = getSecondMulti(); + + Multi concatenated = Multi.createBy().concatenating().streams(multi1, multi2); + // end::concat[] + + List received = new CopyOnWriteArrayList<>(); + concatenated.subscribe().with(received::add); + await().until(() -> received.size() == 9); + assertThat(received).containsExactly(1, 2, 3, 4, 5, 6, 7, 8, 9); + } + + @Test + public void testMergeTicks(SystemOut out) { + // tag::merge-ticks[] + Multi first = Multi.createFrom().ticks().every(Duration.ofMillis(10)) + .onItem().transform(l -> "Stream 1 - " + l); + + Multi second = Multi.createFrom().ticks().every(Duration.ofMillis(15)) + .onItem().transform(l -> "Stream 2 - " + l); + + Multi third = Multi.createFrom().ticks().every(Duration.ofMillis(5)) + .onItem().transform(l -> "Stream 3 - " + l); + + Cancellable cancellable = Multi.createBy().merging().streams(first, second, third) + .subscribe().with(s -> System.out.println("Got item: " + s)); + // end::merge-ticks[] + + await().until(() -> out.get().contains("Got item: Stream 3 - 10")); + cancellable.cancel(); + } + + @Test + public void testConcatenateStrings(SystemOut out) { + // tag::concatenate-strings[] + Multi first = Multi.createFrom().items("A1", "A2", "A3"); + Multi second = Multi.createFrom().items("B1", "B2", "B3"); + + Multi.createBy().concatenating().streams(first, second) + .subscribe().with(item -> System.out.print(item)); // "A1A2A3B1B2B3" + + Multi.createBy().concatenating().streams(second, first) + .subscribe().with(item -> System.out.print(item)); // "B1B2B3A1A2A3" + // end::concatenate-strings[] + + await().until(() -> out.get().contains("A1A2A3B1B2B3")); + await().until(() -> out.get().contains("B1B2B3A1A2A3")); + + assertThat( + Multi.createBy().concatenating().streams(first, second) + .collect().in(StringBuffer::new, StringBuffer::append) + .await().indefinitely() + ).isEqualToIgnoringCase("A1A2A3B1B2B3"); + + assertThat( + Multi.createBy().concatenating().streams(second, first) + .collect().in(StringBuffer::new, StringBuffer::append) + .await().indefinitely() + ).isEqualToIgnoringCase("B1B2B3A1A2A3"); + + } + + private Multi getFirstMulti() { + return Multi.createFrom().items(1, 2, 3, 4, 5, 6) + .onItem().call(() -> Uni.createFrom().nullItem().onItem().delayIt() + .by(Duration.ofMillis(random.nextInt(100) + 1))) + .onItem().transform(i -> (T) i); + } + + private Multi getSecondMulti() { + return Multi.createFrom().items(7, 8, 9) + .onItem().call(() -> Uni.createFrom().nullItem().onItem().delayIt() + .by(Duration.ofMillis(random.nextInt(100) + 1))) + .onItem().transform(i -> (T) i); + } +} diff --git a/content/docs/smallrye-mutiny/main/_examples/java/guides/operators/PaginationTest.java b/content/docs/smallrye-mutiny/main/_examples/java/guides/operators/PaginationTest.java new file mode 100644 index 000000000..e032de2a6 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/_examples/java/guides/operators/PaginationTest.java @@ -0,0 +1,97 @@ +package guides.operators; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.helpers.test.AssertSubscriber; +import org.junit.jupiter.api.Test; + +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.assertj.core.api.Assertions.assertThat; + +public class PaginationTest { + + @SuppressWarnings("Convert2MethodRef") + @Test + public void test() { + // tag::code[] + PaginatedApi api = new PaginatedApi(); + + Multi stream = Multi.createBy().repeating() + .completionStage( + () -> new AtomicInteger(), + state -> api.getPage(state.getAndIncrement())) + .until(list -> list.isEmpty()) + .onItem().disjoint(); + // end::code[] + stream.subscribe().withSubscriber(AssertSubscriber.create(10)) + .assertCompleted() + .assertItems("a", "b", "c", "d", "e", "f", "g", "h"); + + } + + @SuppressWarnings("Convert2MethodRef") + @Test + public void test2() { + // tag::code2[] + PaginatedApi api = new PaginatedApi(); + + Multi stream = Multi.createBy().repeating() + .uni( + () -> new AtomicInteger(), + state -> api.retrieve(state.getAndIncrement())) + .whilst(page -> page.hasNext()); + // end::code2[] + AssertSubscriber subscriber = stream.subscribe() + .withSubscriber(AssertSubscriber.create(10)) + .assertCompleted(); + + assertThat(subscriber.getItems()).hasSize(3); + + } + + private static class Page { + final boolean hasNext; + + private Page(boolean hasNext) { + this.hasNext = hasNext; + } + + public boolean hasNext() { + return hasNext; + } + } + + + private class PaginatedApi { + + Map> pages = new LinkedHashMap<>(); + + public PaginatedApi() { + pages.put(0, Arrays.asList("a", "b", "c")); + pages.put(1, Arrays.asList("d", "e", "f")); + pages.put(2, Arrays.asList("g", "h")); + pages.put(3, Collections.emptyList()); + } + + CompletionStage> getPage(int page) { + List strings = pages.get(page); + if (strings == null) { + strings = Collections.emptyList(); + } + return CompletableFuture.completedFuture(strings); + } + + + Uni retrieve(int page) { + if (page == 2) { + return Uni.createFrom().item(new Page(false)); + } + return Uni.createFrom().item(new Page(true)); + } + } + +} diff --git a/content/docs/smallrye-mutiny/main/_examples/java/guides/operators/PausingDemandTest.java b/content/docs/smallrye-mutiny/main/_examples/java/guides/operators/PausingDemandTest.java new file mode 100644 index 000000000..9f27d3dd1 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/_examples/java/guides/operators/PausingDemandTest.java @@ -0,0 +1,267 @@ +package guides.operators; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.helpers.test.AssertSubscriber; +import io.smallrye.mutiny.subscription.BackPressureStrategy; +import io.smallrye.mutiny.subscription.DemandPauser; +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +class PausingDemandTest { + + @Test + void basicPausing() { + // tag::basic[] + DemandPauser pauser = new DemandPauser(); + + AssertSubscriber sub = AssertSubscriber.create(); + Multi.createFrom().range(0, 100) + .pauseDemand().using(pauser) + // Throttle the multi + .onItem().call(i -> Uni.createFrom().nullItem() + .onItem().delayIt().by(Duration.ofMillis(10))) + .subscribe().withSubscriber(sub); + + // Unbounded request + sub.request(Long.MAX_VALUE); + // Wait for some items + await().untilAsserted(() -> assertThat(sub.getItems()).hasSizeGreaterThan(10)); + + // Pause the stream + pauser.pause(); + assertThat(pauser.isPaused()).isTrue(); + + int sizeWhenPaused = sub.getItems().size(); + + // Wait - no new items should arrive (except a few in-flight) + await().pollDelay(Duration.ofMillis(100)).until(() -> true); + assertThat(sub.getItems()).hasSizeLessThanOrEqualTo(sizeWhenPaused + 5); + + // Resume the stream + pauser.resume(); + assertThat(pauser.isPaused()).isFalse(); + + // All items eventually arrive + sub.awaitCompletion(); + assertThat(sub.getItems()).hasSize(100); + // end::basic[] + } + + @Test + void initiallyPaused() { + // tag::initially-paused[] + DemandPauser pauser = new DemandPauser(); + + AssertSubscriber sub = Multi.createFrom().range(0, 50) + .pauseDemand() + .paused(true) // Start paused + .using(pauser) + .subscribe().withSubscriber(AssertSubscriber.create(Long.MAX_VALUE)); + + // No items arrive while paused + await().pollDelay(Duration.ofMillis(100)).until(() -> true); + assertThat(sub.getItems()).isEmpty(); + assertThat(pauser.isPaused()).isTrue(); + + // Resume to start receiving items + pauser.resume(); + sub.awaitCompletion(); + assertThat(sub.getItems()).hasSize(50); + // end::initially-paused[] + } + + @Test + void lateSubscription() { + // tag::late-subscription[] + DemandPauser pauser = new DemandPauser(); + AtomicBoolean subscribed = new AtomicBoolean(false); + + AssertSubscriber sub = Multi.createFrom().range(0, 50) + .onSubscription().invoke(() -> subscribed.set(true)) + .pauseDemand() + .paused(true) // Start paused + .lateSubscription(true) // Delay subscription until resumed + .using(pauser) + .subscribe().withSubscriber(AssertSubscriber.create(Long.MAX_VALUE)); + + // Stream is not subscribed yet + await().pollDelay(Duration.ofMillis(100)).until(() -> true); + assertThat(subscribed.get()).isFalse(); + assertThat(sub.getItems()).isEmpty(); + + // Resume triggers subscription and item flow + pauser.resume(); + await().untilAsserted(() -> assertThat(subscribed.get()).isTrue()); + sub.awaitCompletion(); + assertThat(sub.getItems()).hasSize(50); + // end::late-subscription[] + } + + @Test + void bufferStrategy() { + // tag::buffer-strategy[] + DemandPauser pauser = new DemandPauser(); + + AssertSubscriber sub = AssertSubscriber.create(Long.MAX_VALUE); + + Multi.createFrom().ticks().every(Duration.ofMillis(10)) + .select().first(100) + .pauseDemand() + .bufferStrategy(BackPressureStrategy.BUFFER) + .bufferSize(20) // Buffer up to 20 items + .using(pauser) + .subscribe().withSubscriber(sub); + + // Wait for some items + await().untilAsserted(() -> assertThat(sub.getItems()).hasSizeGreaterThan(5)); + + // Pause - items buffer up to the limit + pauser.pause(); + + // Wait for buffer to fill + await().pollDelay(Duration.ofMillis(100)) + .untilAsserted(() -> assertThat(pauser.bufferSize()).isGreaterThan(0)); + + // Buffer size is capped + assertThat(pauser.bufferSize()).isLessThanOrEqualTo(20); + + // Resume drains the buffer + pauser.resume(); + await().untilAsserted(() -> assertThat(pauser.bufferSize()).isEqualTo(0)); + // end::buffer-strategy[] + } + + @Test + void dropStrategy() { + // tag::drop-strategy[] + DemandPauser pauser = new DemandPauser(); + + AssertSubscriber sub = Multi.createFrom().ticks().every(Duration.ofMillis(5)) + .select().first(200) + .pauseDemand() + .bufferStrategy(BackPressureStrategy.DROP) // Drop items while paused + .using(pauser) + .subscribe().withSubscriber(AssertSubscriber.create(Long.MAX_VALUE)); + + // Wait for some items + await().untilAsserted(() -> assertThat(sub.getItems()).hasSizeGreaterThan(20)); + + // Pause - subsequent items are dropped + pauser.pause(); + int sizeWhenPaused = sub.getItems().size(); + + // Wait while items are dropped + await().pollDelay(Duration.ofMillis(200)).until(() -> true); + + // Resume - continue from current position + pauser.resume(); + await().atMost(Duration.ofSeconds(3)) + .untilAsserted(() -> assertThat(sub.getItems().size()).isGreaterThan(sizeWhenPaused + 20)); + + sub.awaitCompletion(); + // Not all items arrived (some were dropped) + assertThat(sub.getItems()).hasSizeLessThan(200); + // end::drop-strategy[] + } + + @Test + void bufferManagement() { + // tag::buffer-management[] + DemandPauser pauser = new DemandPauser(); + + AssertSubscriber sub = Multi.createFrom().ticks().every(Duration.ofMillis(10)) + .select().first(100) + .pauseDemand() + .bufferStrategy(BackPressureStrategy.BUFFER) + .bufferUnconditionally() // Unbounded buffer + .using(pauser) + .subscribe().withSubscriber(AssertSubscriber.create(Long.MAX_VALUE)); + + // Wait for some items + await().untilAsserted(() -> assertThat(sub.getItems()).hasSizeGreaterThan(5)); + + // Pause and let buffer fill + pauser.pause(); + await().pollDelay(Duration.ofMillis(100)) + .untilAsserted(() -> assertThat(pauser.bufferSize()).isGreaterThan(0)); + + int bufferSize = pauser.bufferSize(); + assertThat(bufferSize).isGreaterThan(0); + + // Clear the buffer + boolean cleared = pauser.clearBuffer(); + assertThat(cleared).isTrue(); + assertThat(pauser.bufferSize()).isEqualTo(0); + + // Resume - items continue from current position (cleared items are lost) + pauser.resume(); + // end::buffer-management[] + } + + @Test + void externalControl() { + // + DemandPauser pauser = new DemandPauser(); + + // Create and transform the stream + Multi stream = Multi.createFrom().range(0, 1000) + .pauseDemand().using(pauser) + .onItem().transform(i -> "Item-" + i) + .onItem().transform(String::toUpperCase); + + // Control the stream from anywhere + pauser.pause(); + + // Later, subscribe to the stream + AssertSubscriber sub = stream + .subscribe().withSubscriber(AssertSubscriber.create(Long.MAX_VALUE)); + + // No items while paused + await().pollDelay(Duration.ofMillis(100)).until(() -> true); + assertThat(sub.getItems()).isEmpty(); + + // Resume to receive items + pauser.resume(); + sub.awaitCompletion(); + assertThat(sub.getItems()).hasSize(1000); + // + } + + @Test + void conditionalFlow() { + // + DemandPauser pauser = new DemandPauser(); + AtomicInteger errorCount = new AtomicInteger(0); + + AssertSubscriber sub = Multi.createFrom().range(0, 100) + .pauseDemand().using(pauser) + .onItem().invoke(item -> { + // Simulate error-prone processing + if (item % 20 == 0 && item > 0) { + errorCount.incrementAndGet(); + pauser.pause(); // Pause on errors + } + }) + .subscribe().withSubscriber(AssertSubscriber.create(Long.MAX_VALUE)); + + // Stream pauses when errors occur + await().untilAsserted(() -> assertThat(errorCount.get()).isGreaterThan(0)); + await().untilAsserted(() -> assertThat(pauser.isPaused()).isTrue()); + + // Handle the error, then resume + errorCount.set(0); + pauser.resume(); + + // Stream continues + await().pollDelay(Duration.ofMillis(50)).until(() -> true); + assertThat(sub.getItems().size()).isGreaterThan(20); + // + } +} diff --git a/content/docs/smallrye-mutiny/main/_examples/java/guides/operators/PollableSourceTest.java b/content/docs/smallrye-mutiny/main/_examples/java/guides/operators/PollableSourceTest.java new file mode 100644 index 000000000..d5b10e736 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/_examples/java/guides/operators/PollableSourceTest.java @@ -0,0 +1,84 @@ +package guides.operators; + +import guides.extension.SystemOut; +import guides.extension.SystemOutCaptureExtension; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.subscription.Cancellable; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.awaitility.Awaitility.await; + +@ExtendWith(SystemOutCaptureExtension.class) +public class PollableSourceTest { + + private final ExecutorService executor = Executors.newSingleThreadExecutor(); + + @Test + public void test(SystemOut out) { // NOSONAR + // tag::code[] + PollableDataSource source = new PollableDataSource(); + // First creates a uni that emit the polled item. + // Because `poll` blocks, let's use a specific executor + Uni pollItemFromSource = Uni.createFrom().item(source::poll) + .runSubscriptionOn(executor); + // To get the stream of items, just repeat the uni indefinitely + Multi stream = pollItemFromSource.repeat().indefinitely(); + + Cancellable cancellable = stream.subscribe().with(item -> System.out.println("Polled item: " + item)); + // end::code[] + await().until(() -> source.counter.get() >= 4); + // tag::code[] + // ... later .. + // when you don't want the items anymore, cancel the subscription and close the source if needed. + cancellable.cancel(); + source.close(); + // end::code[] + } + + @SuppressWarnings("Convert2MethodRef") + @Test + public void test2(SystemOut out) { // NOSONAR + // tag::code2[] + PollableDataSource source = new PollableDataSource(); + Multi stream = Multi.createBy().repeating() + .supplier(source::poll) + .until(s -> s == null) + .runSubscriptionOn(executor); + + stream.subscribe().with(item -> System.out.println("Polled item: " + item)); + // end::code2[] + await().until(() -> source.counter.get() >= 5); + } + + private static class PollableDataSource { + + private final AtomicInteger counter = new AtomicInteger(); + + String poll() { + block(); + if (counter.get() == 5) { + return null; + } + return Integer.toString(counter.getAndIncrement()); + } + + private void block() { + try { + Thread.sleep(100); // NOSONAR + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + public void close() { + // do nothing. + } + } + +} diff --git a/content/docs/smallrye-mutiny/main/_examples/java/guides/operators/RepetitionsTest.java b/content/docs/smallrye-mutiny/main/_examples/java/guides/operators/RepetitionsTest.java new file mode 100644 index 000000000..c32dcf760 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/_examples/java/guides/operators/RepetitionsTest.java @@ -0,0 +1,31 @@ +package guides.operators; + +import io.smallrye.mutiny.Multi; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class RepetitionsTest { + + @Test + public void distinct() { + Multi multi = Multi.createFrom().items(1, 1, 2, 3, 4, 5, 5, 6, 1, 4, 4); + // tag::distinct[] + List list = multi + .select().distinct() + .collect().asList() + .await().indefinitely(); + // end::distinct[] + + // tag::repetition[] + List list2 = multi + .skip().repetitions() + .collect().asList() + .await().indefinitely(); + // end::repetition[] + assertThat(list).containsExactly(1, 2, 3, 4, 5, 6); + assertThat(list2).containsExactly(1, 2, 3, 4, 5, 6, 1, 4); + } +} diff --git a/content/docs/smallrye-mutiny/main/_examples/java/guides/operators/ReplayTest.java b/content/docs/smallrye-mutiny/main/_examples/java/guides/operators/ReplayTest.java new file mode 100644 index 000000000..268421678 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/_examples/java/guides/operators/ReplayTest.java @@ -0,0 +1,82 @@ +package guides.operators; + +import io.smallrye.mutiny.Multi; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ReplayTest { + + @Test + public void replayAll() { + // tag::replay-all[] + Multi upstream = Multi.createFrom().range(0, 10); + + Multi replay = Multi.createBy().replaying().ofMulti(upstream); + + List items_1 = replay.collect().asList().await().indefinitely(); + List items_2 = replay.collect().asList().await().indefinitely(); + // end::replay-all[] + + assertThat(items_1) + .isEqualTo(items_2) + .hasSize(10); + System.out.println(items_1); + } + + @Test + public void replayLast() { + // tag::replay-last[] + Multi upstream = Multi.createFrom().range(0, 10); + + Multi replay = Multi.createBy().replaying().upTo(3).ofMulti(upstream); + + List items_1 = replay.collect().asList().await().indefinitely(); + List items_2 = replay.collect().asList().await().indefinitely(); + // end::replay-last[] + + assertThat(items_1) + .isEqualTo(items_2) + .hasSize(3); + System.out.println(items_1); + } + + @Test + public void replayWithSeed() { + // tag::replay-seed[] + Multi upstream = Multi.createFrom().range(0, 10); + Iterable seed = Arrays.asList(-10, -5, -1); + + Multi replay = Multi.createBy().replaying().ofSeedAndMulti(seed, upstream); + + List items_1 = replay.collect().asList().await().indefinitely(); + List items_2 = replay.collect().asList().await().indefinitely(); + // end::replay-seed[] + + assertThat(items_1) + .isEqualTo(items_2) + .hasSize(13); + System.out.println(items_1); + } + + @Test + public void errors() { + // tag::replay-errors[] + Multi upstream = Multi.createBy().concatenating().streams( + Multi.createFrom().range(0, 10), + Multi.createFrom().failure(() -> new IOException("boom")) + ); + + Multi replay = Multi.createBy().replaying().upTo(3).ofMulti(upstream); + + replay.subscribe().with( + n -> System.out.println(" -> " + n), + failure -> System.out.println("Failed: " + failure.getMessage()), + () -> System.out.println("Completed")); + // end::replay-errors[] + } +} diff --git a/content/docs/smallrye-mutiny/main/_examples/java/guides/operators/RunSubscriptionOnTest.java b/content/docs/smallrye-mutiny/main/_examples/java/guides/operators/RunSubscriptionOnTest.java new file mode 100644 index 000000000..fbe4d38bf --- /dev/null +++ b/content/docs/smallrye-mutiny/main/_examples/java/guides/operators/RunSubscriptionOnTest.java @@ -0,0 +1,67 @@ +package guides.operators; + +import guides.extension.SystemOut; +import guides.extension.SystemOutCaptureExtension; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.infrastructure.Infrastructure; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Stream; + +import static org.awaitility.Awaitility.await; +import static org.hamcrest.CoreMatchers.is; + +@SuppressWarnings("Convert2MethodRef") +@ExtendWith(SystemOutCaptureExtension.class) +public class RunSubscriptionOnTest { + + @Test + public void testRunSubscriptionOn(SystemOut out) { + Executor executor = Infrastructure.getDefaultExecutor(); + AtomicBoolean completed = new AtomicBoolean(); + //tag::runSubscriptionOn[] + Multi.createFrom().items(() -> { + // called on a thread from the executor + return retrieveItemsFromSource(); + }) + .onItem().transform(this::applySomeOperation) + .runSubscriptionOn(executor) + .subscribe().with( + item -> System.out.println("Item: " + item), + Throwable::printStackTrace, + () -> completed.set(true) + ); + //end::runSubscriptionOn[] + + await().untilAtomic(completed, is(true)); + } + + @Test + public void testEmitOn(SystemOut out) { + Executor executor = Infrastructure.getDefaultExecutor(); + AtomicBoolean completed = new AtomicBoolean(); + //tag::emitOn[] + Multi.createFrom().items(this::retrieveItemsFromSource) + .emitOn(executor) + .onItem().transform(this::applySomeOperation) + .subscribe().with( + item -> System.out.println("Item: " + item), + Throwable::printStackTrace, + () -> completed.set(true) + ); + //end::emitOn[] + + await().untilAtomic(completed, is(true)); + } + + public String applySomeOperation(String s) { + return s.toUpperCase(); + } + + public Stream retrieveItemsFromSource() { + return Stream.of("a", "b", "c"); + } +} diff --git a/content/docs/smallrye-mutiny/main/_examples/java/guides/operators/SelectAndSkipTest.java b/content/docs/smallrye-mutiny/main/_examples/java/guides/operators/SelectAndSkipTest.java new file mode 100644 index 000000000..a8ffa4508 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/_examples/java/guides/operators/SelectAndSkipTest.java @@ -0,0 +1,65 @@ +package guides.operators; + +import io.smallrye.mutiny.Multi; +import org.junit.jupiter.api.Test; + +import java.time.Duration; + +import static org.assertj.core.api.Assertions.assertThat; + +public class SelectAndSkipTest { + + @Test + public void testSelect() { + Multi multi = Multi.createFrom().items(1, 2, 3, 4, 5, 6, 7, 8, 9); + + // tag::take-first[] + Multi firstThreeItems = multi.select().first(3); + // end::take-first[] + + // tag::take-last[] + Multi lastThreeItems = multi.select().last(3); + // end::take-last[] + + // tag::take-while[] + Multi takeWhile = multi.select().first(i -> i < 4); + // end::take-while[] + + // tag::take-for[] + Multi takeForDuration = multi.select().first(Duration.ofSeconds(1)); + // end::take-for[] + + assertThat(firstThreeItems.collect().asList().await().indefinitely()).containsExactly(1, 2, 3); + assertThat(takeWhile.collect().asList().await().indefinitely()).containsExactly(1, 2, 3); + assertThat(lastThreeItems.collect().asList().await().indefinitely()).containsExactly(7, 8, 9); + assertThat(takeForDuration.collect().asList().await().indefinitely()).containsExactly(1, 2, 3, 4, 5, 6, 7, 8, 9); + + } + + @Test + public void testSkip() { + Multi multi = Multi.createFrom().items(1, 2, 3, 4, 5, 6, 7, 8, 9); + + // tag::skip-first[] + Multi skipThreeItems = multi.skip().first(3); + // end::skip-first[] + + // tag::skip-last[] + Multi skipLastThreeItems = multi.skip().last(3); + // end::skip-last[] + + // tag::skip-while[] + Multi skipWhile = multi.skip().first(i -> i < 4); + // end::skip-while[] + + // tag::skip-for[] + Multi skipForDuration = multi.skip().first(Duration.ofSeconds(1)); + // end::skip-for[] + + assertThat(skipThreeItems.collect().asList().await().indefinitely()).containsExactly(4, 5, 6, 7, 8, 9); + assertThat(skipLastThreeItems.collect().asList().await().indefinitely()).containsExactly(1, 2, 3, 4 ,5, 6); + assertThat(skipWhile.collect().asList().await().indefinitely()).containsExactly(4, 5, 6, 7, 8, 9); + assertThat(skipForDuration.collect().asList().await().indefinitely()).isEmpty(); + + } +} diff --git a/content/docs/smallrye-mutiny/main/_examples/java/guides/operators/SplitTest.java b/content/docs/smallrye-mutiny/main/_examples/java/guides/operators/SplitTest.java new file mode 100644 index 000000000..bf2855bb9 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/_examples/java/guides/operators/SplitTest.java @@ -0,0 +1,45 @@ +package guides.operators; + +import io.smallrye.mutiny.Multi; +import org.junit.jupiter.api.Test; + +public class SplitTest { + + // tag::enum[] + enum Signals { + INPUT, + OUTPUT, + OTHER + } + // end::enum[] + + @Test + public void splitDemo() { + // tag::splits[] + Multi multi = Multi.createFrom().items( + "!a", "?b", "!c", "!d", "123", "?e" + ); + + var splitter = multi.split(Signals.class, s -> { + if (s.startsWith("?")) { + return Signals.INPUT; + } else if (s.startsWith("!")) { + return Signals.OUTPUT; + } else { + return Signals.OTHER; + } + }); + + splitter.get(Signals.INPUT) + .onItem().transform(s -> s.substring(1)) + .subscribe().with(signal -> System.out.println("input - " + signal)); + + splitter.get(Signals.OUTPUT) + .onItem().transform(s -> s.substring(1)) + .subscribe().with(signal -> System.out.println("output - " + signal)); + + splitter.get(Signals.OTHER) + .subscribe().with(signal -> System.out.println("other - " + signal)); + // end::splits[] + } +} diff --git a/content/docs/smallrye-mutiny/main/_examples/java/guides/operators/UniJoinTest.java b/content/docs/smallrye-mutiny/main/_examples/java/guides/operators/UniJoinTest.java new file mode 100644 index 000000000..a02897084 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/_examples/java/guides/operators/UniJoinTest.java @@ -0,0 +1,54 @@ +package guides.operators; + +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.groups.UniJoin; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.List; +import java.util.function.Supplier; + +class UniJoinTest { + + @Test + void joinAll() { + // tag::join-all[] + Uni a = Uni.createFrom().item(1); + Uni b = Uni.createFrom().item(2); + Uni c = Uni.createFrom().item(3); + + Uni> res = Uni.join().all(a, b, c).andCollectFailures(); + // end::join-all[] + + // tag::join-all-ff[] + res = Uni.join().all(a, b, c).andFailFast(); + // end::join-all-ff[] + } + + void joinFirst(Uni a, Uni b, Uni c) { + + // tag::join-first[] + Uni res = Uni.join().first(a, b, c).toTerminate(); + // end::join-first[] + + // tag::join-first-withitem[] + res = Uni.join().first(a, b, c).withItem(); + // end::join-first-withitem[] + + Supplier> supplier = () -> Uni.createFrom().item(63); + boolean someCondition = false; + + // tag::builder[] + UniJoin.Builder builder = Uni.join().builder(); + + while (someCondition) { + Uni uni = supplier.get(); + builder.add(uni); + } + + Uni> all = builder.joinAll().andFailFast(); + + Uni first = builder.joinFirst().withItem(); + // end::builder[] + } +} diff --git a/content/docs/smallrye-mutiny/main/_examples/java/guides/operators/UnicastProcessorTest.java b/content/docs/smallrye-mutiny/main/_examples/java/guides/operators/UnicastProcessorTest.java new file mode 100644 index 000000000..237b59345 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/_examples/java/guides/operators/UnicastProcessorTest.java @@ -0,0 +1,34 @@ +package guides.operators; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.helpers.test.AssertSubscriber; +import io.smallrye.mutiny.operators.multi.processors.UnicastProcessor; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class UnicastProcessorTest { + + @Test + public void test() { + // tag::code[] + UnicastProcessor processor = UnicastProcessor.create(); + Multi multi = processor + .onItem().transform(String::toUpperCase) + .onFailure().recoverWithItem("d'oh"); + + // Create a source of items that does not follow the request protocol + new Thread(() -> { + for (int i = 0; i < 1000; i++) { + processor.onNext(Integer.toString(i)); + } + processor.onComplete(); + }).start(); + + // end::code[] + AssertSubscriber subscriber = AssertSubscriber.create(Long.MAX_VALUE); + multi.subscribe().withSubscriber(subscriber) + .awaitCompletion() + .run(() -> assertThat(subscriber.getItems()).hasSize(1000)); + } +} diff --git a/content/docs/smallrye-mutiny/main/_examples/java/guides/reference/PitfallsTest.java b/content/docs/smallrye-mutiny/main/_examples/java/guides/reference/PitfallsTest.java new file mode 100644 index 000000000..1db116a84 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/_examples/java/guides/reference/PitfallsTest.java @@ -0,0 +1,64 @@ +package guides.reference; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import org.junit.jupiter.api.Test; + +import java.util.List; + +public class PitfallsTest { + + private Uni fetch(String id) { + return Uni.createFrom().item("Yolo"); + } + + @Test + public void noMagicScheduling() { + // tag::noMagicJoin[] + Uni> data = Uni.join().all( + fetch("abc"), + fetch("def"), + fetch("123")).andFailFast(); + // end::noMagicJoin[] + } + + @Test + public void inMemoryData() { + // tag::inMemoryData[] + Uni uni = Uni.createFrom().item(123); + Multi multi = Multi.createFrom().items(1, 2, 3, 4, 5); + // end::inMemoryData[] + } + + private List obtainValues(String key) { + return List.of(); + } + + // tag::suspiciousPublisher[] + public Multi fetchData(String key) { + List strings = obtainValues(key); + return Multi.createFrom().iterable(strings); + } + // end::suspiciousPublisher[] + + private Multi streamData(String key) { + return Multi.createFrom().items(""); + } + + private Multi extractData(String line) { + return Multi.createFrom().items(""); + } + + private Multi flatmap() { + // tag::flatmap-ism[] + Multi stream = streamData("abc"); + return stream.onItem().transformToMultiAndMerge(line -> { + if (line.trim().length() > 10) { + return extractData(line); + } else { + return Multi.createFrom().item("[N/A]"); + } + }); + // end::flatmap-ism[] + } +} diff --git a/content/docs/smallrye-mutiny/main/_examples/java/tutorials/CreatingMultiTest.java b/content/docs/smallrye-mutiny/main/_examples/java/tutorials/CreatingMultiTest.java new file mode 100644 index 000000000..b2c5e621a --- /dev/null +++ b/content/docs/smallrye-mutiny/main/_examples/java/tutorials/CreatingMultiTest.java @@ -0,0 +1,134 @@ +package tutorials; + +import guides.extension.SystemOut; +import guides.extension.SystemOutCaptureExtension; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.helpers.BlockingIterable; +import io.smallrye.mutiny.subscription.Cancellable; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.time.Duration; +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.IntStream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.awaitility.Awaitility.await; + +@SuppressWarnings({"Convert2MethodRef"}) +@ExtendWith(SystemOutCaptureExtension.class) +public class CreatingMultiTest { + + @Test + void pipeline(SystemOut out) { + // tag::pipeline[] + Multi.createFrom().items(1, 2, 3, 4, 5) + .onItem().transform(i -> i * 2) + .select().first(3) + .onFailure().recoverWithItem(0) + .subscribe().with(System.out::println); + // end::pipeline[] + assertThat(out.get()).contains("2", "4", "6"); + } + + @Test + void subscription(SystemOut out) { + Multi multi = Multi.createFrom().item(1); + // tag::subscription[] + Cancellable cancellable = multi + .subscribe().with( + item -> System.out.println(item), + failure -> System.out.println("Failed with " + failure), + () -> System.out.println("Completed")); + // end::subscription[] + assertThat(cancellable).isNotNull(); + assertThat(out.get()).contains("1", "Completed").doesNotContain("Failed"); + } + + @Test + public void creation() { + { + // tag::simple[] + Multi multiFromItems = Multi.createFrom().items(1, 2, 3, 4); + Multi multiFromIterable = Multi.createFrom().iterable(Arrays.asList(1, 2, 3, 4, 5)); + // end::simple[] + assertThat(multiFromItems.collect().asList().await().indefinitely()).containsExactly(1, 2, 3, 4); + assertThat(multiFromIterable.collect().asList().await().indefinitely()).containsExactly(1, 2, 3, 4, 5); + } + + { + // tag::supplier[] + AtomicInteger counter = new AtomicInteger(); + Multi multi = Multi.createFrom().items(() -> + IntStream.range(counter.getAndIncrement(), counter.get() * 2).boxed()); + // end::supplier[] + assertThat(multi.collect().asList().await().indefinitely()).containsExactly(0, 1); + assertThat(multi.collect().asList().await().indefinitely()).containsExactly(1, 2, 3); + assertThat(multi.collect().asList().await().indefinitely()).containsExactly(2, 3, 4, 5); + } + + { + // tag::failed[] + // Pass an exception directly: + Multi failed1 = Multi.createFrom().failure(new Exception("boom")); + + // Pass a supplier called for every subscriber: + Multi failed2 = Multi.createFrom().failure(() -> new Exception("boom")); + // end::failed[] + + assertThatThrownBy(() -> failed1.toUni().await().indefinitely()) + .hasMessageContaining("boom"); + assertThatThrownBy(() -> failed2.toUni().await().indefinitely()) + .hasMessageContaining("boom"); + + } + + { + // tag::empty[] + Multi multi = Multi.createFrom().empty(); + // end::empty[] + assertThat(multi.toUni().await().indefinitely()).isNull(); + } + + { + // tag::emitter[] + Multi multi = Multi.createFrom().emitter(em -> { + em.emit(1); + em.emit(2); + em.emit(3); + em.complete(); + }); + // end::emitter[] + assertThat(multi.collect().asList().await().indefinitely()).containsExactly(1, 2, 3); + } + + { + // tag::ticks[] + Multi ticks = Multi.createFrom().ticks().every(Duration.ofMillis(100)); + // end::ticks[] + BlockingIterable longs = ticks + .select().first(3) + .subscribe().asIterable(); + await().until(() -> longs.stream().count() == 3); + } + + { + // tag::generator[] + Multi sequence = Multi.createFrom().generator(() -> 1, (n, emitter) -> { + int next = n + (n / 2) + 1; + if (n < 50) { + emitter.emit(next); + + } else { + emitter.complete(); + } + return next; + }); + // end::generator[] + assertThat(sequence.collect().asList().await().indefinitely()).containsExactly(2, 4, 7, 11, 17, 26, 40, 61); + } + } + +} diff --git a/content/docs/smallrye-mutiny/main/_examples/java/tutorials/CreatingUniTest.java b/content/docs/smallrye-mutiny/main/_examples/java/tutorials/CreatingUniTest.java new file mode 100644 index 000000000..157e621ee --- /dev/null +++ b/content/docs/smallrye-mutiny/main/_examples/java/tutorials/CreatingUniTest.java @@ -0,0 +1,111 @@ +package tutorials; + +import guides.extension.SystemOut; +import guides.extension.SystemOutCaptureExtension; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.subscription.Cancellable; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.awaitility.Awaitility.await; + +@SuppressWarnings({ "ConstantConditions", "Convert2MethodRef" }) +@ExtendWith(SystemOutCaptureExtension.class) +public class CreatingUniTest { + + @Test + void pipeline(SystemOut out) { + // tag::pipeline[] + Uni.createFrom().item(1) + .onItem().transform(i -> "hello-" + i) + .onItem().delayIt().by(Duration.ofMillis(100)) + .subscribe().with(System.out::println); + // end::pipeline[] + await().untilAsserted(() -> + assertThat(out.get()).contains("hello-1") + ); + } + + @Test + void subscription(SystemOut out) { + Uni uni = Uni.createFrom().item(1); + // tag::subscription[] + Cancellable cancellable = uni + .subscribe().with( + item -> System.out.println(item), + failure -> System.out.println("Failed with " + failure)); + // end::subscription[] + assertThat(cancellable).isNotNull(); + assertThat(out.get()).contains("1").doesNotContain("Failed"); + } + + @Test + public void creation() { + { + // tag::simple[] + Uni uni = Uni.createFrom().item(1); + // end::simple[] + assertThat(uni.await().indefinitely()).isEqualTo(1); + } + + { + // tag::supplier[] + AtomicInteger counter = new AtomicInteger(); + Uni uni = Uni.createFrom().item(() -> counter.getAndIncrement()); + // end::supplier[] + assertThat(uni.await().indefinitely()).isEqualTo(0); + assertThat(uni.await().indefinitely()).isEqualTo(1); + assertThat(uni.await().indefinitely()).isEqualTo(2); + } + + { + // tag::failed[] + // Pass an exception directly: + Uni failed1 = Uni.createFrom().failure(new Exception("boom")); + + // Pass a supplier called for every subscriber: + Uni failed2 = Uni.createFrom().failure(() -> new Exception("boom")); + // end::failed[] + + assertThatThrownBy(() -> failed1.await().indefinitely()) + .hasMessageContaining("boom"); + assertThatThrownBy(() -> failed2.await().indefinitely()) + .hasMessageContaining("boom"); + + } + + { + // tag::null[] + Uni uni = Uni.createFrom().nullItem(); + // end::null[] + assertThat(uni.await().indefinitely()).isNull(); + } + + { + String result = "hello"; + // tag::emitter[] + Uni uni = Uni.createFrom().emitter(em -> { + // When the result is available, emit it + em.complete(result); + }); + // end::emitter[] + assertThat(uni.await().indefinitely()).isEqualTo("hello"); + } + + { + CompletionStage stage = CompletableFuture.completedFuture("hello"); + // tag::cs[] + Uni uni = Uni.createFrom().completionStage(stage); + // end::cs[] + assertThat(uni.await().indefinitely()).isEqualTo("hello"); + } + } + +} diff --git a/content/docs/smallrye-mutiny/main/_examples/java/tutorials/HandlingFailuresTest.java b/content/docs/smallrye-mutiny/main/_examples/java/tutorials/HandlingFailuresTest.java new file mode 100644 index 000000000..15d8e3f1d --- /dev/null +++ b/content/docs/smallrye-mutiny/main/_examples/java/tutorials/HandlingFailuresTest.java @@ -0,0 +1,105 @@ +package tutorials; + +import guides.extension.SystemOut; +import guides.extension.SystemOutCaptureExtension; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@SuppressWarnings("Convert2MethodRef") +@ExtendWith(SystemOutCaptureExtension.class) +public class HandlingFailuresTest { + + @Test + public void testInvoke(SystemOut out) { + Uni uni = Uni.createFrom().failure(new Exception("boom")); + Multi multi = Multi.createFrom().failure(new Exception("boom")); + // tag::invoke[] + Uni u = uni + .onFailure().invoke(failure -> log(failure)); + + Multi m = multi + .onFailure().invoke(failure -> log(failure)); + // end::invoke[] + assertThatThrownBy(() -> u.await().indefinitely()).hasMessageContaining("boom"); + assertThatThrownBy(() -> m.collect().asList() + .await().indefinitely()).hasMessageContaining("boom"); + assertThat(out.get()).contains("boom"); + } + + @Test + public void testTransform(SystemOut out) { + Uni uni = Uni.createFrom().failure(new Exception("boom")); + // tag::transform[] + Uni u = uni + .onFailure().transform(failure -> + new ServiceUnavailableException(failure)); + // end::transform[] + assertThatThrownBy(() -> u.await().indefinitely()) + .hasCauseExactlyInstanceOf(ServiceUnavailableException.class); + } + + @Test + public void testRecoverWithItem() { + Uni uni = Uni.createFrom().failure(new Exception("boom")); + // tag::recover-item[] + Uni u1 = uni + .onFailure().recoverWithItem("hello"); + + Uni u2 = uni + .onFailure().recoverWithItem(f -> getFallback(f)); + // end::recover-item[] + assertThat(u1.await().indefinitely()).isEqualTo("hello"); + assertThat(u2.await().indefinitely()).isEqualTo("hey"); + } + + @Test + public void testCompletionOnFailure() { + Multi multi = Multi.createFrom().failure(new Exception("boom")); + // tag::recover-completion[] + Multi m = multi + .onFailure().recoverWithCompletion(); + // end::recover-completion[] + assertThat(m.collect().asList().await().indefinitely()).isEmpty(); + } + + @Test + public void testSwitch() { + Uni uni = Uni.createFrom().failure(new Exception("boom")); + Multi multi = Multi.createFrom().failure(new Exception("boom")); + // tag::recover-switch[] + Uni u = uni + .onFailure().recoverWithUni(f -> getFallbackUni(f)); + + Multi m = multi + .onFailure().recoverWithMulti(f -> getFallbackMulti(f)); + // end::recover-switch[] + assertThat(u.await().indefinitely()).isEqualTo("hello"); + assertThat(m.collect().asList().await().indefinitely()).containsExactly("hey"); + } + + private Multi getFallbackMulti(Throwable f) { + return Multi.createFrom().item("hey"); + } + + private Uni getFallbackUni(Throwable f) { + return Uni.createFrom().item("hello"); + } + + private String getFallback(Throwable f) { + return "hey"; + } + + private void log(Throwable failure) { + System.out.println(failure.getMessage()); + } + + private static class ServiceUnavailableException extends Throwable { + public ServiceUnavailableException(Throwable ignored) { + } + } +} diff --git a/content/docs/smallrye-mutiny/main/_examples/java/tutorials/ObserveTest.java b/content/docs/smallrye-mutiny/main/_examples/java/tutorials/ObserveTest.java new file mode 100644 index 000000000..c32774f81 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/_examples/java/tutorials/ObserveTest.java @@ -0,0 +1,65 @@ +package tutorials; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import org.junit.jupiter.api.Test; + +import java.time.Duration; + +public class ObserveTest { + + @Test + public void test() { + Uni uni = Uni.createFrom().item("hello"); + Multi multi = Multi.createFrom().items("a", "b", "c"); + // tag::invoke[] + Uni u = uni.onItem() + .invoke(i -> System.out.println("Received item: " + i)); + + Multi m = multi.onItem() + .invoke(i -> System.out.println("Received item: " + i)); + // end::invoke[] + + // tag::call[] + multi + .onItem().call(i -> + Uni.createFrom().voidItem() + .onItem().delayIt().by(Duration.ofSeconds(1) + ) + ); + // end::call[] + + MyResource resource = new MyResource(); + // tag::close[] + multi + .onCompletion().call(() -> resource.close()); + // end::close[] + } + + @Test + public void all() { + Multi multi = Multi.createFrom().items("a", "b", "c"); + // tag::invoke-all[] + multi + .onSubscription() + .invoke(() -> System.out.println("⬇️ Subscribed")) + .onItem() + .invoke(i -> System.out.println("⬇️ Received item: " + i)) + .onFailure() + .invoke(f -> System.out.println("⬇️ Failed with " + f)) + .onCompletion() + .invoke(() -> System.out.println("⬇️ Completed")) + .onCancellation() + .invoke(() -> System.out.println("⬆️ Cancelled")) + .onRequest() + .invoke(l -> System.out.println("⬆️ Requested: " + l)); + // end::invoke-all[] + + } + + static class MyResource { + Uni close() { + return Uni.createFrom().voidItem(); + } + } +} diff --git a/content/docs/smallrye-mutiny/main/_examples/java/tutorials/RetryTest.java b/content/docs/smallrye-mutiny/main/_examples/java/tutorials/RetryTest.java new file mode 100644 index 000000000..be31f63cc --- /dev/null +++ b/content/docs/smallrye-mutiny/main/_examples/java/tutorials/RetryTest.java @@ -0,0 +1,61 @@ +package tutorials; + +import guides.extension.SystemOutCaptureExtension; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.time.Duration; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@SuppressWarnings("Convert2MethodRef") +@ExtendWith(SystemOutCaptureExtension.class) +public class RetryTest { + + @Test + public void testRetryAtMost() { + Uni uni = Uni.createFrom().failure(new Exception("boom")); + Multi multi = Multi.createFrom().failure(new Exception("boom")); + // tag::retry-at-most[] + Uni u = uni + .onFailure().retry().atMost(3); + Multi m = multi + .onFailure().retry().atMost(3); + // end::retry-at-most[] + assertThatThrownBy(() -> u.await().indefinitely()).hasMessageContaining("boom"); + assertThatThrownBy(() -> m.collect().asList() + .await().indefinitely()).hasMessageContaining("boom"); + } + + @Test + public void testRetryWithBackoff() { + Uni uni = Uni.createFrom().failure(new Exception("boom")); + // tag::retry-backoff[] + Uni u = uni + .onFailure().retry() + .withBackOff(Duration.ofMillis(100), Duration.ofSeconds(1)) + .atMost(3); + // end::retry-backoff[] + assertThatThrownBy(() -> u.await().indefinitely()) + .getCause() // Expected exception is wrapped in a java.util.concurrent.CompletionException + .hasMessageContaining("boom") + .hasSuppressedException(new IllegalStateException("Retries exhausted: 3/3")); + } + + @Test + public void testRetryUntil() { + Uni uni = Uni.createFrom().failure(new Exception("boom")); + // tag::retry-until[] + Uni u = uni + .onFailure().retry() + .until(f -> shouldWeRetry(f)); + // end::retry-until[] + assertThatThrownBy(() -> u.await().indefinitely()).hasMessageContaining("boom"); + } + + private boolean shouldWeRetry(Throwable ignored) { + return false; + } +} diff --git a/content/docs/smallrye-mutiny/main/_examples/java/tutorials/TransformItemsAsyncTest.java b/content/docs/smallrye-mutiny/main/_examples/java/tutorials/TransformItemsAsyncTest.java new file mode 100644 index 000000000..b8d0ae490 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/_examples/java/tutorials/TransformItemsAsyncTest.java @@ -0,0 +1,138 @@ +package tutorials; + +import guides.extension.SystemOut; +import guides.extension.SystemOutCaptureExtension; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import static org.assertj.core.api.Assertions.assertThat; + +@SuppressWarnings("Convert2MethodRef") +@ExtendWith(SystemOutCaptureExtension.class) +public class TransformItemsAsyncTest { + + interface MyAsyncService { + // tag::service[] + Uni invokeRemoteGreetingService(String name); + // end::service[] + } + + static class MyAsyncServiceImpl implements MyAsyncService { + + @Override + public Uni invokeRemoteGreetingService(String name) { + return Uni.createFrom().item(name); + } + } + + MyAsyncServiceImpl service = new MyAsyncServiceImpl(); + + + private Uni invokeRemoteGreetingService(String name) { + return service.invokeRemoteGreetingService(name); + } + + @Test + public void testCall() { + + Uni uni = Uni.createFrom().item("mutiny"); + // tag::call[] + Uni result = uni + .onItem().transformToUni(name -> invokeRemoteGreetingService(name)); + // end::call[] + + assertThat(result.await().indefinitely()).isEqualTo("mutiny"); + } + + @Test + public void testChain(SystemOut out) { + // tag::chain[] + Uni uni = Uni.createFrom().item("Cameron"); + uni + .onItem().transformToUni(name -> invokeRemoteGreetingService(name)) + .subscribe().with( + item -> System.out.println(item), // Print "Hello Cameron", + fail -> fail.printStackTrace()); // Print the failure stack trace + // end::chain[] + assertThat(out.get()).contains("Cameron"); + } + + + @Test + public void testUniToMulti(SystemOut out) { + Uni uni = Uni.createFrom().item("Cameron"); + // tag::uni-to-multi[] + Multi result = uni + .onItem().transformToMulti(item -> Multi.createFrom().items(item, item)); + // end::uni-to-multi[] + assertThat(result.collect().asList().await().indefinitely()).containsExactly("Cameron", "Cameron"); + + // tag::uni-to-multi-2[] + uni + .onItem().transformToMulti(item -> Multi.createFrom().items(item, item)) + .subscribe().with( + item -> System.out.println(item)); // Called twice + // end::uni-to-multi-2[] + assertThat(out.get()).contains("Cameron"); + } + + @Test + public void testMergeAndConcatUni() { + Multi multi = Multi.createFrom().items("a", "b"); + + // tag::merge-concat[] + Multi merged = multi + .onItem().transformToUniAndMerge(name -> invokeRemoteGreetingService(name)); + + Multi concat = multi + .onItem().transformToUniAndConcatenate(name -> invokeRemoteGreetingService(name)); + // end::merge-concat[] + + assertThat(merged.collect().asList().await().indefinitely()).containsExactly("a", "b"); + assertThat(concat.collect().asList().await().indefinitely()).containsExactly("a", "b"); + } + + @Test + public void testMergeAndConcatMulti() { + Multi multi = Multi.createFrom().items("a", "b"); + + // tag::merge-concat-multi[] + Multi merged = multi + .onItem().transformToMultiAndMerge(item -> someMulti(item)); + + Multi concat = multi + .onItem().transformToMultiAndConcatenate(item -> someMulti(item)); + // end::merge-concat-multi[] + + assertThat(merged.collect().asList().await().indefinitely()).containsExactly("a", "b"); + assertThat(concat.collect().asList().await().indefinitely()).containsExactly("a", "b"); + } + + @Test + public void testMergeConcurrency() { + Multi multi = Multi.createFrom().items("a", "b"); + + // tag::merge-concurrency[] + Multi limitConcurrent = multi + .onItem().transformToUni(item -> callService(item)) + .merge(2); + + Multi defaultConcurrent = multi + .onItem().transformToUni(item -> callService(item)) + .merge(); + // end::merge-concurrency[] + + assertThat(limitConcurrent.collect().asList().await().indefinitely()).containsExactly("a", "b"); + assertThat(defaultConcurrent.collect().asList().await().indefinitely()).containsExactly("a", "b"); + } + + private Uni callService(String item) { + return Uni.createFrom().item(item); + } + + private Multi someMulti(String item) { + return Multi.createFrom().item(item); + } +} diff --git a/content/docs/smallrye-mutiny/main/_examples/java/tutorials/TransformItemsTest.java b/content/docs/smallrye-mutiny/main/_examples/java/tutorials/TransformItemsTest.java new file mode 100644 index 000000000..e975e56a8 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/_examples/java/tutorials/TransformItemsTest.java @@ -0,0 +1,61 @@ +package tutorials; + +import guides.extension.SystemOut; +import guides.extension.SystemOutCaptureExtension; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import static org.assertj.core.api.Assertions.assertThat; + +@SuppressWarnings("Convert2MethodRef") +@ExtendWith(SystemOutCaptureExtension.class) +public class TransformItemsTest { + + @Test + public void testUniTransform(SystemOut out) { + // tag::uni-transform[] + Uni someUni = Uni.createFrom().item("hello"); + someUni + .onItem().transform(i -> i.toUpperCase()) + .subscribe().with( + item -> System.out.println(item)); // Print HELLO + // end::uni-transform[] + assertThat(out.get()).contains("HELLO"); + } + + @Test + public void testMultiTransform() { + Multi multi = Multi.createFrom().items("a", "b", "c"); + // tag::multi-transform[] + Multi m = multi.onItem().transform(i -> i.toUpperCase()); + // end::multi-transform[] + assertThat(m.collect().asList().await().indefinitely()).containsExactly("A", "B", "C"); + } + + @Test + public void testMultiTransformWithSubscription(SystemOut out) { + // tag::multi-transform-2[] + Multi someMulti = Multi.createFrom().items("a", "b", "c"); + someMulti + .onItem().transform(i -> i.toUpperCase()) + .subscribe().with( + item -> System.out.println(item)); // Print A B C + // end::multi-transform-2[] + assertThat(out.get()).contains("A", "B", "C"); + } + + @Test + public void testChain() { + Uni uni = Uni.createFrom().item("hello"); + // tag::chain[] + Uni u = uni + .onItem().transform(i -> i.toUpperCase()) + .onItem().transform(i -> i + "!"); + // end::chain[] + + assertThat(u.await().indefinitely()).isEqualTo("HELLO!"); + + } +} diff --git a/content/docs/smallrye-mutiny/main/_examples/kotlin/guides/Coroutines.kt b/content/docs/smallrye-mutiny/main/_examples/kotlin/guides/Coroutines.kt new file mode 100644 index 000000000..087e809bb --- /dev/null +++ b/content/docs/smallrye-mutiny/main/_examples/kotlin/guides/Coroutines.kt @@ -0,0 +1,62 @@ +package guides + +import io.smallrye.mutiny.Multi +import io.smallrye.mutiny.Uni +// tag::importStatements[] +import io.smallrye.mutiny.coroutines.asFlow +import io.smallrye.mutiny.coroutines.asMulti +import io.smallrye.mutiny.coroutines.asUni +import io.smallrye.mutiny.coroutines.awaitSuspending +// end::importStatements[] +import io.smallrye.mutiny.coroutines.uni +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf + +suspend fun uniAwaitSuspending() { + // tag::uniAwaitSuspending[] + val uni: Uni = Uni.createFrom().item("Mutiny ❤ Kotlin") + try { + // Available within suspend function and CoroutineScope + val item: String = uni.awaitSuspending() + } catch (failure: Throwable) { + // onFailure event happened + } + // end::uniAwaitSuspending[] +} + +@ExperimentalCoroutinesApi +suspend fun deferredAsUni() { + // tag::deferredAsUni[] + val deferred: Deferred = GlobalScope.async { "Kotlin ❤ Mutiny" } + val uni: Uni = deferred.asUni() + // end::deferredAsUni[] +} + +@ExperimentalCoroutinesApi +suspend fun multiAsFlow() { + // tag::multiAsFlow[] + val multi: Multi = Multi.createFrom().items("Mutiny", "❤", "Kotlin") + val flow: Flow = multi.asFlow() + // end::multiAsFlow[] +} + +suspend fun flowAsMulti() { + // tag::flowAsMulti[] + val flow: Flow = flowOf("Kotlin", "❤", "Mutiny") + val multi: Multi = flow.asMulti() + // end::flowAsMulti[] +} + +suspend fun uniBuilder() { + // tag::uniBuilder[] + // import io.smallrye.mutiny.coroutines.uni + coroutineScope { + val uni: Uni = uni { "λ 🚧" } + } + // end::uniBuilder[] +} diff --git a/content/docs/smallrye-mutiny/main/_examples/kotlin/guides/FailureExt.kt b/content/docs/smallrye-mutiny/main/_examples/kotlin/guides/FailureExt.kt new file mode 100644 index 000000000..69b40368f --- /dev/null +++ b/content/docs/smallrye-mutiny/main/_examples/kotlin/guides/FailureExt.kt @@ -0,0 +1,24 @@ +package guides + +import io.smallrye.mutiny.Multi +import io.smallrye.mutiny.Uni +import io.smallrye.mutiny.onFailure +import java.io.IOException + +fun uniReifiedOnFailure() { + // tag::uniReifiedOnFailure[] + // import io.smallrye.mutiny.onFailure + val uni = Uni.createFrom().failure(IOException("disk full")) + val recovered: Uni = uni.onFailure() + .recoverWithItem("recovered") + // end::uniReifiedOnFailure[] +} + +fun multiReifiedOnFailure() { + // tag::multiReifiedOnFailure[] + // import io.smallrye.mutiny.onFailure + val multi = Multi.createFrom().failure(IOException("disk full")) + val recovered: Multi = multi.onFailure() + .recoverWithItem("recovered") + // end::multiReifiedOnFailure[] +} diff --git a/content/docs/smallrye-mutiny/main/_examples/kotlin/guides/MultiAwait.kt b/content/docs/smallrye-mutiny/main/_examples/kotlin/guides/MultiAwait.kt new file mode 100644 index 000000000..c8b17f6ed --- /dev/null +++ b/content/docs/smallrye-mutiny/main/_examples/kotlin/guides/MultiAwait.kt @@ -0,0 +1,41 @@ +package guides + +import io.smallrye.mutiny.Multi +import io.smallrye.mutiny.coroutines.awaitList +import io.smallrye.mutiny.coroutines.awaitFirst +import io.smallrye.mutiny.coroutines.awaitFirstOrThrow +import io.smallrye.mutiny.coroutines.awaitLast +import io.smallrye.mutiny.coroutines.awaitLastOrThrow +import io.smallrye.mutiny.coroutines.awaitEach + +suspend fun multiAwaitList() { + // tag::multiAwaitList[] + val multi = Multi.createFrom().items("a", "b", "c") + val list: List = multi.awaitList() + // end::multiAwaitList[] +} + +suspend fun multiAwaitFirst() { + // tag::multiAwaitFirst[] + val multi = Multi.createFrom().items(1, 2, 3) + val first: Int? = multi.awaitFirst() + val firstOrThrow: Int = multi.awaitFirstOrThrow() + // end::multiAwaitFirst[] +} + +suspend fun multiAwaitLast() { + // tag::multiAwaitLast[] + val multi = Multi.createFrom().items(1, 2, 3) + val last: Int? = multi.awaitLast() + val lastOrThrow: Int = multi.awaitLastOrThrow() + // end::multiAwaitLast[] +} + +suspend fun multiAwaitEach() { + // tag::multiAwaitEach[] + val multi = Multi.createFrom().items(1, 2, 3) + multi.awaitEach { item -> + println("Processing $item") + } + // end::multiAwaitEach[] +} diff --git a/content/docs/smallrye-mutiny/main/_examples/kotlin/guides/MultiExt.kt b/content/docs/smallrye-mutiny/main/_examples/kotlin/guides/MultiExt.kt new file mode 100644 index 000000000..fca9c651a --- /dev/null +++ b/content/docs/smallrye-mutiny/main/_examples/kotlin/guides/MultiExt.kt @@ -0,0 +1,50 @@ +package guides + +import io.smallrye.mutiny.multi +import io.smallrye.mutiny.subscription.BackPressureStrategy +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.coroutineScope +import io.smallrye.mutiny.coroutines.multi as multiSuspend + +fun regularMultiBuilder() { + // tag::multiBuilder[] + // import io.smallrye.mutiny.multi + val multi = multi { + emit(1) + emit(2) + emit(3) + } + // end::multiBuilder[] +} + +fun multiBuilderWithBackPressure() { + // tag::multiBuilderBackPressure[] + // import io.smallrye.mutiny.multi + val multi = multi(backPressure = BackPressureStrategy.DROP) { + emit(1) + emit(2) + emit(3) + } + // end::multiBuilderBackPressure[] +} + +@ExperimentalCoroutinesApi +suspend fun suspendMultiBuilder() { + // tag::multiBuilderSuspend[] + // import io.smallrye.mutiny.coroutines.multi + val queryResult = fetchResults() + coroutineScope { + val multi = multiSuspend(context = this) { + queryResult.forEach { result -> emit(result.download()) } + } + } + // end::multiBuilderSuspend[] +} + +interface ResultHandle { + suspend fun download(): String +} + +suspend fun fetchResults(): Iterable { + TODO("Not yet implemented") +} diff --git a/content/docs/smallrye-mutiny/main/_examples/kotlin/guides/TupleExt.kt b/content/docs/smallrye-mutiny/main/_examples/kotlin/guides/TupleExt.kt new file mode 100644 index 000000000..ae6e56392 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/_examples/kotlin/guides/TupleExt.kt @@ -0,0 +1,21 @@ +package guides + +import io.smallrye.mutiny.Uni +import io.smallrye.mutiny.component1 +import io.smallrye.mutiny.component2 + +fun tupleDestructuring() { + // tag::tupleDestructuring[] + // import io.smallrye.mutiny.component1 + // import io.smallrye.mutiny.component2 + val uni = Uni.combine().all() + .unis( + Uni.createFrom().item("Alice"), + Uni.createFrom().item(30) + ).asTuple() + + val (name, age) = uni.await().indefinitely() + assert(name == "Alice") + assert(age == 30) + // end::tupleDestructuring[] +} diff --git a/content/docs/smallrye-mutiny/main/_examples/kotlin/guides/UniExt.kt b/content/docs/smallrye-mutiny/main/_examples/kotlin/guides/UniExt.kt new file mode 100644 index 000000000..988ba47dc --- /dev/null +++ b/content/docs/smallrye-mutiny/main/_examples/kotlin/guides/UniExt.kt @@ -0,0 +1,21 @@ +package guides + +import io.smallrye.mutiny.Uni +import io.smallrye.mutiny.replaceWithUnit +import io.smallrye.mutiny.uni + +fun uniReplaceWithUnit() { + val uni: Uni = Uni.createFrom().item("") + // tag::uniReplaceWithUnit[] + // import io.smallrye.mutiny.replaceWithUnit + val unitUni : Uni = uni.replaceWithUnit() + assert(unitUni.await().indefinitely() === Unit) + // end::uniReplaceWithUnit[] +} + +fun regularUniBuilder() { + // tag::uniBuilder[] + // import io.smallrye.mutiny.uni + val uni: Uni = uni { "λ 🚧" } + // end::uniBuilder[] +} diff --git a/content/docs/smallrye-mutiny/main/_nav.yml b/content/docs/smallrye-mutiny/main/_nav.yml new file mode 100644 index 000000000..b8ecc5e64 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/_nav.yml @@ -0,0 +1,69 @@ +title: SmallRye Mutiny +nav: +- page: index +- title: Tutorials + items: + - page: tutorials/getting-mutiny + - page: tutorials/hello-mutiny + - page: tutorials/creating-uni-pipelines + - page: tutorials/creating-multi-pipelines + - page: tutorials/observing-events + - page: tutorials/transforming-items + - page: tutorials/transforming-items-asynchronously + - page: tutorials/handling-failures + - page: tutorials/retrying + - page: tutorials/mutiny-workshop +- title: Guides + items: + - page: guides/imperative-to-reactive + - page: guides/reactive-to-imperative + - page: guides/unchecked-exceptions + - page: guides/filtering-items + - page: guides/collecting-items + - page: guides/grouping-items + - page: guides/take-skip-items + - page: guides/eliminate-duplicates-and-repetitions + - page: guides/handling-null + - page: guides/handling-timeouts + - page: guides/delaying-events + - page: guides/pagination + - page: guides/polling + - page: guides/emission-threads + - page: guides/emit-on-vs-run-subscription-on + - page: guides/completion-stage + - page: guides/rx + - page: guides/shortcut-methods + - page: guides/merging-and-concatenating-streams + - page: guides/combining-items + - page: guides/joining-unis + - page: guides/converters + - page: guides/testing + - page: guides/spies + - page: guides/custom-operators + - page: guides/dropped-exceptions + - page: guides/framework-integration + - page: guides/hot-streams + - page: guides/integrate-a-non-reactive-source + - page: guides/kotlin + - page: guides/logging + - page: guides/context-passing + - page: guides/replaying-multis + - page: guides/controlling-demand + - page: guides/multi-split + - page: guides/branching + - page: guides/broadcasting-to-multiple-subscribers +- title: Reference + items: + - page: reference/migrating-to-mutiny-2 + - page: reference/why-is-asynchronous-important + - page: reference/what-is-reactive-programming + - page: reference/what-makes-mutiny-different + - page: reference/uni-and-multi + - page: reference/going-reactive-a-few-pitfalls + - page: reference/publications + - title: Tags index + page: tags-index +- title: API + url: https://javadoc.io/doc/io.smallrye.reactive/mutiny/latest/index.html +- title: Discussions/Questions + url: https://github.com/smallrye/smallrye-mutiny/discussions diff --git a/content/docs/smallrye-mutiny/main/assets/CF_logo_horizontal_single_reverse.svg b/content/docs/smallrye-mutiny/main/assets/CF_logo_horizontal_single_reverse.svg new file mode 100644 index 000000000..2b8907471 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/assets/CF_logo_horizontal_single_reverse.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/main/assets/logo-white.png b/content/docs/smallrye-mutiny/main/assets/logo-white.png new file mode 100644 index 000000000..4472249bb Binary files /dev/null and b/content/docs/smallrye-mutiny/main/assets/logo-white.png differ diff --git a/content/docs/smallrye-mutiny/main/assets/logo-white.svg b/content/docs/smallrye-mutiny/main/assets/logo-white.svg new file mode 100644 index 000000000..fa27cb24f --- /dev/null +++ b/content/docs/smallrye-mutiny/main/assets/logo-white.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/main/assets/logo.png b/content/docs/smallrye-mutiny/main/assets/logo.png new file mode 100644 index 000000000..f8e4ca32a Binary files /dev/null and b/content/docs/smallrye-mutiny/main/assets/logo.png differ diff --git a/content/docs/smallrye-mutiny/main/guides/branching.adoc b/content/docs/smallrye-mutiny/main/guides/branching.adoc new file mode 100644 index 000000000..9391061fb --- /dev/null +++ b/content/docs/smallrye-mutiny/main/guides/branching.adoc @@ -0,0 +1,57 @@ += How to do branching in a reactive pipeline? +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,intermediate + +Mutiny and similar reactive programming libraries do not have _branching_ operators similar to `if / else` and `switch/case` statements in Java. + +This does not mean that we can't express _branching_ in a reactive pipeline, and the most classic way is to use a transformation to a `Uni` (also called `flatMap` in functional programming). + +== Expressing branches as Uni operations + +Suppose that we have a pipeline where a `Uni` is created from a random value, and suppose that we want to have a different processing pipeline depending on whether the value is odd or even. +Let's have these 2 `Uni`-returning methods to model different behaviors: + +[source,java] +---- +include::{examples-dir}/java/guides/BranchingTest.java[tags=branches,indent=0] +---- + +We can use the `transformToUni` operator to plug either method depending on the random number: + +[source,java] +---- +include::{examples-dir}/java/guides/BranchingTest.java[tags=pipeline,indent=0] +---- + +Having such a mapping function is a common pattern: it has conditional logic and each branch returns a `Uni` that represents the "sub-pipeline" of what each branch shall do. + +Note that such constructs are primarily relevant when asynchronous I/O are involved and that such asynchronous I/O operations are typically `Uni`-returning methods such as those found in the https://smallrye.io/smallrye-mutiny-vertx-bindings/[Mutiny Vert.x bindings]. + +[TIP] +==== + +There are other ways to express the "result" of a branch. +You could wrap results in a custom type or a container like `java.util.Optional`. + +You could also return a failed `Uni`, and later react by continuing with another `Uni`, another value, or retrying (which would model a loop!). + +==== + +== Branching in a Multi + +The case of `Multi` is even more interesting because a `null`-completed `Uni` is discarded from the stream by any of the `transformToUni++{++...}` methods: + +[source,java] +---- +include::{examples-dir}/java/guides/BranchingTest.java[tags=multi-pipeline,indent=0] +---- + +where `drop()` is as follows: + +[source,java] +---- +include::{examples-dir}/java/guides/BranchingTest.java[tags=drop,indent=0] +---- + +Any negative value is discarded in this `Multi` pipeline, while the positive even and odd numbers get forwarded to the subscriber. diff --git a/content/docs/smallrye-mutiny/main/guides/broadcasting-to-multiple-subscribers.adoc b/content/docs/smallrye-mutiny/main/guides/broadcasting-to-multiple-subscribers.adoc new file mode 100644 index 000000000..c0b359329 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/guides/broadcasting-to-multiple-subscribers.adoc @@ -0,0 +1,185 @@ += Broadcasting to multiple subscribers (like server-sent events, websockets, etc) +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,beginner + +It is a common requirement in event-driven applications to have multiple subscribers receiving events from a single events source: + +image::images/diagrams/mermaid-f74f7917dcf7.svg[mermaid diagram] + +A good example would be a periodic events stream where the events get pushed every second to multiple https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events[server-sent event] subscribers. + +Let us see how this can be done using Mutiny and the https://quarkus.io/[Quarkus framework]. + +[NOTE] +==== + +We use Quarkus to illustrate a classic real-world usage for the broadcast operator, but the same kind of construct +could be done with another framework or the https://vertx.io/[Vert.x toolkit]. + +==== + +== Use-case: dynamic subscribers + +We expose a HTTP endpoint for server-sent events, and each second we receive the current list of subscribers. +The endpoint is exposed on path `/hello/ticks/++{++id}` where `id` is an identifier for a subscriber. + +We could subscribe and follow the stream using https://httpie.io/[HTTPie] for subscriber `1`: + +[source,text] +---- +$ http --stream :8080/hello/ticks/1 +HTTP/1.1 200 OK +Content-Type: text/event-stream +X-SSE-Content-Type: text/plain +transfer-encoding: chunked + +data:1 + +data:1 + +data:1 + +data:1,2 + +data:1,2 +---- + +The first 3 ticks have just one subscriber (`data:1`), but then once another subscriber arrives at path `/hello/ticks/2` we get ticks with identifiers `1,2`. + +When the connection for subscriber `1` closes, we see the impact on subscriber `2`: + +[source,text] +---- +http --stream :8080/hello/ticks/2 +HTTP/1.1 200 OK +Content-Type: text/event-stream +X-SSE-Content-Type: text/plain +transfer-encoding: chunked + +data:1,2 + +data:1,2 + +data:2 + +data:2 + +data:2 +---- + +== Why is broadcasting required? + +Mutiny offers a publisher for periodic event streams: + +[source,java] +---- +var ticks = Multi.createFrom().ticks().every(Duration.ofSeconds(1)); +---- + +You might wonder why we couldn't simply build our Quarkus endpoints as in: + +[source,java] +---- +@Path("/hello") +public class GreetingResource { + + @GET + @Path("ticks/{id}") + @RestStreamElementType(MediaType.TEXT_PLAIN) + public Multi ticks(String id) { + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + // (rest of the pipeline...) + ; + } +} +---- + +The problem here is that instead of having a single events source to emit ticks, you will have as many as there are subscribers: + +image::images/diagrams/mermaid-cbd13fb6d19b.svg[mermaid diagram] + +You might be able to articulate some logic around such construct, but you will end up with many scheduled operations on the Quarkus thread pool. + +Even worse: if your source is not periodic ticks emitted by Mutiny but some event source (e.g., a Kafka topic, a message broker queue, etc) then you might add correctness issues (e.g., not all subscribers get all messages). + +== Broadcasting, listening to subscriptions and cancellations + +It is fortunately quite easy to express such construct by sharing a common `Multi`, and broadcasting events to each subscriber. + +Let's start with the HTTP endpoint: + +[source,java] +---- +private ConcurrentHashSet identifiers = new ConcurrentHashSet<>(); + +private Multi ticks = (...); + +@GET +@Path("ticks/{id}") +@RestStreamElementType(MediaType.TEXT_PLAIN) +public Multi ticks(String id) { + Log.info("New client with id " + id); + identifiers.add(id); + return ticks.onCancellation().invoke(() -> { + Log.info("Removing client with id " + id); + identifiers.remove(id); + }); +} +---- + +The `ticks` method registers a new client in `identifiers`, and removes it upon cancellation. +The returned `Multi` pipeline piggy-backs on top of the _shared_ `Multi`, which is built as follows: + +[source,java] +---- +private Multi ticks = Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .onItem().transform(tick -> { + Log.info("tick"); + return identifiers.stream().collect(Collectors.joining(",")); + }) + .onSubscription().invoke(() -> Log.info("Starting to emit ticks")) + .onCancellation().invoke(() -> Log.info("No more ticks")) + .broadcast() + .withCancellationAfterLastSubscriberDeparture() + .toAtLeast(1); +---- + +Here are a few observations. + +[arabic] +. For each periodic tick event, we assemble the current subscribers as a string of the form `"1,2,3"` with `.onItem().transform(...)`. +. We log an event when the periodic event stream starts (see `onSubscription().invoke(...)`). +. We log an event when the periodic event stream stops (see `onCancellation().invoke(...)`). +. We broadcast events to all subscribers, but: +[arabic] +.. there must be at least one subscriber before the stream starts, and +.. the stream is cancelled when the last subscriber departs. + +This construction is quite interesting because we don't emit ticks when there are no subscribers, and we stop it when there are none. + +If you play with such an example then you will see logs similar to these: + +[source,text] +---- +INFO [org.acm.GreetingResource] (vert.x-eventloop-thread-0) New client with id 1 +INFO [org.acm.GreetingResource] (vert.x-eventloop-thread-0) Starting to emit ticks +INFO [org.acm.GreetingResource] (executor-thread-1) tick +INFO [org.acm.GreetingResource] (executor-thread-1) tick +INFO [org.acm.GreetingResource] (vert.x-eventloop-thread-0) Removing client with id 1 +INFO [org.acm.GreetingResource] (vert.x-eventloop-thread-0) No more ticks +INFO [org.acm.GreetingResource] (vert.x-eventloop-thread-1) New client with id 1 +INFO [org.acm.GreetingResource] (vert.x-eventloop-thread-1) Starting to emit ticks +INFO [org.acm.GreetingResource] (executor-thread-1) tick +INFO [org.acm.GreetingResource] (executor-thread-1) tick +INFO [org.acm.GreetingResource] (executor-thread-1) tick +INFO [org.acm.GreetingResource] (vert.x-eventloop-thread-0) New client with id 2 +INFO [org.acm.GreetingResource] (executor-thread-1) tick +INFO [org.acm.GreetingResource] (executor-thread-1) tick +INFO [org.acm.GreetingResource] (vert.x-eventloop-thread-1) Removing client with id 1 +INFO [org.acm.GreetingResource] (executor-thread-1) tick +INFO [org.acm.GreetingResource] (executor-thread-1) tick +INFO [org.acm.GreetingResource] (executor-thread-1) tick +INFO [org.acm.GreetingResource] (vert.x-eventloop-thread-0) Removing client with id 2 +INFO [org.acm.GreetingResource] (vert.x-eventloop-thread-0) No more ticks +---- diff --git a/content/docs/smallrye-mutiny/main/guides/collecting-items.adoc b/content/docs/smallrye-mutiny/main/guides/collecting-items.adoc new file mode 100644 index 000000000..83772a291 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/guides/collecting-items.adoc @@ -0,0 +1,91 @@ += Collecting items from Multi +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,beginner + +There are cases where you want to accumulate the items from a `Multi` and produce a `Uni` as a final result. +This is also called a _reduction_ in functional programming. + +The `Uni` fires its item when the `Multi` completes. +Mutiny provides multiple operators to deal with that scenario. +They are available from the `collect()` group. +For example, you can store the items in a list, emit the list on completion, or use a Java `Collector` to customize the aggregation. + +[CAUTION] +==== + +Don't collect items from infinite streams or you will likely end with an out-of-memory failure! + +==== + +== Collecting items into a list + +One of the most common approaches to collect items is to store them in a list (`Uni++<++List++<++T++>>++`) +It emits the final list when the `Multi` completes. + +image::images/diagrams/mermaid-69aa56febc93.svg[mermaid diagram] + +How to achieve this with Mutiny? + +[source,java] +---- +include::{examples-dir}/java/guides/operators/CollectingItemsTest.java[tags=list,indent=0] +---- + +It's important to note that the returned type is a `Uni`. +It emits the list when the multi completes. + +== Collecting items into a map + +You can also collect the items into a `Map`. +In this case, you need to provide a function to compute the key for each item: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/CollectingItemsTest.java[tags=map,indent=0] +---- + +If the key mapper function returns the same key for multiple items, the last one with that key is stored in the final `Map`. +You can collect items in a _multimap_ to handle items with the same keys. + +== Collecting items into a multimap + +A multimap is a `Map++<++K, List++<++T++>>++.` +In the case of a conflicting key, it stores all the items in a list associated with that key. + +[source,java] +---- +include::{examples-dir}/java/guides/operators/CollectingItemsTest.java[tags=multimap,indent=0] +---- + +== Using a custom accumulator + +You can also use a custom _accumulator_ function: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/CollectingItemsTest.java[tags=accumulator,indent=0] +---- + +The `in` method receives two arguments: + +[arabic] +. a supplier providing the new instance of your collection/container +. the accumulator function + +You can also use a Java `Collector`. +For example, in the next example, count the number of items, and produce the final count as item: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/CollectingItemsTest.java[tags=collector,indent=0] +---- + +== Getting the first and last items + +While they are not strictly speaking collecting items, `collect().first()` and `collect().last()` allow retrieving the first and last item from a `Multi`: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/CollectingItemsTest.java[tags=first,indent=0] +---- diff --git a/content/docs/smallrye-mutiny/main/guides/combining-items.adoc b/content/docs/smallrye-mutiny/main/guides/combining-items.adoc new file mode 100644 index 000000000..05cf2c2b8 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/guides/combining-items.adoc @@ -0,0 +1,107 @@ += Combining items from streams +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,intermediate + +Combining items from various streams is an essential pattern in Reactive Programming. + +It associates the emitted items from multiple streams and emits an _aggregate_. +The downstream receives this _aggregate_ and can handle it smoothly. + +There are plenty of use cases, such as executing two tasks concurrently and waiting for both completions, getting the last items from different streams to build an always up-to-date view, and so on. + +== Combining Unis + +Imagine that you have two asynchronous operations to perform like 2 HTTP requests. +You want to send these requests and be notified when both have completed with their responses ready to be consumed. + +Of course, you could send the first request, wait for the response, and then send the second request. +If both requests are independent, we can do something better: send both concurrently and await for both completions! + +image::images/diagrams/mermaid-c54f60b30f42.svg[mermaid diagram] + +How can you achieve this with Mutiny? + +First, each request is a `Uni`, so we have: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/CombiningItemsTest.java[tags=invocations,indent=0] +---- + +Then, we want to combine both _responses_: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/CombiningItemsTest.java[tags=combination,indent=0] +---- + +This code creates a new `Uni` produced by combining `uniA` and `uniB`. +The responses are aggregated inside a `Tuple`: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/CombiningItemsTest.java[tags=subscription,indent=0] +---- + +The `tuple` aggregates the responses in the same order as the `Uni` sequence. + +If one of the `Uni` fails, so does the combination and you receive the failure: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/CombiningItemsTest.java[tags=invocations,indent=0] +include::{examples-dir}/java/guides/operators/CombiningItemsTest.java[tags=combination,indent=0] +include::{examples-dir}/java/guides/operators/CombiningItemsTest.java[tags=subscription,indent=0] +---- + +Using tuples is convenient but only works if you have less than 10 `Uni` objects. +If you want another structure or deal with 10 `Uni` objects or more then use `combineWith`: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/CombiningItemsTest.java[tags=combined-with,indent=0] +---- + +== Combining Multis + +Combining `Multis` consists of associating items from different stream per _index_: + +image::images/diagrams/mermaid-3d4459e3c6a6.svg[mermaid diagram] + +It associates the first items from the combined streams, then the second items: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/CombiningItemsTest.java[tags=combine-multi,indent=0] +---- + +As for `Uni`, you can aggregate the item into tuples (up to 9 items) or combine with a combinator function: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/CombiningItemsTest.java[tags=combine-multi-with,indent=0] +---- + +If one of the streams fails, the combined stream propagates the failure and stops the emission. +The combined stream completes as soon as one of the observed stream sends the completion event. + +[NOTE] +==== + +If one of the observed streams never emits any item then the combined stream will not emit anything. + +==== + +== Combining the latest items of Multis + +It can be useful to combine multiple `Multi` streams and receive the _latest_ items from each stream on every emission: + +image::images/diagrams/mermaid-f0001b5cde9f.svg[mermaid diagram] + +This is achieved using `latest()`: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/CombiningItemsTest.java[tags=combine-last,indent=0] +---- diff --git a/content/docs/smallrye-mutiny/main/guides/completion-stage.adoc b/content/docs/smallrye-mutiny/main/guides/completion-stage.adoc new file mode 100644 index 000000000..9ad73ee5a --- /dev/null +++ b/content/docs/smallrye-mutiny/main/guides/completion-stage.adoc @@ -0,0 +1,86 @@ += How to deal with CompletionStage? +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,intermediate + +`CompletionStage` and `CompletableFuture` are classes provided by Java to represent asynchronous actions. + +== Differences between Uni and CompletionStage + +While `CompletionStage` and `CompletableFuture` are close to `Uni` in terms of use case, there are some fundamental differences. + +`CompletionStage` are _eager_. +When a method returns a `CompletionStage,` the operation has already been triggered. +The outcome is used to complete the returned `CompletionStage`. +On the other side, `Unis` are lazy. +The operation is only triggered once there is a subscription. + +`CompletionStage` _caches_ the outcome. +So, once received, you can retrieve the result. +Every retrieval will get the same result. +With `Uni`, every subscription has the opportunity to re-trigger the operation and gets a different result. + +[TIP] +==== + +You can also _cache_ the outcome with `Uni.memoize().indefinitely()`. + +==== + +== From Uni to CompletionStage + +You can create a `CompletionStage` from `Uni` using `uni.subscribeAsCompletionStage()`. + +[source,java] +---- +include::{examples-dir}/java/guides/CompletionStageTest.java[tags=uni-subscribe-cs,indent=0] +---- + +It's important to understand that retrieving a `CompletionStage` subscribes to the `Uni`. +If you do this operation twice, it subscribes to the `Uni` twice and re-trigger the operation. + +[source,java] +---- +include::{examples-dir}/java/guides/CompletionStageTest.java[tags=uni-subscribe-cs-twice,indent=0] +---- + +== Creating a Uni from a CompletionStage + +To create a `Uni` from a `CompletionStage`, use `Uni.createFrom().completionStage(...)`. + +[source,java] +---- +include::{examples-dir}/java/guides/CompletionStageTest.java[tags=create-uni,indent=0] +---- + +As you can see, there are two versions. +The first one receives the `CompletionStage` directly, while the second one gets a supplier. +In the case of multiple subscriptions on the produced `Uni`, the supplier is called multiple times (once per subscription), and so can change the return `CompletionStage`. +It also delays the creation of the `CompletionStage` until there is a subscription, which only triggers the operation at that time. +If you pass the instance directly, it will always use the same one (even for multiple subscriptions) and triggers the operation even if there is no subscription. +For these reasons, it is generally better to use the variant accepting a supplier. + +Note that if the completion stage produces a `null` value, the resulting `Uni` emits `null` as item. +If the completion stages complete exceptionally, the failure is emitted by the resulting `Uni`. + +== Creating a Multi from a CompletionStage + +To create a `Multi` from a `CompletionStage`, use `Multi.createFrom().completionStage(...)`. +It produces: + +* a multi emitting an item and completing - if the value produced by the completion stage is not `null`, +* an empty multi if the value produced by the completion stage is `null`, +* a failed multi is completion stage is completed exceptionally. + +[source,java] +---- +include::{examples-dir}/java/guides/CompletionStageTest.java[tags=create-multi,indent=0] +---- + +For the same reason as for `Uni`, there are two versions: + +[arabic] +. one accepting a `CompletionStage` directly +. one accepting a `Supplier++<++CompletionStage++>++`, called at subscription-time, for every subscription. + +It is recommended to use the second version. diff --git a/content/docs/smallrye-mutiny/main/guides/context-passing.adoc b/content/docs/smallrye-mutiny/main/guides/context-passing.adoc new file mode 100644 index 000000000..4dcb0b0a1 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/guides/context-passing.adoc @@ -0,0 +1,90 @@ += Context passing +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,advanced + +Mutiny reactive pipelines let data flow from publishers to subscribers. + +In the vast majority of cases a publisher shall have _all_ required data, and operators shall perform processing based on item values. +For instance a network request shall be made with all request data known in advance, and response processing shall only depend on the response payload. + +That being said there are cases were this is not sufficient, and some data has to be carried along with items. +For instance one intermediary operator in a pipeline may have to make another networked request from which we need to extract some correlation identifier which will be used by another operator down the pipeline. +In such cases one will be tempted to forward tuples consisting of some item value plus some "extra" data. + +For such cases Mutiny offers a _subscriber-provided context_, so all operators involved in a subscription can share some form of _implicit data_. + +== What's in a context? + +A context is a simple key / value, in-memory storage. +Data can be queried, added and deleted from a context, as shown in the following snippet: + +[source,java] +---- +include::{examples-dir}/java/guides/ContextPassingTest.java[tags=contextManipulation,indent=0] +---- + +`Context` objects are thread-safe, and can be created from sequences of key / value pairs (as shown above), from a Java `Map`, or they can be created empty. + +Note that an empty-created context defers its internal storage allocation until the first call to `put`. +You can see `Context` as a glorified `ConcurrentHashMap` delegate, although this is an implementation detail and Mutiny might explore various internal storage strategies in the future. + +[TIP] +==== + +Contexts shall be primarily used to share transient data used for networked I/O processing such as correlation identifiers, tokens, etc. + +They should not be used as general-purpose data structures that are frequently updated and that hold large amounts of data. + +==== + +== How to access a context? + +Given a `Uni` or a `Multi`, a context can be accessed using the `withContext` operator, as in: + +[source,java] +---- +include::{examples-dir}/java/guides/ContextPassingTest.java[tags=contextSampleUsage,indent=0] +---- + +This operator builds a sub-pipeline using 2 parameters: the current `Uni` or `Multi` and the context. + +[IMPORTANT] +==== + +The function passed to `withContext` is called at subscription time. + +This means that the context has not had a chance to be updated by upstream operators yet, so be careful with what you do in the body of that function. + +==== + +There is another way to access the context by using the `attachContext` method: + +[source,java] +---- +include::{examples-dir}/java/guides/ContextPassingTest.java[tags=contextAttachedSampleUsage,indent=0] +---- + +This method materializes the context in the regular pipeline items using the wrapper `ItemWithContext` class. +The `get` method provides the item while the `context` method provides the context. + +== How to access a context at the pipeline source? + +The `Uni` and `Multi` _builder_ methods like `Multi.createFrom()` provide publishers, not operators, so they don't have the `withContext` method. + +The first option is to use the `Uni.createFrom().context(...)` or `Multi.createFrom().context(...)` general purpose method to materialize the context: + +[source,java] +---- +include::{examples-dir}/java/guides/ContextPassingTest.java[tags=builderUsage,indent=0] +---- + +The `context` method takes a function that accepts a `Context` and returns a pipeline. +This is very similar to the `deferred` builder. + +If you use an `emitter` builder then for both `Uni` and `Multi` cases the emitter object offers a `context` method to access the context: + +[source,java] +---- +include::{examples-dir}/java/guides/ContextPassingTest.java[tags=emitterUsage,indent=0] +---- diff --git a/content/docs/smallrye-mutiny/main/guides/controlling-demand.adoc b/content/docs/smallrye-mutiny/main/guides/controlling-demand.adoc new file mode 100644 index 000000000..2b4d098b4 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/guides/controlling-demand.adoc @@ -0,0 +1,158 @@ += Controlling the demand +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,advanced + +== Pacing the demand + +A subscription is used for 2 purposes: cancelling a request and demanding batches of items. + +The `Multi.paceDemand()` operator can be used to automatically issue requests at certain points in time. + +The following example issues requests of 25 items every 100ms: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/ControlDemandTest.java[tags=pacing,indent=0] +---- + +`FixedDemandPacer` is a simple _pacer_ with a fixed demand and a fixed delay. + +You can create more elaborated pacers by implementing the `DemandPacer` interface. +To do so you provide an initial request and a function to evaluate the next request which is evaluated based on the previous request and the number of items emitted since the last request: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/ControlDemandTest.java[tags=custom-pacer,indent=0] +---- + +The previous example is a custom pacer that doubles the demand and increases the delay for each new request. + +== Capping the demand requests + +The `capDemandsTo` and `capDemandUsing` operators can be used to cap the demand from downstream subscribers. + +The `capDemandTo` operator defines a maximum demand that can flow: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/ControlDemandTest.java[tags=capConstant,indent=0] +---- + +Here we cap requests to 50 items, so it takes 2 requests to get all 100 items of the upstream range. +The first request of 75 items is capped to a request of 50 items, leaving an outstanding demand of 25 items. +The second request of 25 items is added to the outstanding demand, resulting in a request of 50 items and completing the stream. + +You can also define a custom function that provides a capping value based on a custom formula, or based on earlier demand observations: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/ControlDemandTest.java[tags=capFunction,indent=0] +---- + +Here we have a function that requests 75% of the downstream requests. + +Note that the function must return a value `n` that satisfies `(0 ++<++ n ++<++= requested)` where `requested` is the downstream demand. + +== Pausing the demand + +The `Multi.pauseDemand()` operator provides fine-grained control over demand propagation in reactive streams. +Unlike cancellation, which terminates the subscription, pausing allows to suspend demand without unsubscribing from the upstream. +This is useful for implementing flow control patterns where item flow needs to be paused based on external conditions. + +=== Basic pausing and resuming + +The `pauseDemand()` operator works with a `DemandPauser` handle that allows to control the stream: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/PausingDemandTest.java[tags=basic,indent=0] +---- + +The `DemandPauser` provides methods to: + +* `pause()`: Stop propagating demand to upstream +* `resume()`: Resume demand propagation and deliver buffered items +* `isPaused()`: Check the current pause state + +Note that a few items may still arrive after pausing due to in-flight requests that were already issued to upstream. + +=== Starting in a paused state + +You can create a stream that starts paused and only begins flowing when explicitly resumed: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/PausingDemandTest.java[tags=initially-paused,indent=0] +---- + +This is useful when you want to prepare a stream but delay its execution until certain conditions are met. + +=== Late subscription + +By default, the upstream subscription happens immediately even when starting paused. +The `lateSubscription()` option delays the upstream subscription until the stream is resumed: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/PausingDemandTest.java[tags=late-subscription,indent=0] +---- + +=== Buffer strategies + +When a stream is paused, the operator stops requesting new items from upstream. +However, items that were already requested (due to downstream demand) may still arrive. +Buffer strategies control what happens to these in-flight items. + +The `pauseDemand()` operator supports three buffer strategies: `BUFFER` (default), `DROP`, and `IGNORE`. +Configuring any other strategy will throw an `IllegalArgumentException`. + +==== BUFFER strategy (default) + +Already-requested items are buffered while paused and delivered when resumed: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/PausingDemandTest.java[tags=buffer-strategy,indent=0] +---- + +You can configure the buffer size: + +* `bufferUnconditionally()`: Unbounded buffer +* `bufferSize(n)`: Buffer up to `n` items, then fail with buffer overflow + +When the buffer overflows, the stream fails with an `IllegalStateException`. + +*Important*: The buffer only holds items that were already requested from upstream before pausing. +When paused, no new requests are issued to upstream, so the buffer size is bounded by the outstanding demand at the time of pausing. + +==== DROP strategy + +Already-requested items are dropped while paused: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/PausingDemandTest.java[tags=drop-strategy,indent=0] +---- + +Items that arrive while paused are discarded, and when resumed, the stream continues requesting fresh items. + +==== IGNORE strategy + +Already-requested items continue to flow downstream while paused. +This strategy doesn't use any buffers. +It only pauses demand from being issued to upstream, but does not pause the flow of already requested items. + +=== Buffer management + +When using the BUFFER strategy, you can inspect and manage the buffer: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/PausingDemandTest.java[tags=buffer-management,indent=0] +---- + +The `DemandPauser` provides: + +* `bufferSize()`: Returns the current number of buffered items +* `clearBuffer()`: Clears the buffer (only works while paused), returns `true` if successful diff --git a/content/docs/smallrye-mutiny/main/guides/converters.adoc b/content/docs/smallrye-mutiny/main/guides/converters.adoc new file mode 100644 index 000000000..313692321 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/guides/converters.adoc @@ -0,0 +1,253 @@ += Using other reactive programming libraries +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,advanced + +You may need to integrate libraries exposing an API using other reactive programming libraries such as RX Java or Reactor. +Mutiny has a built-in conversion mechanism to ease that integration. + +== Picking the right dependency + +You need to add another dependency to access the converters. +Each artifact contains the converters for a specific reactive library. +Pick the right one and add it to your project: + +[.tabs] +==== + +Reactor:: ++ +-- + +[source,xml,subs="attributes+"] +---- + + + io.smallrye.reactive + mutiny-reactor + {versions-mutiny} + +---- + +-- + +RxJava 3:: ++ +-- + +[source,xml,subs="attributes+"] +---- + + + io.smallrye.reactive + mutiny-rxjava3 + {versions-mutiny} + +---- + +-- + +==== + +== Integration with Project Reactor + +https://projectreactor.io/[Project Reactor] is a popular reactive programming library. +It offers two types: `Mono` and `Flux,` both implementing Reactive Stream `Publisher`. + +To use the Reactor `++<++-++>++` Mutiny converter, add the following imports to your class: + +[source,java] +---- +import io.smallrye.mutiny.converters.multi.MultiReactorConverters; +import io.smallrye.mutiny.converters.uni.UniReactorConverters; +---- + +=== Converting a Flux or a Mono into a Multi + +Both `Flux` and `Mono` implement `Publisher`. +As a result, we can use the Reactive Streams interoperability to convert instances from `Flux++<++T++>++` and `Mono++<++T++>++` to `Multi++<++T++>++`: + +[source,java] +---- +include::{examples-dir}/java/guides/integration/ReactorTest.java[tags=reactor-multi-create,indent=0] +---- + +[WARNING] +==== + +Reactor still uses the legacy Reactive Streams APIs instead of `java.util.concurrent.Flow`, so you need to perform an adaptation. + +We recommend using the https://smallrye.io/smallrye-mutiny-zero/[Mutiny Zero Flow Adapters library] as in these examples (Maven coordinates `io.smallrye.reactive:mutiny-zero-flow-adapters`). + +==== + +=== Converting a Flux or a Mono into a Uni + +As you can create `Uni` from a `Publisher`, the same approach can be used to create `Uni` instances: + +[source,java] +---- +include::{examples-dir}/java/guides/integration/ReactorTest.java[tags=reactor-uni-create,indent=0] +---- + +When a `Flux` or `Mono` sends the _completion_ event without having emitted any item, the resulting `Uni` emits `null`. + +When converting a `Flux` to `Uni`, the resulting `Uni` emits the first item. +After that emission, it cancels the subscription to the `Flux`. + +=== Converting a Multi into a Flux or Mono + +Converting a `Multi` into a `Flux` or a `Mono` uses the Reactive Streams interoperability: + +[source,java] +---- +include::{examples-dir}/java/guides/integration/ReactorTest.java[tags=reactor-create-multi,indent=0] +---- + +=== Converting a Uni into a Flux or Mono + +Converting a `Uni` into a `Flux` or a `Mono` requires a converter, as `Uni` does not implement Reactive Streams. + +[source,java] +---- +include::{examples-dir}/java/guides/integration/ReactorTest.java[tags=reactor-create-uni,indent=0] +---- + +If the `Uni` emits `null`, it sends the _completion_ event. + +=== Using converter instead of Reactive Streams + +While Reactive Streams interoperability is convenient, Mutiny also provides converters to create `Flux` and `Mono` from `Uni` and `Multi`: + +[source,java] +---- +include::{examples-dir}/java/guides/integration/ReactorTest.java[tags=uni-export,indent=0] +include::{examples-dir}/java/guides/integration/ReactorTest.java[tags=multi-export,indent=0] +---- + +== Integration with RX Java 3 + +RxJava is another popular reactive programming library. +It offers 5 types: `Completable` (no item), `Single` (one item), `Maybe` (0 or 1 item), `Observable` (multiple items), `Flowable` (multiple items, implements Reactive Stream `Publisher`). + +To use the RxJava `++<++-++>++` Mutiny converters, add the following imports to your class: + +[source,java] +---- +import io.smallrye.mutiny.converters.multi.MultiRx3Converters; +import io.smallrye.mutiny.converters.uni.UniRx3Converters; +---- + +=== Converting an Observable or a Flowable into a Multi + +Both `Observable` and `Flowable` are item streams. +However, `Observable` does not implement `Publisher` and so does not have back-pressure support. + +To create `Multi` from an `Observable,` you need a specific converter: + +[source,java] +---- +include::{examples-dir}/java/guides/integration/RxJavaTest.java[tags=rx-multi-create-observable,indent=0] +---- + +Converting a `Flowable` is easier, as it's a `Publisher`: + +[source,java] +---- +include::{examples-dir}/java/guides/integration/RxJavaTest.java[tags=rx-multi-create-flowable,indent=0] +---- + +[WARNING] +==== + +Like Reactor, RxJava still uses the legacy Reactive Streams APIs instead of `java.util.concurrent.Flow`, so you need to perform an adaptation. + +==== + +=== Converting a Completable, Single or Maybe into a Multi + +To create a `Multi` from a `Completable,` `Single` or `Maybe` you need specific converters, as none of these types implement Reactive Streams. + +[source,java] +---- +include::{examples-dir}/java/guides/integration/RxJavaTest.java[tags=rx-multi-create-single,indent=0] +---- + +* Creating a `Multi` from a `Completable` always produces a `Multi++<++Void++>++` that only emits the _completion_ or _failure_ event. +* Creating a `Multi` from a `Single` produces a `Multi`. That `Multi` emits the item and then completes it. +* Creating a `Multi` from a `Maybe` produces a `Multi`. That `Multi` emits the item (if any) and then completes it. +If the `Maybe` is empty, then the created `Multi` emits the _completion_ event. + +When a `Completable,` `Single,` or `Maybe` emits a failure, then the resulting `Multi` emits that failure. + +=== Converting an Observable or a Flowable into a Uni + +To create a `Uni` from an `Observable,` you need to use a specific converter: + +[source,java] +---- +include::{examples-dir}/java/guides/integration/RxJavaTest.java[tags=rx-uni-create-observable,indent=0] +---- + +The creation from a `Flowable` can be done using the Reactive Streams interoperability: + +[source,java] +---- +include::{examples-dir}/java/guides/integration/RxJavaTest.java[tags=rx-uni-create-flowable,indent=0] +---- + +In both cases, it cancels the subscription to the `Flowable` or `Observable` after receiving the first item. +If the `Flowable` or `Observable` completes without items, the `Uni` emits a `null` item. + +=== Converting a Completable, Single or Maybe into a Uni + +To create a `Uni` from a `Completable,` `Single,` or `Maybe`, you need to use a specific converter: + +[source,java] +---- +include::{examples-dir}/java/guides/integration/RxJavaTest.java[tags=rx-uni-create-single,indent=0] +---- + +Converting a `Completable` to a `Uni` always produces a `Uni++<++Void++>++,` that emits either `null` once the `Completable` completes or the failure if it fails. +The `Maybe` to `Uni` conversion emits a `null` item if the `Maybe` completes without an item. + +=== Converting a Multi into a RX Java objects + +The conversion from a `Multi` to the various RX Java objects is done using converters: + +[source,java] +---- +include::{examples-dir}/java/guides/integration/RxJavaTest.java[tags=create-rx-from-multi,indent=0] +---- + +The creation of a `Completable` from a `Multi` discards all the items emitted by the `Multi`. +It only forwards the _completion_ or _failure_ event. + +Converting a `Multi` into a `Single` returns a `Single++<++Optional++<++T++>>++,` as the `Multi` may complete without items. +You can also produce a `Single++<++T++>++` and emit a _failure_ event if the `Multi` completes without items. +You can configure the thrown exception using `onEmptyThrow.` + +[TIP] +==== + +You can also create a `Flowable` from a `Multi` using: `Flowable.fromPublisher(multi)`. + +==== + +=== Converting a Uni into a RX Java type + +Similarly to the conversion from a `Multi` into an RX Type, converting a `Uni` requires a converter: + +[source,java] +---- +include::{examples-dir}/java/guides/integration/RxJavaTest.java[tags=create-rx-from-uni,indent=0] +---- + +The creation of a `Completable` from a `Uni` discards the item and sends the _completion_ signal after emission. + +Converting a `Uni` into a `Single` returns a `Single++<++Optional++<++T++>>++,` as the `Uni` may emit `null.` +You can also produce a `Single++<++T++>++` and emits a _failure_ event if the `Uni` sends `null.` +Configure the failure to forward using `failOnNull.` + +The creation of a `Maybe,` `Flowable,` or an `Observable` from a `Uni` produces an empty `Maybe,` `Flowable,` or `Observable` if the `Uni` emits `null.` +For `Flowable` and `Observable,` if the `Uni` emits a _non-null_ item, that item is emitted, followed immediately by the _completion_ signal. diff --git a/content/docs/smallrye-mutiny/main/guides/custom-operators.adoc b/content/docs/smallrye-mutiny/main/guides/custom-operators.adoc new file mode 100644 index 000000000..8c2baa638 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/guides/custom-operators.adoc @@ -0,0 +1,30 @@ += Can I have custom operators? +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,advanced + +Yes, but please write operators responsibly! + +Both `Uni` and `Multi` support custom operators using the `plug` operator. +Here is an example where we use a custom `Multi` operator that randomly drops items: + +[source,java] +---- +include::{examples-dir}/java/guides/PlugTest.java[tags=plug,indent=0] +---- + +with the operator defined as follows: + +[source,java] +---- +include::{examples-dir}/java/guides/PlugTest.java[tags=custom-operator,indent=0] +---- + +[CAUTION] +==== + +Custom operators are an advanced feature: when possible please use the existing operators and use helpers such as `stage` to write readable code. + +In the case of custom `Multi` operators it is wise to test them against the _Reactive Streams TCK_. + +==== diff --git a/content/docs/smallrye-mutiny/main/guides/delaying-events.adoc b/content/docs/smallrye-mutiny/main/guides/delaying-events.adoc new file mode 100644 index 000000000..c62577753 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/guides/delaying-events.adoc @@ -0,0 +1,68 @@ += How to delay events? +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,intermediate + +== Delaying Uni's item + +When you have a `Uni`, you can delay the item emission using `onItem().delayIt().by(...)`: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/DelayTest.java[tags=delay-by,indent=0] +---- + +You pass a duration. +When the item is received, it _waits for_ that duration before propagating it to the downstream consumer. + +You can also delay the item's emission based on another _companion_ `Uni`: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/DelayTest.java[tags=delay-until,indent=0] +---- + +The item is propagated downstream when the `Uni` returned by the function emits an item (possibly `null`). +If the function emits a failure (or throws an exception), this failure is propagated downstream. + +== Throttling a Multi + +Multi does not have a _delayIt_ operator because applying the same delay to all items is rarely what you want to do. +However, there are several ways to apply a delay in a `Multi`. + +First, you can use the `onItem().call()`, which delays the emission until the `Uni` produced the `call` emits an item. +For example, the following snippet delays all the items by 10 ms: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/DelayTest.java[tags=delay-multi,indent=0] +---- + +In general, you don't want to apply the same delay to all the items. +You can combine `call` with a random delay as follows: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/DelayTest.java[tags=delay-multi-random,indent=0] +---- + +Finally, you may want to throttle the items. +For example, you can introduce a (minimum) one-second delay between each item. +To achieve this, combine `Multi.createFrom().ticks()` and the multi to throttled: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/DelayTest.java[tags=throttling-multi,indent=0] +---- + +[TIP] +==== + +The `onOverflow().drop()` is used to avoid the _ticks_ to fail if the other stream (`multi`) is too slow. + +==== + +== Delaying other types of events + +We have looked at how to delay items, but you may need to delay other events, such as subscription or failure. +For these, use the `call` approach, and return a `Uni` that delay the event's propagation. diff --git a/content/docs/smallrye-mutiny/main/guides/dropped-exceptions.adoc b/content/docs/smallrye-mutiny/main/guides/dropped-exceptions.adoc new file mode 100644 index 000000000..10d0a1524 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/guides/dropped-exceptions.adoc @@ -0,0 +1,26 @@ += How to deal with dropped exceptions? +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,advanced + +There are a few corner cases where Mutiny cannot propagate an exception to a `Uni` or a `Multi` subscriber. + +Consider the following example: + +[source,java] +---- +include::{examples-dir}/java/guides/infrastructure/DroppedExceptionTest.java[tags=code,indent=0] +---- + +The `onCancellation().call(...)` method is called when the `Uni` subscription is cancelled. +The returned `Uni` is failed with a `IOException`, but since the subscription itself has been cancelled then there is no way to catch the exception. + +By default Mutiny reports such dropped exceptions to the standard error stream along with the corresponding stack trace. +You can change how these exceptions are handled using `Infrastructure.setDroppedExceptionHandler`. + +The following logs dropped exceptions to a logger: + +[source,java] +---- +include::{examples-dir}/java/guides/infrastructure/DroppedExceptionTest.java[tags=override-handler,indent=0] +---- diff --git a/content/docs/smallrye-mutiny/main/guides/eliminate-duplicates-and-repetitions.adoc b/content/docs/smallrye-mutiny/main/guides/eliminate-duplicates-and-repetitions.adoc new file mode 100644 index 000000000..4781db7ef --- /dev/null +++ b/content/docs/smallrye-mutiny/main/guides/eliminate-duplicates-and-repetitions.adoc @@ -0,0 +1,60 @@ += Eliminate duplicates and repetitions +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,beginner + +When observing a `Multi`, you may see duplicated items or repetitions. +The `multi.select()` and `multi.skip()` groups provide methods to only select distinct items or drop repetitions. + +== Selecting distinct + +The `.select().distinct()` operator removes all the duplicates. +As a result, the downstream only contains distinct items: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/RepetitionsTest.java[tags=distinct,indent=0] +---- + +If you have a stream emitting the `++{++1, 1, 2, 3, 4, 5, 5, 6, 1, 4, 4}` items, then applying `.select().distinct()` on such a stream produces: `++{++1, 2, 3, 4, 5, 6}`. + +[IMPORTANT] +==== + +The operator keeps a reference on all the emitted items, and so, it could lead to memory issues if the stream contains too many distinct items. + +==== + +[TIP] +==== + +By default, `select().distinct()` uses the `hashCode` method from the item's class. +You can pass a custom comparator for more advanced checks. + +==== + +If you have a stream emitting items of type `T`, where duplicates can be identified through an attribute of `T` of type `K`, +then an `extractor` of type `Function++<++T, K++>++` can be defined. Applying `.select().distinct(extractor)` on such a stream will +eliminate duplicates but have a lesser memory overhead as only the references to the extracted keys need to be kept, not the whole object. +A typical usage of this might be for a stream of records where uniqueness is determined by a UUID assigned to every record. + +== Skipping repetitions + +The `.skip().repetitions()` operator removes subsequent repetitions of an item: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/RepetitionsTest.java[tags=repetition,indent=0] +---- + +If you have a stream emitting the `++{++1, 1, 2, 3, 4, 5, 5, 6, 1, 4, 4}` items, then applying `.skip().repetitions()` on such a stream produces: `++{++1, 2, 3, 4, 5, 6, 1, 4}`. + +Unlike `.select().distinct()`, you can use this operator on large or infinite streams. + +[TIP] +==== + +By default, `skip().repetitions()` uses the `equals` method from the item's class. +You can pass a custom comparator for more advanced checks. + +==== diff --git a/content/docs/smallrye-mutiny/main/guides/emission-threads.adoc b/content/docs/smallrye-mutiny/main/guides/emission-threads.adoc new file mode 100644 index 000000000..4e3144069 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/guides/emission-threads.adoc @@ -0,0 +1,36 @@ += How to change the emission thread? +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,intermediate + +Except indicated otherwise, Mutiny invokes the next _stage_ using the thread emitting the event from upstream. +So, in the following code, the _transform_ stage is invoked from the thread emitting the event. + +[source,java] +---- +include::{examples-dir}/java/guides/operators/EmitOnTest.java[tags=example,indent=0] +---- + +You can switch to another thread using the `emitOn` operator. +The `emitOn` operator lets you switch the thread used to dispatch (upstream -++>++ downstream) events, so items, failure and completion events. +Just pass the _executor_ you want to use. + +[source,java] +---- +include::{examples-dir}/java/guides/operators/EmitOnTest.java[tags=code,indent=0] +---- + +[NOTE] +==== + +You cannot pass a specific thread, but you can implement a simple `Executor` dispatching on that specific thread, or use a _single threaded executor_. + +==== + +[WARNING] +==== + +Be careful as this operator can lead to concurrency problems with non thread-safe objects such as CDI request-scoped beans. +It might also break reactive-streams semantics with items being emitted concurrently. + +==== diff --git a/content/docs/smallrye-mutiny/main/guides/emit-on-vs-run-subscription-on.adoc b/content/docs/smallrye-mutiny/main/guides/emit-on-vs-run-subscription-on.adoc new file mode 100644 index 000000000..7588f9234 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/guides/emit-on-vs-run-subscription-on.adoc @@ -0,0 +1,43 @@ += What is the difference between emitOn and runSubscriptionOn? +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,intermediate + +The `emitOn` and `runSubscriptionOn` are 2 operators influencing on which threads the event are dispatched. +However, they target different types of events and different directions. + +== The case of emitOn + +`emitOn` takes events coming from upstream (items, completion, failure) and replays them downstream on a thread from the given executor. +Consequently, it affects where the subsequent operators execute (until another `emitOn` is used): + +[source,java] +---- +include::{examples-dir}/java/guides/operators/RunSubscriptionOnTest.java[tags=emitOn,indent=0] +---- + +The previous code produces the following sequence: + +image::images/diagrams/mermaid-18f9a50c0627.svg[mermaid diagram] + +[WARNING] +==== + +Be careful as this operator can lead to concurrency problems with non thread-safe objects such as CDI request-scoped beans. +It might also break reactive-streams semantics with items being emitted concurrently. + +==== + +== The case of runSubscriptionOn + +`runSubscriptionOn` applies to the subscription process. +It requests the upstream to run its subscription (call of the `subscribe` method on its own upstream) on a thread from the given executor: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/RunSubscriptionOnTest.java[tags=runSubscriptionOn,indent=0] +---- + +So, if we consider the previous code snippet, it produces the following sequence: + +image::images/diagrams/mermaid-76df06b35153.svg[mermaid diagram] diff --git a/content/docs/smallrye-mutiny/main/guides/filtering-items.adoc b/content/docs/smallrye-mutiny/main/guides/filtering-items.adoc new file mode 100644 index 000000000..e621770d9 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/guides/filtering-items.adoc @@ -0,0 +1,33 @@ += Filtering items from Multi +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,beginner + +When observing a `Multi`, you may not want to forward all the received items to the downstream. + +Use the `multi.select()` group to select items. + +[source,java] +---- +include::{examples-dir}/java/guides/operators/FilterTest.java[tags=filter,indent=0] +---- + +To _select_ items passing a given predicate, use `multi.select().where(predicate)`: + +`where` accepts a predicate called for each item. +If the predicate returns `true`, the item propagated downstream. +Otherwise, it drops the item. + +The predicate passed to `where` is synchronous. +The `when` method provides an asynchronous version: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/FilterTest.java[tags=test,indent=0] +---- + +`when` accepts a function called for each item. + +Unlike `where` where the predicate returns a boolean synchronously, the function returns a `Uni++<++Boolean++>++`. +It forwards the item downstream if the `uni` produced by the function emits `true`. +Otherwise, it drops the item. diff --git a/content/docs/smallrye-mutiny/main/guides/framework-integration.adoc b/content/docs/smallrye-mutiny/main/guides/framework-integration.adoc new file mode 100644 index 000000000..7dfbb8eb8 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/guides/framework-integration.adoc @@ -0,0 +1,32 @@ += How can I integrate Mutiny with my framework? +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,advanced + +Sometimes, Mutiny needs to execute tasks on other threads, such as monitoring time or delaying actions. +Most operators relying on such capacity let you pass either a `ScheduledExecutorService` or an `ExecutorService`. + +By default, Mutiny uses the a _cached_ thread pool as default executor, that creates new threads as needed, but reuse previously constructed threads when they are available. +A `ScheduledExecutorService` is also created but delegates the execution of the delayed/scheduled tasks to the default executor. + +In the case you want to integrate Mutiny with a thread pool managed by a platform, you can configure it using `Infrastructure.setDefaultExecutor()` method: + +[source,java] +---- +include::{examples-dir}/java/guides/infrastructure/InfrastructureTest.java[tags=infra,indent=0] +---- + +You can configure the default executor using the `Infrastructure.setDefaultExecutor` method: + +[source,java] +---- +include::{examples-dir}/java/guides/infrastructure/InfrastructureTest.java[tags=set-infra,indent=0] +---- + +[TIP] +==== + +If you are using Quarkus, the default executor is already configured to use the Quarkus worker thread pool. +Logging is also configured correctly. + +==== diff --git a/content/docs/smallrye-mutiny/main/guides/grouping-items.adoc b/content/docs/smallrye-mutiny/main/guides/grouping-items.adoc new file mode 100644 index 000000000..de10842de --- /dev/null +++ b/content/docs/smallrye-mutiny/main/guides/grouping-items.adoc @@ -0,0 +1,153 @@ += Grouping items from Multi +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,intermediate + +Mutiny provides several operators to group items from a `Multi` stream. +You can group items by a key function (similar to SQL's `GROUP BY`), split items into fixed-size chunks, or create time-based windows. + +The grouping operators are available from the `group()` method on `Multi`. + +== Grouping into Lists + +The `group().intoLists()` operator allows you to collect items into lists based on size or time. + +=== Fixed-size lists + +Use `group().intoLists().of(size)` to create fixed-size lists from the stream: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/GroupingItemsTest.java[tags=groupIntoLists,indent=0] +---- + +The last list may contain fewer items if the stream doesn't divide evenly. + +=== Time-based lists + +You can create time-based lists using `group().intoLists().every(Duration)`: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/GroupingItemsTest.java[tags=timeBasedListGrouping,indent=0] +---- + +=== Size and time-based lists + +You can combine both size and time constraints using `group().intoLists().of(size, Duration)`: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/GroupingItemsTest.java[tags=sizeAndTimeBasedListGrouping,indent=0] +---- + +This will emit a list when either the size limit is reached or the duration expires, whichever comes first. + +== Grouping into Multi Streams + +The `group().intoMultis()` operator allows you to create separate `Multi` streams from your data. Unlike `intoLists()` which materializes all items into memory, `intoMultis()` keeps items as streams, which is better for: + +* Applying stream transformations to each group +* Processing large groups without loading everything into memory +* Composing with other reactive operators + +=== Fixed-size Multi streams + +Use `group().intoMultis().of(size)` to create `Multi` streams of a fixed size: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/GroupingItemsTest.java[tags=groupIntoMultis,indent=0] +---- + +=== Time-based Multi streams + +You can create time-based windows using `group().intoMultis().every(Duration)`: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/GroupingItemsTest.java[tags=timeBasedGrouping,indent=0] +---- + +== Grouping by a key function + +The `group().by()` operator groups items based on a key function, emitting a `Multi++<++GroupedMulti++<++K, V++>>++` where each `GroupedMulti` represents a group of items sharing the same key. + +[source,java] +---- +include::{examples-dir}/java/guides/operators/GroupingItemsTest.java[tags=groupByKey,indent=0] +---- + +Each `GroupedMulti` has a `key()` method that returns the key for that group. +Items are distributed to groups based on the key function result. + +=== Using both key and value mappers + +You can transform items while grouping them by providing both a key mapper and a value mapper: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/GroupingItemsTest.java[tags=groupByKeyAndValue,indent=0] +---- + +== Processing groups with merge vs concatenate + +When processing groups, you need to decide how to combine the results back into a single stream. Similar to xref:tutorials/transforming-items-asynchronously.adoc[transforming items asynchronously], you can use either *merge* or *concatenate*: + +=== Using merge + +With merge, groups are processed concurrently - items from different groups can interleave in the output stream: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/GroupingItemsTest.java[tags=groupByWithMerge,indent=0] +---- + +[WARNING] +.Upstream request starvation with merge +==== + +When using `.merge(concurrency)` or similar merge operations after `group().by()`, +*the concurrency parameter must be greater than or equal to the number of groups that are created but not terminated*. + +If you create more groups than the concurrency limit allows, some groups cannot make progress while waiting for others to complete. +This leads to a *request starvation* where the upstream won't receive requests for emitting new items. + +==== + +==== Example causing request starvation + +[source,java] +---- +include::{examples-dir}/java/guides/operators/GroupingItemsTest.java[tags=groupByDeadlock,indent=0] +---- + +In this example, 10 groups are created but only 2 can be processed concurrently. +Groups 3-10 cannot make progress because the downstream subscriber is busy with groups 1-2. +Meanwhile, groups 1-2 may not complete because they're waiting for backpressure signals from the full pipeline. +The problem is even more exacerbated with infinite streams and infinite groups. + +==== How to avoid request starvation + +[arabic] +. *Set concurrency ++>++= number of groups*: If you know the maximum number of groups in advance, set the concurrency parameter to at least that number using `.merge(n)` +. *Use unbounded concurrency*: Call `.merge(Integer.MAX++_++VALUE)` to allow unlimited number of concurrent groups +. *Use concatenate instead*: Process groups sequentially (see below) + +=== Using concatenate + +With concatenate, groups are processed sequentially - each group must fully terminate before the next group can start processing: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/GroupingItemsTest.java[tags=groupByWithConcatenate,indent=0] +---- + +== Choosing between group().by() and split() + +Mutiny provides both `group().by()` and `split()` operators. Here's when to use each: + +* *Use `group().by()`* when you don't know the keys in advance and the number of groups is dynamic. +* *Use `split()`* when you know all possible keys upfront (defined by an enum) and you want individual `Multi` instances for each split. + +See the xref:guides/multi-split.adoc[splitting guide] for more details on `split()`. diff --git a/content/docs/smallrye-mutiny/main/guides/handling-null.adoc b/content/docs/smallrye-mutiny/main/guides/handling-null.adoc new file mode 100644 index 000000000..fa3abd3dc --- /dev/null +++ b/content/docs/smallrye-mutiny/main/guides/handling-null.adoc @@ -0,0 +1,40 @@ += How to handle null? +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,beginner + +The `Uni` type can emit `null` as item. + +While there are mixed feelings about `null`, it's part of the Java language and so handled in the `Uni` type. + +[IMPORTANT] +==== + +`Multi` does not support `null` items as it would break the compatibility with the _Reactive Streams_ protocol. + +==== + +Emitting `null` is convenient when returning `Uni++<++Void++>++`. +However, the downstream must expect `null` as item. + +Thus, `Uni` provides specific methods to handle `null` item. +`uni.onItem().ifNull()` lets you decide what you want to do when the received item is `null`: + +[source,java] +---- +include::{examples-dir}/java/guides/UniNullTest.java[tags=code,indent=0] +---- + +A symmetric group of methods is also available with `ifNotNull` which let you handle the case where the item is _not null_: + +[source,java] +---- +include::{examples-dir}/java/guides/UniNullTest.java[tags=code-not-null,indent=0] +---- + +[IMPORTANT] +==== + +While supported, emitting `null` should be avoided except for `Uni++<++Void++>++`. + +==== diff --git a/content/docs/smallrye-mutiny/main/guides/handling-timeouts.adoc b/content/docs/smallrye-mutiny/main/guides/handling-timeouts.adoc new file mode 100644 index 000000000..a687b0a37 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/guides/handling-timeouts.adoc @@ -0,0 +1,53 @@ += How to handle timeouts? +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,intermediate + +Unis are often used to represent asynchronous operations, like making an HTTP call. +So, it's not rare to need to add a timeout or a deadline on this kind of operation. +If we don't get a response (receive an item in the Mutiny lingo) before that deadline, we consider that the operation failed. + +We can then recover from this failure by using a fallback value, retrying, or any other failure handling strategy. + +To configure a timeout use `Uni.ifNoItem().after(Duration)`: + +[source,java] +---- +include::{examples-dir}/java/guides/UniTimeoutTest.java[tags=code,indent=0] +---- + +When the deadline is reached, you can do various actions. +First you can simply fail: + +[source,java] +---- +include::{examples-dir}/java/guides/UniTimeoutTest.java[tags=fail,indent=0] +---- + +A `TimeoutException` is propagated in this case. +So you can handle it specifically in the downstream: + +[source,java] +---- +include::{examples-dir}/java/guides/UniTimeoutTest.java[tags=fail-recover,indent=0] +---- + +You can also pass a custom exception: + +[source,java] +---- +include::{examples-dir}/java/guides/UniTimeoutTest.java[tags=fail-with,indent=0] +---- + +Failing and recovering might be inconvenient. +So, you can pass a fallback item or `Uni` directly: + +[source,java] +---- +include::{examples-dir}/java/guides/UniTimeoutTest.java[tags=fallback,indent=0] +---- + +[source,java] +---- +include::{examples-dir}/java/guides/UniTimeoutTest.java[tags=fallback-uni,indent=0] +---- diff --git a/content/docs/smallrye-mutiny/main/guides/hot-streams.adoc b/content/docs/smallrye-mutiny/main/guides/hot-streams.adoc new file mode 100644 index 000000000..87c93ce06 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/guides/hot-streams.adoc @@ -0,0 +1,26 @@ += Hot streams +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,advanced + +In a _cold_ stream, the stream is created when one subscriber subscribes to the stream. +So, if no one subscribes, the actual stream is not created, saving resources (that would be wasted because nobody is interested in the items). + +In a _hot_ stream, the stream exists before subscribers subscribe. +The stream emits items even if no subscribers observe the stream. +If there are no subscribers, the items are just dropped. +Subscribers only get items emitted after their subscription, meaning that any previous items would not be received. + +To create a hot stream, you can use `io.smallrye.mutiny.operators.multi.processors.BroadcastProcessor` that: + +* drops items if no subscribers are present, +* forwards items to the set of observing subscribers. + +[source,java] +---- +include::{examples-dir}/java/guides/operators/BroadcastProcessorTest.java[tags=code,indent=0] +---- + +Note that the `BroadcastProcessor` subscribes to the _hot_ source aggressively and without back-pressure. +However, the `BroadcastProcessor` enforces the back-pressure protocol per subscriber. +If a subscriber is not ready to handle an item emitted by the _hot_ source, an `io.smallrye.mutiny.subscription.BackPressureFailure` is forwarded to this subscriber. diff --git a/content/docs/smallrye-mutiny/main/guides/imperative-to-reactive.adoc b/content/docs/smallrye-mutiny/main/guides/imperative-to-reactive.adoc new file mode 100644 index 000000000..535317c49 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/guides/imperative-to-reactive.adoc @@ -0,0 +1,71 @@ += From imperative to reactive +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,advanced + +If you use Mutiny, there is a good chance you may want to avoid blocking the caller thread. + +In a _pure_ reactive application, the application logic is executed on one of the few I/O threads, and blocking one of these would have dramatic consequences. +So, here is the big question: _how do you deal with blocking code?_ + +Let's imagine you have blocking code (e.g., connecting to a database using JDBC, reading a file from the file system...), and you want to integrate that into your reactive pipelines while avoiding blocking. +You would need to isolate such blocking parts of your code and run these parts on worker threads. + +Mutiny provides two operators to customize the threads used to handle events: + +* `runSubscriptionOn` - to configure the thread used to execute the code happening at subscription-time +* `emitOn` - to configure the thread used to dispatch events downstream + +== Running blocking code on subscription + +It is very usual to deal with the blocking call during the subscription. +In this case, the `runSubscription` operator is what you need: + +[source,java] +---- +include::{examples-dir}/java/guides/integration/ImperativeToReactiveTest.java[tags=uni-runSubscriptionOn,indent=0] +---- + +The code above creates a Uni that will supply the item using a blocking call, here the `invokeRemoteServiceUsingBlockingIO` method. +To avoid blocking the subscriber thread, it uses `runSubscriptionOn` which switches the thread and call `invokeRemoteServiceUsingBlockingIO` on another thread. +Here we pass the default worker thread pool, but you can use your own executor. + +[TIP] +==== + +What's that default worker pool? + +In the previous snippet, you may wonder about `Infrastructure.getDefaultWorkerPool()`. +Mutiny allows the underlying platform to provide a default worker pool. +`Infrastructure.getDefaultWorkerPool()` provides access to this pool. + +==== + +If the underlying platform does not provide a pool, a default one is used. + +Note that `runSubscriptionOn` does not subscribe to the Uni. +It specifies the executor to use when a subscription happens. + +While the snippet above uses `Uni`, you can also use `runSubscriptionOn` on a `Multi`. + +== Executing blocking calls on event + +Using `runSubscriptionOn` works when the blocking operation happens at subscription time. +But, when dealing with `Multi` and need to execute blocking operations for each item, you need to use `emitOn`. + +While `runSubscriptionOn` runs the subscription on the given executor, `emitOn` configures the executor used to propagate downstream the items, failure and completion events: + +[source,java] +---- +include::{examples-dir}/java/guides/integration/ImperativeToReactiveTest.java[tags=multi-emitOn,indent=0] +---- + +`emitOn` is also available on `Uni`. + +[WARNING] +==== + +Be careful as this operator can lead to concurrency problems with non thread-safe objects such as CDI request-scoped beans. +It might also break reactive-streams semantics with items being emitted concurrently. + +==== diff --git a/content/docs/smallrye-mutiny/main/guides/integrate-a-non-reactive-source.adoc b/content/docs/smallrye-mutiny/main/guides/integrate-a-non-reactive-source.adoc new file mode 100644 index 000000000..0b0a7982c --- /dev/null +++ b/content/docs/smallrye-mutiny/main/guides/integrate-a-non-reactive-source.adoc @@ -0,0 +1,20 @@ += How can I create a Multi from a non-reactive source? +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,advanced + +The `UnicastProcessor` is an implementation of `Multi` that lets you enqueue items in a queue. + +The items are then dispatched to the subscriber using the request protocol. +While this pattern is against the idea of back-pressure, it lets you connect sources of data that do not support back-pressure with your subscriber. + +In the following example, the `UnicastProcessor` is used by a thread emitting items. +These items are enqueued in the processor and replayed when the subscriber is connected, following the request protocol. + +[source,java] +---- +include::{examples-dir}/java/guides/operators/UnicastProcessorTest.java[tags=code,indent=0] +---- + +By default, the `UnicastProcessor` uses an unbounded queue. +You can also pass a fixed size queue that would reject the items once full. diff --git a/content/docs/smallrye-mutiny/main/guides/joining-unis.adoc b/content/docs/smallrye-mutiny/main/guides/joining-unis.adoc new file mode 100644 index 000000000..eaaa1a204 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/guides/joining-unis.adoc @@ -0,0 +1,67 @@ += Joining several unis +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,intermediate + +A `Uni` represents an operation that either emits a value or a failure. +Examples of operations that fit into a `Uni` include: HTTP client requests, database `insert` queries, sending messages to a broker, etc. + +It is common to trigger several _concurrent_ operations, then _join_ on the results. +For instance you can make HTTP requests to 3 different HTTP APIs, then collect all HTTP responses. +Or you can just take the response from the one who was the fastest. + +`Uni` offers the `join` group to assemble all results from a list of `Uni`, pick the first one that terminates, or pick the first one that terminates with a value. + +== Joining multiple unis + +Given multiple `Uni`, you can join them all and obtain a `Uni` that emits a list of values: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/UniJoinTest.java[tags=join-all,indent=0] +---- + +The assembled values are in the same order as the list of unis. +The last call to `.andCollectFailures()` specifies that if one or several `Uni` fail, then the failures are assembled in a `CompositeException`. + +Sometimes you just want to _fail fast_ if any of the `Uni` fails, and not wait for all unis to terminate: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/UniJoinTest.java[tags=join-all-ff,indent=0] +---- + +When any `Uni` fails, then the failure is directly forwarded as a failure of `res`. + +== Joining on the first Uni + +In some cases you do not want to have all the results but just that of the first `Uni` to respond. +There are actually 2 different cases, depending on whether you want the result of the first `Uni` that emits a value, or just the result of the first `Uni` to terminate. + +If you want to get the first `Uni` that terminates: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/UniJoinTest.java[tags=join-first,indent=0] +---- + +If you want to have the first `Uni` that emits a value (and forget the first failures), then: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/UniJoinTest.java[tags=join-first-withitem,indent=0] +---- + +When all unis fail then `res` fails with a `CompositeException` that reports all failures. + +== Using a builder object + +There are situations where it can be more convenient to gather the unis to join in an iterative fashion. +For this purpose you can use a builder object, as in: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/UniJoinTest.java[tags=builder,indent=0] +---- + +The builder offers `joinAll()` and `joinFirst()` methods. diff --git a/content/docs/smallrye-mutiny/main/guides/kotlin.adoc b/content/docs/smallrye-mutiny/main/guides/kotlin.adoc new file mode 100644 index 000000000..06fd1b4e3 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/guides/kotlin.adoc @@ -0,0 +1,205 @@ += Kotlin integration +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,intermediate + +The module `mutiny-kotlin` provides an integration with Kotlin for use with coroutines and convenient language features. + +There are extension methods available for converting between Mutiny and Kotlin (coroutine) types. +For implementation details please have also a look to these methods' documentation. + +== Dependency coordinates + +The coroutine extension functions are shipped in the package `io.smallrye.mutiny.coroutines`. + +[source,kotlin] +---- +include::{examples-dir}/kotlin/guides/Coroutines.kt[tags=importStatements,indent=0] +---- + +You need to add the following dependency to your project: + +[.tabs] +==== + +Maven:: ++ +-- + +[source,xml,subs="attributes+"] +---- + + io.smallrye.reactive + mutiny-kotlin + {versions-mutiny} + +---- + +-- + +Gradle (Kotlin):: ++ +-- + +[source,kotlin,subs="attributes+"] +---- +implementation("io.smallrye.reactive:mutiny-kotlin:{versions-mutiny}") +---- + +-- + +Gradle (Groovy):: ++ +-- + +[source,groovy,subs="attributes+"] +---- +implementation "io.smallrye.reactive:mutiny-kotlin:{versions-mutiny}" +---- + +-- + +==== + +== Awaiting a Uni in coroutines + +Within a coroutine or suspend function you can easily await Uni events in a suspended way: + +[source,kotlin] +---- +include::{examples-dir}/kotlin/guides/Coroutines.kt[tags=uniAwaitSuspending,indent=0] +---- + +== Processing a Multi as Flow + +The coroutine `Flow` type matches `Multi` semantically, even though it isn't a feature complete reactive streams implementation. +You can process a `Multi` as `Flow` as follows: + +[source,kotlin] +---- +include::{examples-dir}/kotlin/guides/Coroutines.kt[tags=multiAsFlow,indent=0] +---- + +[NOTE] +==== + +There's no flow control availabe for Kotlin's `Flow`. Published items are buffered for consumption using a coroutine `Channel`. +The buffer size and overflow strategy of that `Channel` can be configured using optional arguments: +`Multi.asFlow(bufferCapacity = Channel.UNLIMITED, bufferOverflowStrategy = BufferOverflow.SUSPEND)`, +for more details please consult the method documentation. + +==== + +== Providing a Deferred value as Uni + +The other way around is also possible, let a Deferred become a Uni: + +[source,kotlin] +---- +include::{examples-dir}/kotlin/guides/Coroutines.kt[tags=deferredAsUni,indent=0] +---- + +== Creating a Multi from a Flow + +Finally, creating a Multi from a Flow is also possible: + +[source,kotlin] +---- +include::{examples-dir}/kotlin/guides/Coroutines.kt[tags=flowAsMulti,indent=0] +---- + +== Awaiting Multi results in coroutines + +You can consume `Multi` results directly from suspend functions without converting to `Flow` first: + +[source,kotlin] +---- +include::{examples-dir}/kotlin/guides/MultiAwait.kt[tags=multiAwaitList,indent=0] +---- + +[source,kotlin] +---- +include::{examples-dir}/kotlin/guides/MultiAwait.kt[tags=multiAwaitFirst,indent=0] +---- + +[source,kotlin] +---- +include::{examples-dir}/kotlin/guides/MultiAwait.kt[tags=multiAwaitLast,indent=0] +---- + +[source,kotlin] +---- +include::{examples-dir}/kotlin/guides/MultiAwait.kt[tags=multiAwaitEach,indent=0] +---- + +== Language convenience + +=== Unit instead of Void (null) value + +Kotlin has a special value type `Unit` similar to Java's `Void`. +While regular `Uni++<++Void++>++` holds a `null` item, you can get a `Unit` by using the extension function `replaceWithUnit()`: + +[source,kotlin] +---- +include::{examples-dir}/kotlin/guides/UniExt.kt[tags=uniReplaceWithUnit,indent=0] +---- + +=== Uni builder + +Building a `Uni` from Kotlin code can easily be achieved using the following builders available as regular or coroutine variant: + +[source,kotlin] +---- +include::{examples-dir}/kotlin/guides/UniExt.kt[tags=uniBuilder,indent=0] +---- + +[source,kotlin] +---- +include::{examples-dir}/kotlin/guides/Coroutines.kt[tags=uniBuilder,indent=0] +---- + +=== Multi builder + +Building a `Multi` from Kotlin code can be achieved using the `multi` builder, available as regular or coroutine variant: + +[source,kotlin] +---- +include::{examples-dir}/kotlin/guides/MultiExt.kt[tags=multiBuilder,indent=0] +---- + +The builder accepts an optional back-pressure strategy: + +[source,kotlin] +---- +include::{examples-dir}/kotlin/guides/MultiExt.kt[tags=multiBuilderBackPressure,indent=0] +---- + +A coroutine variant allows suspend calls within the builder: + +[source,kotlin] +---- +include::{examples-dir}/kotlin/guides/MultiExt.kt[tags=multiBuilderSuspend,indent=0] +---- + +=== Tuple destructuring + +Mutiny's `Tuple2` through `Tuple9` support Kotlin destructuring declarations: + +[source,kotlin] +---- +include::{examples-dir}/kotlin/guides/TupleExt.kt[tags=tupleDestructuring,indent=0] +---- + +=== Typed failure handling + +Use reified generics for concise failure type matching on both `Uni` and `Multi`: + +[source,kotlin] +---- +include::{examples-dir}/kotlin/guides/FailureExt.kt[tags=uniReifiedOnFailure,indent=0] +---- + +[source,kotlin] +---- +include::{examples-dir}/kotlin/guides/FailureExt.kt[tags=multiReifiedOnFailure,indent=0] +---- diff --git a/content/docs/smallrye-mutiny/main/guides/logging.adoc b/content/docs/smallrye-mutiny/main/guides/logging.adoc new file mode 100644 index 000000000..4135995b1 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/guides/logging.adoc @@ -0,0 +1,66 @@ += Logging events +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,beginner + +Both `Uni` and `Multi` offer a `log` operator that can be used to trace events as they flow through operators. + +Mutiny does not make any assumption on _how_ logging is defined, and does not rely on any specific logging API. + +== Using a logging operator + +The `log` method comes in 2 forms: one that takes an identifier and one that derives the identifier from the upstream class: + +[source,java] +---- +include::{examples-dir}/java/guides/infrastructure/OperatorLoggingTest.java[tags=log,indent=0] +---- + +Here the `log` operator traces all events between the `onItem().transform(...)` operator and the subscriber, as in the following output: + +.... +11:01:48.709 [main] INFO Multi.MultiMapOp.0 - onSubscription() +11:01:48.711 [main] INFO Multi.MultiMapOp.0 - request(9223372036854775807) +11:01:48.711 [main] INFO Multi.MultiMapOp.0 - onItem(10) +>>> 10 +11:01:48.711 [main] INFO Multi.MultiMapOp.0 - onItem(20) +>>> 20 +11:01:48.711 [main] INFO Multi.MultiMapOp.0 - onItem(30) +>>> 30 +11:01:48.711 [main] INFO Multi.MultiMapOp.0 - onCompletion() +.... + +There are a few things to note here: + +[arabic] +. we are logging on a `Multi`, so the logging event is prefixed with `Multi` (and `Uni` in the case of a... `Uni`), and +. since we did not specify any identifier in the `log` method call, `MultiMapOp` has been derived from the preceding operator (non-qualified) class name, and +. since there can be multiple subscriptions an integer is appended to the identifier (`0`, `1`, `2`, ...). + +== Defining logging + +What happens when events are being logged is defined with the `Infrastructure` class. +Events are written by default to the standard console output in a format similar to: + +.... +[--> Multi.MultiMapOp.0 | onSubscription() +[--> Multi.MultiMapOp.0 | request(9223372036854775807) +[--> Multi.MultiMapOp.0 | onItem(10) +[--> Multi.MultiMapOp.0 | onItem(20) +[--> Multi.MultiMapOp.0 | onItem(30) +[--> Multi.MultiMapOp.0 | onCompletion() +.... + +The following is an example of configuring logging with http://www.slf4j.org++[++SLF4J++]++: + +[source,java] +---- +include::{examples-dir}/java/guides/infrastructure/OperatorLoggingTest.java[tags=set-logger,indent=0] +---- + +[TIP] +==== + +Note that this is only useful to do when embedding Mutiny in your own stack, some frameworks like https://quarkus.io[Quarkus] will already have defined the correct logging strategy. + +==== diff --git a/content/docs/smallrye-mutiny/main/guides/merging-and-concatenating-streams.adoc b/content/docs/smallrye-mutiny/main/guides/merging-and-concatenating-streams.adoc new file mode 100644 index 000000000..43d1277b3 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/guides/merging-and-concatenating-streams.adoc @@ -0,0 +1,87 @@ += Merging and Concatenating Streams +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,intermediate + +Merging or concatenating streams is a frequent operation which consists in taking multiple streams and creating a new `Multi` out of them. +Such an operation observes the items emitted by the different streams and produces a new `Multi` emitting the events. + +All the streams merged or concatenated this way should emit the same type of items. + +== The difference between merge and concatenate + +Understanding the difference between _merge_ and _concatenate_ is essential. + +When _merging_ streams, it observes the different upstreams and emits the items as they come. +If the streams emit their items concurrently, the items from the different streams are interleaved. + +image::images/diagrams/mermaid-74e1ac453bf9.svg[mermaid diagram] + +When using _merge_, failures are also propagated to the merged stream, and no more items are emitted after that failure. +The _completion_ event is only emitted by the merged stream when all the observed streams are completed. + +But if we want to keep the order of the observed stream, we need to _concatenate_. + +When _concatenating_, it waits for the first stream to complete before subscribing to the second one. Thus, it ensures that all the items from the first stream have been emitted before emitting the second stream items. It preserves an order corresponding to the source: + +image::images/diagrams/mermaid-c6f9029821c4.svg[mermaid diagram] + +When the first stream emits the completion event, it switches to the second stream, and so on. +When the last stream completes, the concatenated stream sends the completion event. +As for _merge_, if a stream fails then there won't be further events. + +== Merging Multis + +To create a new `Multi` from the _merge_ of multiple `Multi` streams use: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/MergeConcatTest.java[tags=merge,indent=0] +---- + +For example, we can merge multiple streams emitting periodical events: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/MergeConcatTest.java[tags=merge-ticks,indent=0] +---- + +and the output would be similar to: + +[source,text] +---- +Got item: Stream 1 - 0 +Got item: Stream 2 - 0 +Got item: Stream 3 - 0 +Got item: Stream 3 - 1 +Got item: Stream 1 - 1 +Got item: Stream 3 - 2 +Got item: Stream 2 - 1 +Got item: Stream 3 - 3 +Got item: Stream 1 - 2 +Got item: Stream 3 - 4 +Got item: Stream 3 - 5 +---- + +== Concatenating Multis + +To create a new `Multi` from the _concatenation_ of multiple `Multi` streams use: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/MergeConcatTest.java[tags=concat,indent=0] +---- + +Don't forget that the streams order matters in this case, as `(streamA, streamB)` does not provide the same result as `(streamB, streamA)`: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/MergeConcatTest.java[tags=concatenate-strings,indent=0] +---- + +[IMPORTANT] +==== + +If one of the concatenated streams is unbounded (infinite), the next streams in the list won't be consumed! + +==== diff --git a/content/docs/smallrye-mutiny/main/guides/multi-split.adoc b/content/docs/smallrye-mutiny/main/guides/multi-split.adoc new file mode 100644 index 000000000..d8427562a --- /dev/null +++ b/content/docs/smallrye-mutiny/main/guides/multi-split.adoc @@ -0,0 +1,51 @@ += Splitting a Multi into several Multi +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,intermediate + +It is possible to split a `Multi` into several `Multi` streams. + +== Using the split operator + +Suppose that we have a stream of strings that represent _signals_, and that we want a `Multi` for each kind of signal: + +* `?foo`, `?bar` are _input_ signals, +* `!foo`, `!bar` are _output_ signals, +* `foo`, `bar` are _other_ signals. + +To do that, we need a function that maps each item of the stream to its target stream. +The splitter API needs a Java enumeration to define keys, as in: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/SplitTest.java[tags=enum,indent=0] +---- + +Now we can use the `split` operator that provides a splitter object, and fetch individual `Multi` for each split stream using the `get` method: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/SplitTest.java[tags=splits,indent=0] +---- + +This prints the following console output: + +.... +output - a +input - b +output - c +output - d +other - 123 +input - e +.... + +== Notes on using splits + +* Items flow when all splits have a subscriber. +* The flow stops when either of the subscribers cancels, or when any subscriber has a no outstanding demand. +* The flow resumes when all splits have a subscriber again, and when all subscribers have outstanding demand. +* Only one subscriber can be active for a given split. Other subscription attempts will receive an error. +* When a subscriber cancels, then a new subscription attempt on its corresponding split can succeed. +* Subscribing to an already completed or errored split results in receiving the terminal signal (`onComplete()` or `onFailure(err)`). +* The upstream `Multi` gets subscribed to when the first split subscription happens, no matter which split it is. +* The first split subscription passes its context, if any, to the upstream `Multi`. It is expected that all split subscribers share the same context object, or the behavior of your code will most likely be incorrect. diff --git a/content/docs/smallrye-mutiny/main/guides/pagination.adoc b/content/docs/smallrye-mutiny/main/guides/pagination.adoc new file mode 100644 index 000000000..006afadb3 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/guides/pagination.adoc @@ -0,0 +1,45 @@ += How to use paginated APIs? +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,intermediate + +There are many REST / HTTP APIs using pagination, _i.e._ return only a subset of the results and you need to request the next _page_ to get the next batch. +Each batch contains a list of item(s). + +To use this kind of API and generate a continuous stream of items, you need to use the `Multi.createBy().repeating()` function. +However, we need to pass a cursor / state to advance and avoid requesting again and again the same page. +Fortunately, `repeating` provides methods to pass a shared state. +So by combining these methods and `disjoint` you can generate streams from these pages: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/PaginationTest.java[tags=code,indent=0] +---- + +First, you create a `Multi` containing the items emitted by the `CompletionStage` supplier and pass a state supplier to progress among the pages. + +Then, use `until` to call the paginated API until we have all the items. +At the point we have a stream of list of item such as `++[++"a", "b", "c"++]++, ++[++"d", "e"++]++, ++[]++`. +However, we want the following stream: `"a", "b", "c", "d", "e"`. +The `disjoint` method does exactly this. +It gets the items from the lists and passes them downstream: + +image::images/diagrams/mermaid-df98fc1dd069.svg[mermaid diagram] + +`Multi.createBy().repeating()` lets you choose the number of iterations using: + +* `atMost` - exact number of repetitions (or failure happens before reaching that number) +* `until` - the repetition is stopped if the item emitted by the `Uni` *passes* a test (predicate). +It does not propagate the item that did pass the check, and it stops the repetition. +The check verifies if the current item does not contain valid data. +* `whilst` - the repetition is stopped if the item emitted by the `Uni` *does not pass* a test (predicate). +It does propagate the item downstream even if the check does not pass. +However, it stops the repetition. +The test verifies if there is a _next_ batch to be retrieved. + +The following code illustrates the usage of `whilst`: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/PaginationTest.java[tags=code2,indent=0] +---- diff --git a/content/docs/smallrye-mutiny/main/guides/polling.adoc b/content/docs/smallrye-mutiny/main/guides/polling.adoc new file mode 100644 index 000000000..88aa7115e --- /dev/null +++ b/content/docs/smallrye-mutiny/main/guides/polling.adoc @@ -0,0 +1,21 @@ += How to use polling? +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,advanced + +There are many poll-based API around us. +Sometimes you need to use these APIs to generate a stream from the polled values. + +To do this, use the `repeat()` feature: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/PollableSourceTest.java[tags=code,indent=0] +---- + +You can also stop the repetition using the `repeat().until()` method which will continue the repetition until the given predicate returns `true`, and/or directly create a `Multi` using `Multi.createBy().repeating()`: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/PollableSourceTest.java[tags=code2,indent=0] +---- diff --git a/content/docs/smallrye-mutiny/main/guides/reactive-to-imperative.adoc b/content/docs/smallrye-mutiny/main/guides/reactive-to-imperative.adoc new file mode 100644 index 000000000..b1ede52c1 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/guides/reactive-to-imperative.adoc @@ -0,0 +1,54 @@ += From reactive to imperative +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,advanced + +There are use cases where you need the items in an imperative manner instead of asynchronous. +Typically, when you serve an HTTP request from a worker thread, you can block. + +Mutiny provides the ability to block until you get the items. + +== Awaiting on Uni's item + +When dealing with a `Uni,` you can block and await the item using: + +[source,java] +---- +include::{examples-dir}/java/guides/integration/ReactiveToImperativeTest.java[tags=await,indent=0] +---- + +This method blocks the caller thread until the observed `uni` emits the item. +Note that the returned item can be `null` if the `uni` emits `null.` +If the `uni` fails, it throws the exception, wrapped in the `CompletionException` for _checked_ exception. + +Blocking forever may not be a great idea. +You can use `uni.await().atMost(Duration)` to pass a deadline. +When the deadline is reached, a `TimeoutException` is thrown: + +[source,java] +---- +include::{examples-dir}/java/guides/integration/ReactiveToImperativeTest.java[tags=atMost,indent=0] +---- + +== Iterating over Multi's items + +When dealing with a `Multi,` you may want to iterate over the items using a simple "foreach." +You can achieve this using `multi.subscribe().asIterable()`: + +[source,java] +---- +include::{examples-dir}/java/guides/integration/ReactiveToImperativeTest.java[tags=iterable,indent=0] +---- + +The returned `iterable` is blocking. +It waits for the next items, and during that time, blocks the caller thread. + +The iteration ends once the last item is consumed. +If the `multi` emits a failure, an exception is thrown. + +Similar to `asIterable()`, the `asStream` method lets you retrieve a `java.util.stream.Stream`: + +[source,java] +---- +include::{examples-dir}/java/guides/integration/ReactiveToImperativeTest.java[tags=stream,indent=0] +---- diff --git a/content/docs/smallrye-mutiny/main/guides/replaying-multis.adoc b/content/docs/smallrye-mutiny/main/guides/replaying-multis.adoc new file mode 100644 index 000000000..d2cc64c0e --- /dev/null +++ b/content/docs/smallrye-mutiny/main/guides/replaying-multis.adoc @@ -0,0 +1,94 @@ += Replaying Multis +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,advanced + +A `Multi` is a _cold-source_: no processing happens until you subscribe. + +While the `broadcast` operator can be used so that multiple subscribers consume a `Multi` events _at the same time_, it does not support replaying items for _late subscribers_: when a subscriber joins after the `Multi` has completed (or failed), then it won't receive any item. + +This is where _replaying_ can be useful. + +== Replaying all events + +Replaying all events from an upstream `Multi` works as follows: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/ReplayTest.java[tags=replay-all,indent=0] +---- + +Both `item++_++1` and `item++_++2` trigger new subscriptions, and both lists contain the following elements: + +.... +[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] +.... + +Replaying works by turning `upstream` into a _hot-stream_, meaning that it gets requested `Long.MAX++_++VALUE` elements. +This is done when the first subscription happens. + +The replay operator stores the items in an internal _replay log_, and then each subscriber gets to replay them. + +[IMPORTANT] +==== + +Subscribers demand and cancellation requests are honored while replaying, but `upstream` cannot be cancelled. + +Be careful with unbounded streams as you can exhaust memory! + +In such cases or when you need to replay large amounts of data, you might opt to use some eventing middleware rather than Mutiny replays. + +==== + +== Replaying the last 'n' events + +You can limit the number of elements to replay by using the `upTo` method: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/ReplayTest.java[tags=replay-last,indent=0] +---- + +Each new subscriber gets to replay the last `n` elements from where the replay log is at subscription time. +For instance the first subscriber can observe all events, while a subscriber that joins 2 seconds later might not observe the earlier events. + +Since `Multi.createFrom().range(0, 10)` is an _immediate_ stream, both `item++_++1` and `item++_++2` lists contain the last items: + +.... +[7, 8, 9] +.... + +== Prepending with seed data + +In some cases you might want to prepend some _seed_ data that will be available for replay before the upstream starts emitting. + +You can do so using an `Iterable` to provide such seed data: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/ReplayTest.java[tags=replay-seed,indent=0] +---- + +In which case subscribers can observe the following events: + +.... +[-10, -5, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9] +.... + +== Replay of failures and completions + +Subscribers get to observe not just items but also the failure and completion events: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/ReplayTest.java[tags=replay-errors,indent=0] +---- + +Running this code yields the following output for any subscriber: + +.... +-> 7 +-> 8 +-> 9 +Failed: boom +.... diff --git a/content/docs/smallrye-mutiny/main/guides/rx.adoc b/content/docs/smallrye-mutiny/main/guides/rx.adoc new file mode 100644 index 000000000..b41ec4f0f --- /dev/null +++ b/content/docs/smallrye-mutiny/main/guides/rx.adoc @@ -0,0 +1,31 @@ += Using map, flatMap and concatMap +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,advanced + +If you are a seasoned reactive developer, you may miss the `map`, `flatMap`, `concatMap` methods. + +The Mutiny API is quite different from the _standard_ reactive eXtensions API. + +There are multiple reasons for this choice. +Typically, _flatMap_ is not necessarily well understood by every developer, leading to potentially catastrophic consequences. + +That being said, Mutiny provides the _map_, _flatMap_ and _concatMap_ methods, implementing the most common variant for each: + +[source,java] +---- +include::{examples-dir}/java/guides/RxTest.java[tags=rx,indent=0] +---- + +The Mutiny equivalents are: + +* `map -++>++ onItem().transform()` +* `flatMap -++>++ onItem().transformToUniAndMerge` and `onItem().transformToMultiAndMerge` +* `concatMap -++>++ onItem().transformToUniAndConcatenate` and `onItem().transformToMultiAndConcatenate` + +The following snippet demonstrates how to uses these methods: + +[source,java] +---- +include::{examples-dir}/java/guides/RxTest.java[tags=mutiny,indent=0] +---- diff --git a/content/docs/smallrye-mutiny/main/guides/shortcut-methods.adoc b/content/docs/smallrye-mutiny/main/guides/shortcut-methods.adoc new file mode 100644 index 000000000..219dcdcb1 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/guides/shortcut-methods.adoc @@ -0,0 +1,44 @@ += Shortcut methods +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,beginner + +The Mutiny API is decomposed around the idea of groups, each group handling a specific event. +However, to avoid verbosity, Mutiny also exposes _shortcuts_ for the most used methods. +Be aware that these shorts, while making the code shorter, may harm the readability and understandability. + +To _peek_ at items, you can use the `invoke` method: + +[source,java] +---- +include::{examples-dir}/java/guides/ShortcutsTest.java[tags=invoke,indent=0] +---- + +`invoke` is a shortcut for `onItem().invoke(...)`. + +Mutiny also provides the `call` method for executing an action returning a `Uni`. +This is useful to execute an asynchronous action without modifying incoming item: + +[source,java] +---- +include::{examples-dir}/java/guides/ShortcutsTest.java[tags=call,indent=0] +---- + +`call` is a shortcut for `onItem().call(...)`. + +The following table lists the available shortcuts available by the `Uni` class: + +[width="100%",cols="41%,59%",options="header",] +|=== +|Shortcut |Equivalent +|`uni.map(x -++>++ y)` |`uni.onItem().transform(x -++>++ y)` +|`uni.flatMap(x -++>++ uni2)` |`uni.onItem().transformToUni(x -++>++ uni2)` +|`uni.chain(x -++>++ uni2)` |`uni.onItem().transformToUni(x -++>++ uni2)` +|`uni.invoke(x -++>++ System.out.println(x))` |`uni.onItem().invoke(x -++>++ System.out.println(x))` +|`uni.call(x -++>++ uni2)` |`uni.onItem().call(x -++>++ uni2)` +|`uni.eventually(() -++>++ System.out.println("eventually"))` |`uni.onTermination().invoke(() -++>++ System.out.println("eventually"))` +|`uni.eventually(() -++>++ uni2)` |`uni.onTermination().call((ignoredItem, ignoredError, ignoredCancellation) -++>++ uni2)` +|`uni.replaceWith(x)` |`uni.onItem().transform(ignored -++>++ x)` +|`uni.replaceWith(uni2)` |`uni.onItem().transformToUni(ignored -++>++ uni2)` +|`uni.replaceIfNullWith(x)` |`uni.onItem().ifNull().continueWith(x)` +|=== diff --git a/content/docs/smallrye-mutiny/main/guides/spies.adoc b/content/docs/smallrye-mutiny/main/guides/spies.adoc new file mode 100644 index 000000000..2a87e08e8 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/guides/spies.adoc @@ -0,0 +1,71 @@ += Spying on events +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,advanced + +Spies are useful when you need to track which _events_ flow into a `Uni` or a `Multi`. +Spies can track events from groups such as `onItem()`, `onFailure()`, `onSubscribe()`, etc. + +The `io.smallrye.mutiny.helpers.spies.Spy` interface offers factory methods to spy on selected groups, or even on all groups. + +== Spying selected groups + +The following example spies on requests and completion group events: + +[source,java] +---- +include::{examples-dir}/java/guides/SpiesTest.java[tags=selected,indent=0] +---- + +The standard output stream shall display the following text: + +.... +1 +2 +3 +Number of requests: 9223372036854775807 +Completed? true +.... + +The number of requests corresponds to `Long.MAX++_++VALUE`, and a completion event was sent. + +[IMPORTANT] +==== + +It is important to note that spies observe and report events for all subscribers, not just one in particular. + +You should call the `.reset()` method on a given spy to resets its statistics such as the invocation count. + +==== + +== Spying all groups + +You can take advantage of a _global spy_ if you are interested in all event groups: + +[source,java] +---- +include::{examples-dir}/java/guides/SpiesTest.java[tags=global,indent=0] +---- + +Running the snippet above gives the following output: + +.... +1 +2 +3 +Number of requests: 9223372036854775807 +Cancelled? false +Failure? null +Items: [1, 2, 3] +.... + +[WARNING] +==== + +Tracking `onItem()` events on a `Multi` requires storing all items into a list, which can yield an out-of-memory +exception with large streams. + +In such cases consider using `Spy.onItem(multi, false)` to obtain a spy that does not store items, but that can +still report data such as the number of received events (see `spy.invocationCount()`). + +==== diff --git a/content/docs/smallrye-mutiny/main/guides/take-skip-items.adoc b/content/docs/smallrye-mutiny/main/guides/take-skip-items.adoc new file mode 100644 index 000000000..f466d1062 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/guides/take-skip-items.adoc @@ -0,0 +1,116 @@ += Take/Skip the first or last items +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,beginner + +Multi provides the ability to: + +* only forward items from the beginning of the observed multi, +* only forward the last items (and discard all the other ones), +* skip items from the beginning of the multi, +* skip the last items. + +These actions are available from the `multi.select()` and `multi.skip()` groups, allowing to, respectively, select and skip +items from upstream. + +== Selecting items + +The `multi.select().first` method forwards on the _n_ *first* items from the multi. +It forwards that amount of items and then sends the completion signal. +It also cancels the upstream subscription. + +[source,java] +---- +include::{examples-dir}/java/guides/operators/SelectAndSkipTest.java[tags=take-first,indent=0] +---- + +[NOTE] +==== + +The `select().first()` method selects only the first item. + +==== + +If the observed multi emits fewer items, it sends the completion event when the upstream completes. + +Similarly, The `multi.select().last` operator forwards on the _n_ *last* items from the multi. +It discards all the items emitted beforehand. + +[source,java] +---- +include::{examples-dir}/java/guides/operators/SelectAndSkipTest.java[tags=take-last,indent=0] +---- + +[NOTE] +==== + +The `select().last()` method selects only the last item. + +==== + +The `multi.select().first(Predicate)` operator forwards the items while the passed predicate returns `true`: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/SelectAndSkipTest.java[tags=take-while,indent=0] +---- + +It calls the predicate for each item. +Once the predicate returns `false`, it stops forwarding the items downstream. +It also sends the completion event and cancels the upstream subscription. + +Finally, `multi.select().first(Duration)` operator picks the first items emitted during a given period. +Once the passed duration expires, it sends the completion event and cancels the upstream subscription. +If the observed multi completes before the passed duration, it sends the completion event. + +[source,java] +---- +include::{examples-dir}/java/guides/operators/SelectAndSkipTest.java[tags=take-for,indent=0] +---- + +== Skipping items + +You can also skip items using `multi.skip()`. + +The `multi.skip().first(n)` method skips the _n_ *first* items from the multi. +It forwards all the remaining items and sends the completion event when the upstream multi completes. + +[source,java] +---- +include::{examples-dir}/java/guides/operators/SelectAndSkipTest.java[tags=skip-first,indent=0] +---- + +If the observed multi emits fewer items, it sends the completion event without emitting any items. + +[NOTE] +==== + +`skip().last()` drops only the very last item. + +==== + +Similarly, The `multi.skip().last(n)` operator skips on the _n_ *last* items from the multi: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/SelectAndSkipTest.java[tags=skip-last,indent=0] +---- + +The `multi.skip().first(Predicate)` operator skips the items while the passed predicate returns `true`: + +[source,java] +---- +include::{examples-dir}/java/guides/operators/SelectAndSkipTest.java[tags=skip-while,indent=0] +---- + +It calls the predicate for each item. +Once the predicate returns `false`, it stops discarding the items and starts forwarding downstream. + +Finally, `multi.skip().first(Duration)` operator skips the first items for a given period. +Once the passed duration expires, it sends the items emitted after the deadline downstream. +If the observed multi completes before the passed duration, it sends the completion event. + +[source,java] +---- +include::{examples-dir}/java/guides/operators/SelectAndSkipTest.java[tags=skip-for,indent=0] +---- diff --git a/content/docs/smallrye-mutiny/main/guides/testing.adoc b/content/docs/smallrye-mutiny/main/guides/testing.adoc new file mode 100644 index 000000000..6722c54dd --- /dev/null +++ b/content/docs/smallrye-mutiny/main/guides/testing.adoc @@ -0,0 +1,72 @@ += How can I write unit / integration tests? +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,beginner + +Mutiny provides subscribers for `Uni` and `Multi` offering helpful assertion methods. +You can use them to test pipelines. + +== Testing a Uni + +Here is an example to test a `Uni`: + +[source,java] +---- +include::{examples-dir}/java/guides/TestSubscribersTest.java[tags=uni,indent=0] +---- + +You can also use predicate-based assertions and item inspection: + +[source,java] +---- +include::{examples-dir}/java/guides/TestSubscribersTest.java[tags=uni-predicate,indent=0] +---- + +== Testing a Multi + +Testing a `Multi` pipeline is similar: + +[source,java] +---- +include::{examples-dir}/java/guides/TestSubscribersTest.java[tags=multi,indent=0] +---- + +Predicate-based assertions work on `Multi` too: + +[source,java] +---- +include::{examples-dir}/java/guides/TestSubscribersTest.java[tags=multi-predicate,indent=0] +---- + +== Declarative verification with AssertMulti + +For multi-item streams, `AssertMulti` provides a declarative step-by-step verifier. +Build a sequence of expectations, then call `verify()`: + +[source,java] +---- +include::{examples-dir}/java/guides/TestSubscribersTest.java[tags=assert-multi,indent=0] +---- + +You can control demand explicitly for backpressure testing: + +[source,java] +---- +include::{examples-dir}/java/guides/TestSubscribersTest.java[tags=assert-multi-demand,indent=0] +---- + +== Testing failures + +The assertions do not just focus on _good_ outcomes, you can also test failures as in: + +[source,java] +---- +include::{examples-dir}/java/guides/TestSubscribersTest.java[tags=failing,indent=0] +---- + +With `AssertMulti`: + +[source,java] +---- +include::{examples-dir}/java/guides/TestSubscribersTest.java[tags=assert-multi-failure,indent=0] +---- diff --git a/content/docs/smallrye-mutiny/main/guides/unchecked-exceptions.adoc b/content/docs/smallrye-mutiny/main/guides/unchecked-exceptions.adoc new file mode 100644 index 000000000..8b0f4206d --- /dev/null +++ b/content/docs/smallrye-mutiny/main/guides/unchecked-exceptions.adoc @@ -0,0 +1,42 @@ += Dealing with checked exceptions +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: guide,intermediate + +When implementing your reactive pipeline, you write lots of functions (`java.util.function.Function`), consumers (`java.util.function.Consumer`), suppliers (`java.util.function.Supplier`) and so on. + +By default, you cannot throw checked exceptions. + +When integrating libraries throwing checked exceptions (like `IOException`) it's not very convenient to add a `try/catch` block and wrap the thrown exception into a runtime exception: + +[source,java] +---- +include::{examples-dir}/java/guides/UncheckedTest.java[tags=rethrow,indent=0] +---- + +Mutiny provides utilities to avoid having to do this manually. + +If your operation throws a _checked exception_, you can use the https://javadoc.io/doc/io.smallrye.reactive/mutiny/latest/io/smallrye/mutiny/unchecked/Unchecked.html[`io.smallrye.mutiny.unchecked.Unchecked`] wrappers. + +For example, if your synchronous transformation uses a method throwing a checked exception, wrap it using `Unchecked.function`: + +[source,java] +---- +include::{examples-dir}/java/guides/UncheckedTest.java[tags=transform,indent=0] +---- + +You can also wrap consumers such as in: + +[source,java] +---- +include::{examples-dir}/java/guides/UncheckedTest.java[tags=invoke,indent=0] +---- + +[TIP] +==== + +You can add the following import statement to simplify the usage of the provided methods: + +`import static io.smallrye.mutiny.unchecked.Unchecked.++*++;` + +==== diff --git a/content/docs/smallrye-mutiny/main/images/diagrams/mermaid-0277dcc2b534.svg b/content/docs/smallrye-mutiny/main/images/diagrams/mermaid-0277dcc2b534.svg new file mode 100644 index 000000000..d390d8d76 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/images/diagrams/mermaid-0277dcc2b534.svg @@ -0,0 +1 @@ +SubscriberProcessorPublisherSubscriberProcessorPublishersubscribesubscribesubscriptionsubscriptionrequest(5)request(5)onItem("a")onItem("A")onItem("b")onItem("B")cancel()cancel() \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/main/images/diagrams/mermaid-18f9a50c0627.svg b/content/docs/smallrye-mutiny/main/images/diagrams/mermaid-18f9a50c0627.svg new file mode 100644 index 000000000..684d7dc80 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/images/diagrams/mermaid-18f9a50c0627.svg @@ -0,0 +1 @@ +SubscriberemitOn(executor)MultiSubscriberemitOn(executor)MultiOn caller threadOn executor threadonItem(1)1onItem(1)2 \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/main/images/diagrams/mermaid-3b8d4c58438a.svg b/content/docs/smallrye-mutiny/main/images/diagrams/mermaid-3b8d4c58438a.svg new file mode 100644 index 000000000..bfccc4178 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/images/diagrams/mermaid-3b8d4c58438a.svg @@ -0,0 +1 @@ +SubscriberMulti(item)TransformerUniSubscriberMulti(item)TransformerUnionItem(item)1subscribe(...)2onSubscribe(sub)3onItem(item)4onItem(item)5onItem(item)6onItem(item)7 \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/main/images/diagrams/mermaid-3d4459e3c6a6.svg b/content/docs/smallrye-mutiny/main/images/diagrams/mermaid-3d4459e3c6a6.svg new file mode 100644 index 000000000..2e8d7dc42 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/images/diagrams/mermaid-3d4459e3c6a6.svg @@ -0,0 +1 @@ +SubscriberCombined streamStream BStream ASubscriberCombined streamStream BStream AonItem(1)1onItem(2)2onItem(a)3onItem([1,a])4onItem(3)5onItem(b)6onItem([2,b])7onItem(c)8 \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/main/images/diagrams/mermaid-491ff27a51a5.svg b/content/docs/smallrye-mutiny/main/images/diagrams/mermaid-491ff27a51a5.svg new file mode 100644 index 000000000..767fb50f9 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/images/diagrams/mermaid-491ff27a51a5.svg @@ -0,0 +1 @@ +ObserverStreamObserverStreamObserverStreamObserverStreamonItem("a")onItem("a")onItem("b")onItem("b")onItem("c")onFailure(err)onCompletion() \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/main/images/diagrams/mermaid-69aa56febc93.svg b/content/docs/smallrye-mutiny/main/images/diagrams/mermaid-69aa56febc93.svg new file mode 100644 index 000000000..47b820123 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/images/diagrams/mermaid-69aa56febc93.svg @@ -0,0 +1 @@ +SubscriberCollect operatorMultiSubscriberCollect operatorMultionItem(1)1onItem(2)2onItem(3)3onItem([1, 2, 3])4 \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/main/images/diagrams/mermaid-74e1ac453bf9.svg b/content/docs/smallrye-mutiny/main/images/diagrams/mermaid-74e1ac453bf9.svg new file mode 100644 index 000000000..d0afbd3b0 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/images/diagrams/mermaid-74e1ac453bf9.svg @@ -0,0 +1 @@ +Merged streamStream BStream AMerged streamStream BStream Asubscribe1subscribe2onSubscribe(s)3onItem(1)4onSubscribe(s)5onItem(2)6onItem(a)7onItem(3)8onItem(b)9onItem(c)10 \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/main/images/diagrams/mermaid-76df06b35153.svg b/content/docs/smallrye-mutiny/main/images/diagrams/mermaid-76df06b35153.svg new file mode 100644 index 000000000..137defedd --- /dev/null +++ b/content/docs/smallrye-mutiny/main/images/diagrams/mermaid-76df06b35153.svg @@ -0,0 +1 @@ +SubscriberrunSubscriptionOn(executor)MultiSubscriberrunSubscriptionOn(executor)Multion Caller threadOn executor threadsubscribe1subscribe2 \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/main/images/diagrams/mermaid-bc092005fd3f.svg b/content/docs/smallrye-mutiny/main/images/diagrams/mermaid-bc092005fd3f.svg new file mode 100644 index 000000000..2f934d514 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/images/diagrams/mermaid-bc092005fd3f.svg @@ -0,0 +1 @@ +DownstreamReturned UnisonItem().call(...)MultiDownstreamReturned UnisonItem().call(...)Multiitem11item12result13result14item25item26result27result28item39item310result311result312 \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/main/images/diagrams/mermaid-c54f60b30f42.svg b/content/docs/smallrye-mutiny/main/images/diagrams/mermaid-c54f60b30f42.svg new file mode 100644 index 000000000..07645de13 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/images/diagrams/mermaid-c54f60b30f42.svg @@ -0,0 +1 @@ +SubscriberCombined streamStream BStream ASubscriberCombined streamStream BStream AonItem(1)1onItem(a)2onItem([1,a])3onItem(2)4onItem(b)5onItem([2,b])6 \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/main/images/diagrams/mermaid-c6f9029821c4.svg b/content/docs/smallrye-mutiny/main/images/diagrams/mermaid-c6f9029821c4.svg new file mode 100644 index 000000000..64a7d800c --- /dev/null +++ b/content/docs/smallrye-mutiny/main/images/diagrams/mermaid-c6f9029821c4.svg @@ -0,0 +1 @@ +Concatenated streamStream BStream AConcatenated streamStream BStream Asubscribe1onSubscribe(s)2onItem(1)3onItem(2)4onItem(3)5onCompletion()6subscribe7onSubscribe(s)8onItem(a)9onItem(b)10onItem(c)11 \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/main/images/diagrams/mermaid-cbd13fb6d19b.svg b/content/docs/smallrye-mutiny/main/images/diagrams/mermaid-cbd13fb6d19b.svg new file mode 100644 index 000000000..4d0a969d8 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/images/diagrams/mermaid-cbd13fb6d19b.svg @@ -0,0 +1 @@ +

a, b, c

a, b, c

a, b, c

a, b, c

a, b, c

a, b, c

Events source

Events source

Events source

Publisher

Publisher

Publisher

Subscriber 1

Subscriber 2

Subscriber 3

\ No newline at end of file diff --git a/content/docs/smallrye-mutiny/main/images/diagrams/mermaid-df98fc1dd069.svg b/content/docs/smallrye-mutiny/main/images/diagrams/mermaid-df98fc1dd069.svg new file mode 100644 index 000000000..876990fe8 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/images/diagrams/mermaid-df98fc1dd069.svg @@ -0,0 +1 @@ +SubscriberDisjoint operatorMultiSubscriberDisjoint operatorMultionItem([a,b,c])1onItem(a)2onItem(b)3onItem(c)4onItem([d,e])5onItem(d)6onItem(e)7 \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/main/images/diagrams/mermaid-e6dd95e32a3f.svg b/content/docs/smallrye-mutiny/main/images/diagrams/mermaid-e6dd95e32a3f.svg new file mode 100644 index 000000000..deda34e8f --- /dev/null +++ b/content/docs/smallrye-mutiny/main/images/diagrams/mermaid-e6dd95e32a3f.svg @@ -0,0 +1 @@ +SubscriberTransformerMultiSubscriberTransformerMultionItem(a1)1onItem(a2)2onItem(b1)3onItem(b2)4onItem(c1)5onItem(c2)6 \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/main/images/diagrams/mermaid-e88e1918cd26.svg b/content/docs/smallrye-mutiny/main/images/diagrams/mermaid-e88e1918cd26.svg new file mode 100644 index 000000000..8f33f491e --- /dev/null +++ b/content/docs/smallrye-mutiny/main/images/diagrams/mermaid-e88e1918cd26.svg @@ -0,0 +1 @@ +DownstreamonItem().invoke(...)MultiDownstreamonItem().invoke(...)Multicallback executionitem11item12item23item24item35item36 \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/main/images/diagrams/mermaid-f0001b5cde9f.svg b/content/docs/smallrye-mutiny/main/images/diagrams/mermaid-f0001b5cde9f.svg new file mode 100644 index 000000000..5818402e1 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/images/diagrams/mermaid-f0001b5cde9f.svg @@ -0,0 +1 @@ +SubscriberCombined streamStream BStream ASubscriberCombined streamStream BStream AonItem(1)1onItem(2)2onItem(a)3onItem([2,a])4onItem(3)5onItem([3,a])6onItem(b)7onItem([3,b])8onItem(c)9onItem([3,c])10 \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/main/images/diagrams/mermaid-f67adb6018b3.svg b/content/docs/smallrye-mutiny/main/images/diagrams/mermaid-f67adb6018b3.svg new file mode 100644 index 000000000..3fbcf4900 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/images/diagrams/mermaid-f67adb6018b3.svg @@ -0,0 +1 @@ +SubscriberUni(item)TransformerUniSubscriberUni(item)TransformerUnionItem(item)1subscribe(...)2onSubscribe(sub)3onItem(result)4onItem(result)5 \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/main/images/diagrams/mermaid-f71496c9eae5.svg b/content/docs/smallrye-mutiny/main/images/diagrams/mermaid-f71496c9eae5.svg new file mode 100644 index 000000000..fc83f5b8c --- /dev/null +++ b/content/docs/smallrye-mutiny/main/images/diagrams/mermaid-f71496c9eae5.svg @@ -0,0 +1 @@ +SubscriberProcessor 2Processor 1PublisherSubscriberProcessor 2Processor 1PublisheronItemonItemonItemonItemonItemonItemonCompletiononCompletiononCompletion \ No newline at end of file diff --git a/content/docs/smallrye-mutiny/main/images/diagrams/mermaid-f74f7917dcf7.svg b/content/docs/smallrye-mutiny/main/images/diagrams/mermaid-f74f7917dcf7.svg new file mode 100644 index 000000000..b92c0941f --- /dev/null +++ b/content/docs/smallrye-mutiny/main/images/diagrams/mermaid-f74f7917dcf7.svg @@ -0,0 +1 @@ +

a, b, c

a, b, c

a, b, c

a, b, c

Events source

Publisher

Subscriber 1

Subscriber 2

Subscriber 3

\ No newline at end of file diff --git a/content/docs/smallrye-mutiny/main/index.adoc b/content/docs/smallrye-mutiny/main/index.adoc new file mode 100644 index 000000000..74b38c67d --- /dev/null +++ b/content/docs/smallrye-mutiny/main/index.adoc @@ -0,0 +1,88 @@ += Mutiny -- Intuitive Event-Driven Reactive Programming Library for Java +include::_attributes.adoc[] + +[source,java] +---- +Uni request = makeSomeNetworkRequest(params); + +request.ifNoItem().after(ofMillis(100)) + .failWith(() -> new TooSlowException("💥")) + .onFailure(IOException.class).recoverWithItem(fail -> "📦") + .subscribe().with( + item -> log("👍 " + item), + err -> log(err.getMessage()) + ); +---- + +xref:tutorials/getting-mutiny.adoc[Get Mutiny {versions-mutiny},role=button] + +[.cards] +-- + +**** + +[discrete] +== Event-Driven + +Mutiny places _events_ at the core of its design. + +With Mutiny, you observe _events_, _react_ to them, and create elegant and readable processing pipelines. + +A PhD in functional programming is not required. + +**** + +**** + +[discrete] +== Navigable + +Even with smart code completion, classes with hundreds of methods are confusing. + +Mutiny provides a navigable and explicit API driving you towards the operator you need. + +**** + +**** + +[discrete] +== Non-blocking I/O + +Mutiny is the perfect companion to tame the asynchronous nature of applications with non-blocking I/O. + +Compose operations in a declarative fashion, transform data, enforce progress, recover from failures and more. + +**** + +**** + +[discrete] +== Quarkus and Vert.x native + +Mutiny is integrated in https://quarkus.io[Quarkus] where every reactive API uses Mutiny, and https://vertx.io[Eclipse Vert.x] clients are made available using Mutiny bindings. + +Mutiny is however an independent library that can ultimately be used in any Java application. + +**** + +**** + +[discrete] +== Made for an asynchronous world + +Mutiny can be used in any asynchronous application such as event-driven microservices, message-based applications, network utilities, data stream processing, and of course... reactive applications! + +**** + +**** + +[discrete] +== Reactive Converters Built-In + +Mutiny is based on the https://www.reactive-streams.org/[Reactive Streams protocol] and https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/concurrent/Flow.html[Java Flow], and so it can be integrated with any other reactive programming library. + +In addition, it proposes _converters_ to interact with other popular libraries. + +**** + +-- diff --git a/content/docs/smallrye-mutiny/main/reference/distributed_systems_are_asynchronous.png b/content/docs/smallrye-mutiny/main/reference/distributed_systems_are_asynchronous.png new file mode 100644 index 000000000..0d00a8c9e Binary files /dev/null and b/content/docs/smallrye-mutiny/main/reference/distributed_systems_are_asynchronous.png differ diff --git a/content/docs/smallrye-mutiny/main/reference/going-reactive-a-few-pitfalls.adoc b/content/docs/smallrye-mutiny/main/reference/going-reactive-a-few-pitfalls.adoc new file mode 100644 index 000000000..ac6f0e7b2 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/reference/going-reactive-a-few-pitfalls.adoc @@ -0,0 +1,74 @@ += Going reactive: a few pitfalls +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: reference,beginner + +Don't get us wrong, reactive programming is a fantastic way to write resource-efficient code! + +That being said, reactive programming has a learning curve that should not be taken lightly, and in some cases it is safer to write imperative code that you fully comprehend over reactive code that you don't fully grok. + +We have assembled a few considerations that we think new users should know before they embark into writing complex reactive business logic. + +== Mutiny doesn't auto-magically make your code asynchronous + +This is a common source of confusion for new reactive programmers. +Mutiny itself *does not perform any scheduling work*, except for the xref:guides/emit-on-vs-run-subscription-on.adoc[`emitOn` and `runSubscriptionOn` operators]. + +Consider the following code where we _join_ results from multiple asynchronous operations, materialised by the `Uni++<++String++>++`-returning `fetch` method: + +[source,java] +---- +include::{examples-dir}/java/guides/reference/PitfallsTest.java[tags=noMagicJoin,indent=0] +---- + +You might think that the `join` operator schedules the calls to `fetch` to be run concurrently, and then collects the results into a list. +This is not how it works! + +The `join` operator does subscribe to each `Uni++<++String++>++` returned by each call to `fetch`. +When it receives a value, it puts it into a list, and when all values have been received, that list is emitted. +The threads involved here are the ones that emit values in `fetch`. +If `fetch` uses async I/O underneath then you should observe true concurrency, but if `fetch` just emits a value right when the subscription happens then you will merely observe a sequential execution of each call to `fetch`, in order. + +== When to prefer `Uni++<++List++<++T++>>++` over `Multi++<++T++>++` + +The reason why `Multi` exists is to model streams over back-pressured sources. +By conforming to the https://www.reactive-streams.org/[Reactive Streams protocol], a `Multi` respects the control flow requests from its subscribers, avoiding classic problems such as a fast producer and a slow consumer that can yield to memory exhaustion problems. + +That being said, not everything is a stream. +Take the example of relational databases: *databases don't stream!* (for the most parts) + +When you do a query such as `SELECT ++*++ FROM ABC WHERE INDEX ++<++ 123`, you get result rows. +While you might wrap the results in a `Multi++<++Row++>++` as a convenience, the network protocol of the database still sends you all `Row` values and is very unlikely to support any notion of back-pressure on a SQL query result. + +This is why `Uni++<++List++<++Row++>>++` is in this case a better representation of an asynchronous operation than `Multi++<++Row++>++`, because the underlying networked service protocol does not provide you with any back-pressured stream. + +== Creating `Uni` and `Multi` from in-memory data might be suspicious + +You will find lots of occurrences of creating `Uni` and `Multi` from in-memory data in this documentation, as in: + +[source,java] +---- +include::{examples-dir}/java/guides/reference/PitfallsTest.java[tags=inMemoryData,indent=0] +---- + +This is convenient and expected when creating tests and examples, but this should be a strong warning in production. +Indeed, if we have a method such as the following: + +[source,java] +---- +include::{examples-dir}/java/guides/reference/PitfallsTest.java[tags=suspiciousPublisher,indent=0] +---- + +then it is clear that there is nothing _"reactive"_ in this code _(sadly, you can find such idioms in some well-known "reactive" client libraries, but we digress)_. + +As a rule of thumb, if your *initial* publisher does not make any I/O operation and it already has the data available in memory, then it is suspicious: + +* if it is a `Uni`, then it does not really model an asynchronous I/O operation because the data is already here, and +* if it is a `Multi` then not only there is no asynchronous I/O operation involved, but there is no need for a back-pressure protocol either (see the previous section). + +What is not suspicious however is to create, say, a `Multi` to perform a transformation operation: + +[source,java] +---- +include::{examples-dir}/java/guides/reference/PitfallsTest.java[tags=flatmap-ism,indent=0] +---- diff --git a/content/docs/smallrye-mutiny/main/reference/migrating-to-mutiny-2.adoc b/content/docs/smallrye-mutiny/main/reference/migrating-to-mutiny-2.adoc new file mode 100644 index 000000000..c036345db --- /dev/null +++ b/content/docs/smallrye-mutiny/main/reference/migrating-to-mutiny-2.adoc @@ -0,0 +1,52 @@ += Migrating to Mutiny 2 +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: reference,beginner + +The upgrade is transparent for most code bases that _use_ Mutiny in applications (e.g., Quarkus applications). + +== Highlights + +* Mutiny 2 is a major release with source and binary incompatible changes to the Mutiny `0.x` and `1.x` series. +* The main highlight of Mutiny 2 is that it is now based on top of the `java.util.concurrent.Flow` APIs instead of the legacy _Reactive Streams APIs_. +* The `Flow` APIs have been part of the JDK since Java 9, and they are the modern _Reactive Streams APIs_. +* Mutiny remains a faithful implementation of the _Reactive Streams_ specification and passes the `Flow` variant of the _Reactive Streams TCK_. +* Deprecated APIs in Mutiny `1.x` have been removed, and experimental APIs have been promoted. + +== Impact of the switch from legacy Reactive Streams APIs to JDK Flow + +* The `Flow` types are isomorphic to the legacy _Reactive Streams API_ types. +* We recommend that you migrate to `Flow` in your own code bases. +* You should encourage third-party libraries to migrate to `Flow`. +* You can always use _adapters_ to go back and forth between `Flow` and legacy _Reactive Streams_ types. + +=== General guidelines + +* If your code _only uses_ `Uni` and `Multi` (i.e., not `org.reactivestreams.Publisher`), then you will be source-compatible with Mutiny 2. You should still recompile and check that your test suites pass. +* If you expose `Multi` as a `org.reactivestreams.Publisher` then you will either need an _adapter_ (see below) or migrate to `java.util.concurrent.Flow.Publisher`. +* If you interact with `org.reactivestreams.Publisher` publishers and you can't migrate them to `java.util.concurrent.Flow.Publisher` (e.g., because it is a third-party library), then you will need an _adapter_. Please encourage third-party libraries to migrate to `Flow`. + +=== Adapters between Flow and legacy Reactive Streams APIs + +* We recommend using the adapters from the https://smallrye.io/smallrye-mutiny-zero[Mutiny Zero project]. +** The Maven coordinates are `groupId: io.smallrye.reactive`, `artifactId: mutiny-zero-flow-adapters` +** Use `AdaptersToFlow` to convert from _Reactive Streams_ types to `Flow` types, and +** Use `AdaptersToReactiveStreams` to convert `Flow` types to _Reactive Streams_ types. +* The Mutiny Zero adapters have virtually zero overhead. + +== Other API changes + +=== Deprecated API removals + +* `Uni` and `Multi` `onSubscribe()` group is now `onSubscription()`. +* `AssertSubscriber.await()` has been replaced by event-specific methods (items, failure, completion, etc). +* The _RxJava 2_ integration module has been discarded (only RxJava 3 is now supported). + +=== Experimental API promotions + +* `Uni` and `Multi` subscription-bound contexts. +* `Uni.join()` publisher. +* `.ifNoItem()` timeout operators. +* `Uni` and `Multi` spies. +* `capDemandsUsing()` and `paceDemand()` request management operators. +* `Multi` `replay()` operator. diff --git a/content/docs/smallrye-mutiny/main/reference/publications.adoc b/content/docs/smallrye-mutiny/main/reference/publications.adoc new file mode 100644 index 000000000..05db24027 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/reference/publications.adoc @@ -0,0 +1,18 @@ += Publications +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: reference,advanced + +Julien Ponge, Arthur Navarro, Clément Escoffier, and Frédéric Le Mouël. 2021. +*https://doi.org/10.1145/3486605.3486788[Analysing the Performance and Costs of Reactive Programming Libraries in Java].* +_In Proceedings of the 8th ACM SIGPLAN International Workshop on Reactive and Event-Based Languages and Systems (REBLS ’21)_, October 18, 2021, Chicago, IL, USA. ACM, New York, NY, USA, 10 pages. +https://hal.inria.fr/hal-03409277/document[(PDF)] + +____ +Modern services running in cloud and edge environments need to be resource-efficient to increase deployment density and reduce operating costs. +Asynchronous I/O combined with asynchronous programming provides a solid technical foundation to reach these goals. +Reactive programming and reactive streams are gaining traction in the Java ecosystem. +However, reactive streams implementations tend to be complex to work with and maintain. +This paper discusses the performance of the three major reactive streams compliant libraries used in Java applications: RxJava, Project Reactor, and SmallRye Mutiny. +As we will show, advanced optimization techniques such as operator fusion do not yield better performance on realistic I/O-bound workloads, and they significantly increase development and maintenance costs. +____ diff --git a/content/docs/smallrye-mutiny/main/reference/uni-and-multi.adoc b/content/docs/smallrye-mutiny/main/reference/uni-and-multi.adoc new file mode 100644 index 000000000..bc8181723 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/reference/uni-and-multi.adoc @@ -0,0 +1,44 @@ += Uni and Multi +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: reference,beginner + +Mutiny defines two _reactive_ types: + +* `Multi` - represents streams of ++_++0..++*_++ items (potentially unbounded) +* `Uni` - represents streams receiving either an item or a failure + +[TIP] +==== + +The Mutiny name comes from the contraction of `Multi` and `Uni` names + +==== + +Both `Uni` and `Multi` are asynchronous types. +They receive and fire events at any time. + +You may wonder why we make the distinction between `Uni` and `Multi.` +Conceptually, a `Uni` is a `Multi,` right? + +In practice, you don't use `Unis` and `Multis` the same way. +The use cases and operations are different. + +* `Uni` does not need the complete ceremony presented above as the _request_ does not make sense. +* The `subscribe` event expresses the interest and triggers the computation, no need for an additional _request_. +* `Uni` can handle items having a `null` value (and has specific methods to handle this case). +* `Multi` does not allow it (because the Reactive Streams specification forbids it). +* Having a `Uni` implementing `Publisher` would be a bit like having `Optional` implementing `Iterable`. + +In other words, `Uni`: + +* can receive at most 1 `item` event, or a `failure` event +* cannot receive a `completion` event (`null` in the case of 0 items) +* cannot receive a `request` event + +The following snippet shows how you can use `Uni` and `Multi`: + +[source,java] +---- +include::{examples-dir}/java/guides/UniMultiComparisonTest.java[tags=code,indent=0] +---- diff --git a/content/docs/smallrye-mutiny/main/reference/what-is-reactive-programming.adoc b/content/docs/smallrye-mutiny/main/reference/what-is-reactive-programming.adoc new file mode 100644 index 000000000..9065de5f3 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/reference/what-is-reactive-programming.adoc @@ -0,0 +1,31 @@ += What is Reactive Programming? +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: reference,beginner + +Mutiny is a reactive programming library. +If you look on Wikipedia for reactive programming, you will find the following definition: + +____ +Reactive Programming combines functional programming, the observer pattern, and the iterable pattern. +____ + +While correct, we never found this definition very helpful. +It does not convey clearly what's reactive programming is all about. +So, let's make another definition, much more straightforward: + +____ +Reactive programming is about programming with data streams. +____ + +That's it. +Reactive programming is about streams and especially, observing them. +It pushes that idea to its limit: with reactive programming, everything is a data stream. + +With reactive programming, you observe streams and implement side effects when _something_ flows in the stream: + +image::images/diagrams/mermaid-491ff27a51a5.svg[mermaid diagram] + +It's asynchronous by nature as you don't know when the _data_ is going to be seen. +Yet, reactive programming goes beyond this. +It provides a toolbox to compose streams and process events. diff --git a/content/docs/smallrye-mutiny/main/reference/what-makes-mutiny-different.adoc b/content/docs/smallrye-mutiny/main/reference/what-makes-mutiny-different.adoc new file mode 100644 index 000000000..aec919089 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/reference/what-makes-mutiny-different.adoc @@ -0,0 +1,108 @@ += What makes Mutiny different? +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: reference,beginner + +There are other reactive programming libraries out there. +In the Java world, we can mention Project Reactor and Rx Java. + +So, what makes Mutiny different from these two well-known libraries? +*The API!* + +Asynchronous is hard to grasp for most developers, and for good reasons. +Thus, the API must not require advanced knowledge or add cognitive overload. +It should help you design your logic and still be intelligible when you return to the code 6 months later. + +To achieve this, Mutiny is built on three pillars: + +* *Event-Driven* - with Mutiny, you listen for _events_ and handle them, +* *API Navigability* - based on the event-driven nature, the API is built around the type of events and drive the navigation based on the kind of event you want to handle, +* *Simplicity* - Mutiny provides only two types (`Multi` and `Uni`), which can handle any kind of asynchronous interactions. + +== Events? + +When you use Mutiny, you design a pipeline in which the events flow. +Your code observes these events and react. + +Each processing stage is a new pipe you append to the pipeline. +This pipe may change the events, create new ones, drops, buffers, whatever you need. + +In general, events flow from upstream to downstream, from source to sinks. +Some events can _swim_ upstream from the sinks to the source. + +Events going from upstream to downstream are published by `Publishers` and consumed by (downstream) `Subscribers,` which may also produce events for their own downstream, as illustrated by the following diagram: + +image::images/diagrams/mermaid-f71496c9eae5.svg[mermaid diagram] + +Four types of events can flow in this direction: + +* *Subscribed* - indicates that the upstream has taken into account the subscription - more on this later, +* *Items* - events containing some (business) _value_, +* *Completion* - event indicating that the source won't emit any more items, +* *Failure* - event telling that something terrible happened upstream and that the source cannot continue to emit items. + +`Failure` and `Completion` are terminal events. +Once they are sent, no more items will flow. + +Three types of events flow in the opposite direction, _i.e._ from downstream to upstream: + +* *Subscription* - event sent by a _subscriber_ to indicate its interest for the events (such as items) emitted by upstream +* *Requests* - event sent by a _subscriber_ indicating how many items event it can handle - this is related to back-pressure +* *Cancellation* - event sent by a _subscriber_ to stop the reception of events. + +In a typical scenario: + +[arabic] +. A subscriber _subscribes_ to the upstream - the upstream receive the `subscription request`, and when initialized sends the `subscribed` event to the subscriber +. The subscriber gets the `subscribed` event with a _subscription_ used to emit the `requests` and `cancellation` events +. The subscriber sends a `request` event indicating how many items it can handle at this moment; it can request 1, _n_, or infinite. +. The publisher receiving the `request` event starts emitting at most _n_ item events to the subscriber +. The subscriber can decide at any time to request more events or to cancel the subscription + +image::images/diagrams/mermaid-0277dcc2b534.svg[mermaid diagram] + +The `request` event is the cornerstone of the back-pressure protocol. +A subscriber should not request more than what it can handle, and a publisher should not emit more items than the amount of request received. + +[NOTE] +==== + +Mutiny uses the https://www.reactive-streams.org/[Reactive Streams] protocol for back-pressure management and the https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/concurrent/Flow.html[Java Flow APIs]. + +==== + +[IMPORTANT] +==== + +Don't forget to subscribe! + +If no subscriber _subscribes_, no items will be emitted. +More importantly, nothing will ever happen. + +If your program does not do anything, check that it subscribes, it's a very common error. + +==== + +== An event-driven API? + +Mutiny is an event-driven API. + +For each type of event, there is an `on` associated method that lets you handle this specific event. +For example: + +[source,java] +---- +include::{examples-dir}/java/guides/EventsTest.java[tags=code,indent=0] +---- + +Of course, the methods presented in this snippet are not very interesting, although they are quite useful to trace what's going on. + +You can see a common pattern emerging: + +[source,java] +---- +.onEvent().invoke(event -> ...); +---- + +`invoke` is just one of the methods available. +Each _group_ proposes methods specific to the type of event. For example, `onFailure().recover`, `onCompletion().continueWith` and so on. diff --git a/content/docs/smallrye-mutiny/main/reference/why-is-asynchronous-important.adoc b/content/docs/smallrye-mutiny/main/reference/why-is-asynchronous-important.adoc new file mode 100644 index 000000000..b06a0d807 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/reference/why-is-asynchronous-important.adoc @@ -0,0 +1,45 @@ += Why is asynchronous important? +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: reference,beginner + +We are living in a distributed world. + +Most of the applications built nowadays are distributed systems. +The Cloud, IoT, microservices, mobile application, even simple CRUD applications are distributed applications. + +Still, developing distributed systems is hard! + +.Distributed systems are asynchronous +image::reference/distributed_systems_are_asynchronous.png[Distributed systems are asynchronous] + +Communications in distributed systems are inherently asynchronous and unreliable. +Anything can go wrong, anytime, and often with no prior notice. + +Network disruptions, unavailable services, software, or hardware failures are just a tiny subset of the wide variety of failures that can happen in distributed systems. + +_Correctly_ building distributed applications is a considerable challenge, as it requires re-assessing almost everything we know from traditional software development. + +Most classic applications use a synchronous development model. +Synchronous code is easy to reason about, more comfortable to write and read than asynchronous code, but it has some hidden cost. +This cost emerges when building I/O intensive applications, quite common in distributed applications. + +In general, these traditional applications assign one thread per request, and so they handle multiple concurrent requests with multiple threads. +When the request processing needs to interact over the network, it uses that _worker_ thread, which blocks the thread until the response has been received. +This response may never come, so you need to add watchdogs handling timeouts and other resilience patterns. +And, to handle more requests concurrently, you need to create more threads. + +Threads come at a cost. +Each thread requires memory, and the more threads you have, the more CPU cycles are used to handle the context switches. +Thus, this model ends up being costly, limits the deployment density, and on the Cloud means that you pay bigger bills. + +Fortunately, there is another way, and it relies on non-blocking I/O, an efficient way to handle I/O interactions that do not require additional threads. +While applications using non-blocking I/O are more efficient and better suited for the Cloud's distributed nature, they come with a considerable constraint: you must never block the I/O thread. + +Thus, you need to implement your business logic using an asynchronous development model. + +I/O is not the only reason why asynchronous is essential in Today's systems. +Most of the interactions in the real world are asynchronous and event-driven. +Representing these interactions using synchronous processes is not only wrong; it also introduces fragility in your application. + +Asynchronous is a significant shift. +Mutiny helps you to take the plunge. diff --git a/content/docs/smallrye-mutiny/main/tags-index.adoc b/content/docs/smallrye-mutiny/main/tags-index.adoc new file mode 100644 index 000000000..6f43a9375 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/tags-index.adoc @@ -0,0 +1,24 @@ += Index +include::_attributes.adoc[] + +== Document kinds + +[width="100%",cols="18%,82%",options="header",] +|=== +|Tag |Description +|`tutorial` |Tutorials to get started with Mutiny. +|`guides` |Topic-centric guides on how to use Mutiny. +|`reference` |Reference documents to understand core concepts in Mutiny. +|=== + +== Audience level + +[cols=",",options="header",] +|=== +|Tag |Description +|`beginner` |Reading materials if you are new to Mutiny +|`intermediate` |Intermediate operations beyond the basics +|`advanced` |Advanced operations beyond classic usage of Mutiny +|=== + +== Tags diff --git a/content/docs/smallrye-mutiny/main/tutorials/creating-multi-pipelines.adoc b/content/docs/smallrye-mutiny/main/tutorials/creating-multi-pipelines.adoc new file mode 100644 index 000000000..8d551c63f --- /dev/null +++ b/content/docs/smallrye-mutiny/main/tutorials/creating-multi-pipelines.adoc @@ -0,0 +1,152 @@ += Creating `Multi` pipelines +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: tutorial,beginner + +A `Multi` represents a _stream_ of data. +A stream can emit 0, 1, n, or an infinite number of items. + +You will rarely create instances of `Multi` yourself but instead use a reactive client that exposes a Mutiny API. +Still, just like `Uni` there exists a rich API for creating `Multi` objects. + +== The Multi type + +A `Multi++<++T++>++` is a data stream that: + +* emits `0..n` item events +* emits a failure event +* emits a completion event for bounded streams + +[WARNING] +==== + +Failures are terminal events: after having received a failure no further item will be emitted. + +==== + +`Multi++<++T++>++` provides many operators that create, transform, and orchestrate `Multi` sequences. +The operators can be used to define a processing pipeline. +The events flow in this pipeline, and each operator can process or transform the events. + +`Multis` are lazy by nature. +To trigger the computation, you must subscribe. + +The following snippet provides a simple example of pipeline using `Multi`: + +[source,java] +---- +include::{examples-dir}/java/tutorials/CreatingMultiTest.java[tags=pipeline,indent=0] +---- + +== Subscribing to a Multi + +Remember, if you don't subscribe, nothing is going to happen. +Also, the pipeline is materialized for each _subscription_. + +When subscribing to a `Multi,` you can pass an item callback (invoked when the item is emitted), or pass two callbacks, one receiving the item and one receiving the failure, or three callbacks to handle respectively the item, failure and completion events. + +[source,java] +---- +include::{examples-dir}/java/tutorials/CreatingMultiTest.java[tags=subscription,indent=0] +---- + +Note the returned `Cancellable`: this object allows canceling the stream if need be. + +== Creating Multi from items + +There are many ways to create `Multi` instances. +See `Multi.createFrom()` to see all the possibilities. + +For instance, you can create a `Multi` from known items or from an `Iterable`: + +[source,java] +---- +include::{examples-dir}/java/tutorials/CreatingMultiTest.java[tags=simple,indent=0] +---- + +Every subscriber receives the same set of items (`1`, `2`... `5`) just after the subscription. + +You can also use `Suppliers`: + +[source,java] +---- +include::{examples-dir}/java/tutorials/CreatingMultiTest.java[tags=supplier,indent=0] +---- + +The `Supplier` is called for every subscriber, so each of them will get different values. + +[TIP] +==== + +You can create ranges using `Multi.createFrom().range(start, end)`. + +==== + +== Creating failing Multis + +Streams can also fail. + +Failures are used to indicate to the downstream subscribers that the source encountered a terrible error and cannot continue emitting items. +Create failed `Multi` instances with: + +[source,java] +---- +include::{examples-dir}/java/tutorials/CreatingMultiTest.java[tags=failed,indent=0] +---- + +== Creating empty Multis + +Unlike `Uni,` `Multi` streams don't send `null` items (this is forbidden in _reactive streams_). + +Instead `Multi` streams send completion events indicating that there are no more items to consume. +Of course, the completion event can happen even if there are no items, creating an empty stream. + +You can create such a stream using: + +[source,java] +---- +include::{examples-dir}/java/tutorials/CreatingMultiTest.java[tags=empty,indent=0] +---- + +== Creating Multis using an emitter (_advanced_) + +You can create a `Multi` using an emitter. +This approach is useful when integrating callback-based APIs: + +[source,java] +---- +include::{examples-dir}/java/tutorials/CreatingMultiTest.java[tags=emitter,indent=0] +---- + +The emitter can also send a failure. +It can also get notified of cancellation to, for example, stop the work in progress. + +== Creating Multis from _ticks_ (_advanced_) + +You can create a stream that emit a _ticks_ periodically: + +[source,java] +---- +include::{examples-dir}/java/tutorials/CreatingMultiTest.java[tags=ticks,indent=0] +---- + +The downstream receives a `long,` which is a counter. +For the first tick, it's 0, then 1, then 2, and so on. + +== Creating Multis from a generator (_advanced_) + +You can create a stream from some _initial state_, and a _generator function_: + +[source,java] +---- +include::{examples-dir}/java/tutorials/CreatingMultiTest.java[tags=generator,indent=0] +---- + +The initial state is given through a supplier (here `() -++>++ 1`). +The generator function accepts 2 arguments: + +* the current state, +* an emitter that can emit a new item, emit a failure, or emit a completion. + +The generator function return value is the next _current state_. +Running the previous example gives the following number suite: `++{++2, 4, 7, 11, 17, 26, 40, 61}`. diff --git a/content/docs/smallrye-mutiny/main/tutorials/creating-uni-pipelines.adoc b/content/docs/smallrye-mutiny/main/tutorials/creating-uni-pipelines.adoc new file mode 100644 index 000000000..6ffa03df1 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/tutorials/creating-uni-pipelines.adoc @@ -0,0 +1,128 @@ += Creating `Uni` pipelines +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: tutorial,beginner + +A `Uni` represents a _stream_ that can only emit either an item or a failure event. + +You rarely create instances of `Uni` yourself, but, instead, use a reactive client exposing a Mutiny API that provides `Uni` objects. +That being said, it can be handy at times. + +== The Uni type + +A `Uni++<++T++>++` is a specialized stream that emits only an item or a failure. +Typically, `Uni++<++T++>++` are great to represent asynchronous actions such as a remote procedure call, an HTTP request, or an operation producing a single result. + +`Uni++<++T++>++` provides many operators that create, transform, and orchestrate `Uni` sequences. + +As said, `Uni++<++T++>++` emits either an item or a failure. +Note that the item can be `null,` and the `Uni` API has specific methods for this case. + +Typically, a `Uni++<++Void++>++` always emits `null` as item event or a failure if the represented operation fails. +You can consider the item event as a completion signal indicating the success of the operation. + +The offered operators can be used to define a processing pipeline. +The event, either the item or failure, flows in this pipeline, and each operator can process or transform the event. +`Unis` are lazy by nature. + +To trigger the computation, you must have a final subscriber indicating your interest. +The following snippet provides a simple example of pipeline using `Uni`: + +[source,java] +---- +include::{examples-dir}/java/tutorials/CreatingUniTest.java[tags=pipeline,indent=0] +---- + +== Subscribing to a Uni + +[IMPORTANT] +==== + +Remember: if you don't subscribe, nothing is going to happen. +What's more, the pipeline is materialized for each _subscription_. + +==== + +When subscribing to a `Uni`, you can pass an item callback (invoked when the item is emitted), or two callbacks (one receiving the item and one receiving the failure): + +[source,java] +---- +include::{examples-dir}/java/tutorials/CreatingUniTest.java[tags=subscription,indent=0] +---- + +Note the returned `Cancellable`: this object allows canceling the operation if need be. + +== Creating Unis from items + +There are many ways to create `Uni` instances. +Use `Uni.createFrom()` to see all the possibilities. + +You can, for instance, create a `Uni` from a known value: + +[source,java] +---- +include::{examples-dir}/java/tutorials/CreatingUniTest.java[tags=simple,indent=0] +---- + +Every subscriber receives the item `1` just after the subscription. + +You can also pass a `Supplier`: + +[source,java] +---- +include::{examples-dir}/java/tutorials/CreatingUniTest.java[tags=supplier,indent=0] +---- + +The `Supplier` is called for every subscriber. +So, each of them will get a different value. + +== Creating failing Unis + +Operations represented by `Unis` can also emit a failure event, indicating that the operation failed. + +You can create failed `Uni` instances with: + +[source,java] +---- +include::{examples-dir}/java/tutorials/CreatingUniTest.java[tags=failed,indent=0] +---- + +== Creating `Uni++<++Void++>++` + +When the represented operation to not produce a result, you still need a way to indicate the operation's completion. +For this, you need to emit a `null` item: + +[source,java] +---- +include::{examples-dir}/java/tutorials/CreatingUniTest.java[tags=null,indent=0] +---- + +== Creating Unis using an emitter (_advanced_) + +You can create a `Uni` using an emitter. +This approach is useful when integrating callback-based APIs: + +[source,java] +---- +include::{examples-dir}/java/tutorials/CreatingUniTest.java[tags=emitter,indent=0] +---- + +The emitter can also send a failure. +It can also get notified of cancellation to, for example, stop the work in progress. + +== Creating Unis from a CompletionStage (_advanced_) + +You can also `Uni` objects from `CompletionStage` / `CompletableFuture`. +This is useful when integrating with APIs that are based on these types: + +[source,java] +---- +include::{examples-dir}/java/tutorials/CreatingUniTest.java[tags=cs,indent=0] +---- + +[TIP] +==== + +You can also create a `CompletionStage` from a `Uni` using `uni.subscribe().asCompletionStage()` + +==== diff --git a/content/docs/smallrye-mutiny/main/tutorials/getting-mutiny.adoc b/content/docs/smallrye-mutiny/main/tutorials/getting-mutiny.adoc new file mode 100644 index 000000000..651586995 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/tutorials/getting-mutiny.adoc @@ -0,0 +1,142 @@ += Getting started with Mutiny +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: tutorial,beginner + +== Using Mutiny in a Java application + +Add the _dependency_ to your project using your preferred build tool: + +[.tabs] +==== + +Apache Maven:: ++ +-- + +[source,xml,subs="attributes+"] +---- + + io.smallrye.reactive + mutiny + {versions-mutiny} + +---- + +-- + +Gradle (Groovy):: ++ +-- + +[source,groovy,subs="attributes+"] +---- +implementation 'io.smallrye.reactive:mutiny:{versions-mutiny}' +---- + +-- + +Gradle (Kotlin):: ++ +-- + +[source,kotlin,subs="attributes+"] +---- +implementation("io.smallrye.reactive:mutiny:{versions-mutiny}") +---- + +-- + +JBang:: ++ +-- + +[source,java,subs="attributes+"] +---- +//DEPS io.smallrye.reactive:mutiny:{versions-mutiny} +---- + +-- + +==== + +== Using Mutiny with Quarkus + +Most of the https://quarkus.io[Quarkus] extensions with reactive capabilities already depend on Mutiny. + +You can also add the `quarkus-mutiny` dependency explicitly from the command-line: + +[source,bash] +---- +mvn quarkus:add-extension -Dextensions=mutiny +---- + +or by editing the `pom.xml` file and adding: + +[source,xml] +---- + + io.quarkus + quarkus-mutiny + +---- + +== Using Mutiny with Vert.x + +Most of the https://vertx.io[Eclipse Vert.x] stack modules are available through the https://smallrye.io/smallrye-mutiny-vertx-bindings/[SmallRye Mutiny Vert.x Bindings] project. + +Bindings for Vert.x modules are named by prepending `smallrye-mutiny-`. +As an example here's how to add a dependency to the `vertx-core` Mutiny bindings: + +[.tabs] +==== + +Apache Maven:: ++ +-- + +[source,xml,subs="attributes+"] +---- + + io.smallrye.reactive + smallrye-mutiny-vertx-core + {versions-vertx-bindings} + +---- + +-- + +Gradle (Groovy):: ++ +-- + +[source,groovy,subs="attributes+"] +---- +implementation 'io.smallrye.reactive:smallrye-mutiny-vertx-core:{versions-vertx-bindings}' +---- + +-- + +Gradle (Kotlin):: ++ +-- + +[source,kotlin,subs="attributes+"] +---- +implementation("io.smallrye.reactive:smallrye-mutiny-vertx-core:{versions-vertx-bindings}") +---- + +-- + +JBang:: ++ +-- + +[source,java,subs="attributes+"] +---- +//DEPS io.smallrye.reactive:smallrye-mutiny-vertx-core:{versions-vertx-bindings} +---- + +-- + +==== diff --git a/content/docs/smallrye-mutiny/main/tutorials/handling-failures.adoc b/content/docs/smallrye-mutiny/main/tutorials/handling-failures.adoc new file mode 100644 index 000000000..5dab71250 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/tutorials/handling-failures.adoc @@ -0,0 +1,96 @@ += Handling failures +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: tutorial,beginner + +Mutiny provides several operators to handle failures. + +Remember, failures are terminal events sent by the observed stream, indicating that something _bad_ happened. +After a failure, no more items are being received. + +When such an event is received, you can: + +* propagate the failure downstream (default), or +* transform the failure into another failure, or +* recover from it by switching to another stream, passing a fallback item, or completing, or +* retrying (covered in the next guide) + +If you don't handle the failure event, it is propagated downstream until a stage handles the failure or reaches the final subscriber. + +[IMPORTANT] +==== + +on `Multi`, a failure cancels the subscription, meaning you will not receive any more items. +The `retry` operator lets you re-subscribe and continue the reception. + +==== + +== Observing failures + +It can be useful to execute some custom action when a failure happens. +For example, you can log the failure: + +[source,java] +---- +include::{examples-dir}/java/tutorials/HandlingFailuresTest.java[tags=invoke,indent=0] +---- + +[TIP] +==== + +You can also execute an asynchronous action using `onFailure().call(Function++<++Throwable, Uni++<++?++>++)`. +The received failure will be propagated downstream when the `Uni` produced by the passed function emits its item. + +==== + +== Transforming failures + +Another useful action on failure is to transform the failure into a _more meaningful_ failure. + +Typically, you can wrap a low-level failure (like an `IOException`) into a business failure (`ServiceUnavailableException`): + +[source,java] +---- +include::{examples-dir}/java/tutorials/HandlingFailuresTest.java[tags=transform,indent=0] +---- + +== Recovering using fallback item(s) + +In general, upon failure, you want to recover. +The first approach is to recover by replacing the failure with an item: + +[source,java] +---- +include::{examples-dir}/java/tutorials/HandlingFailuresTest.java[tags=recover-item,indent=0] +---- + +The second approach receives a `Supplier` to compute the fallback item. +For the downstream, it didn't fail; it gets the fallback item instead. + +However, don't forget that failures are terminal! +So for `Multi`, the downstream receives the fallback item followed by the completion signal, as no more items can be produced. + +== Completing on failure + +When observing a `Multi` you can replace the failure with the completion signal: + +[source,java] +---- +include::{examples-dir}/java/tutorials/HandlingFailuresTest.java[tags=recover-completion,indent=0] +---- + +The downstream won't see the failure, just the completion event. + +== Switching to another stream + +On failure, you may want to switch to an alternate stream. +When the failure is received, it subscribes to this other stream and propagates the items from this stream instead: + +[source,java] +---- +include::{examples-dir}/java/tutorials/HandlingFailuresTest.java[tags=recover-switch,indent=0] +---- + +The `recoverWithUni` and `recoverWithMulti` methods replace the failed upstream with the returned stream. + +The fallback streams must produce the same type of event as the original upstream. diff --git a/content/docs/smallrye-mutiny/main/tutorials/hello-mutiny.adoc b/content/docs/smallrye-mutiny/main/tutorials/hello-mutiny.adoc new file mode 100644 index 000000000..80fab7b4e --- /dev/null +++ b/content/docs/smallrye-mutiny/main/tutorials/hello-mutiny.adoc @@ -0,0 +1,75 @@ += Hello Mutiny! +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: tutorial,beginner + +Once you made Mutiny available to your classpath, you can start writing code. +Let's start with this simple program: + +[source,java] +---- +include::{examples-dir}/java/FirstProgram.java[indent=0] +---- + +This program prints: + +.... +>> HELLO MUTINY +.... + +== Dissecting the pipeline + +What's interesting is how this message is _built_. +We described a processing pipeline taking an item, processing it and finally consuming it. + +First, we create a `Uni`, one of the two types with `Multi` that Mutiny provides. +A `Uni` is a stream emitting either a single item or a failure. + +Here, we create a `Uni` emitting the `"hello"` item. +This is the input of our pipeline. +Then we process this item: + +* we append `" mutiny"`, then +* we make it an uppercase string. + +This forms the processing part of our pipeline, and then we finally *subscribe* to the pipeline. + +This last part is essential. +If you don't have a final subscriber, nothing is going to happen. +Mutiny types are lazy, meaning that you need to express your interest. +If you don't, the computation won't even start. + +[IMPORTANT] +==== + +If your program doesn't do anything, verify that you didn't forget to subscribe! + +==== + +== Mutiny uses a builder API! + +Another important aspect is the pipeline construction. +Appending a new _stage_ to a pipeline returns a new `Uni.` + +The previous program is equivalent to: + +[source,java] +---- +include::{examples-dir}/java/FirstProgramTest.java[tags=uni,indent=0] +---- + +It is fundamental to understand that this program is not equivalent to: + +[source,java] +---- +include::{examples-dir}/java/FirstProgramTest.java[tags=uni2,indent=0] +---- + +This program just prints `"++>>++ hello"`, as it does not use the appended stages and the final subscriber consumes the first `Uni.` + +[WARNING] +==== + +Mutiny APIs are not fluent and each computation stage returns a new object. + +==== diff --git a/content/docs/smallrye-mutiny/main/tutorials/mutiny-workshop.adoc b/content/docs/smallrye-mutiny/main/tutorials/mutiny-workshop.adoc new file mode 100644 index 000000000..99fcf5a81 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/tutorials/mutiny-workshop.adoc @@ -0,0 +1,15 @@ += Go further with the Mutiny workshop! +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: tutorial,beginner + +One great option to teach yourself Mutiny is to go through the https://github.com/smallrye/smallrye-mutiny/tree/main/workshop-examples[Mutiny workshop examples]. + +These self-contained https://jbang.dev/[JBang] scripts cover the main parts of the Mutiny APIs. + +It's a fun and easy way to discover Mutiny! + +Check out https://github.com/smallrye/smallrye-mutiny/tree/main/workshop-examples to learn more. + +.Running a workshop sample +image::tutorials/running-workshop-sample.png[Running a workshop sample,width=400] diff --git a/content/docs/smallrye-mutiny/main/tutorials/observing-events.adoc b/content/docs/smallrye-mutiny/main/tutorials/observing-events.adoc new file mode 100644 index 000000000..3d9e1ca28 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/tutorials/observing-events.adoc @@ -0,0 +1,111 @@ += Observing events +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: tutorial,beginner + +Learn how to observe the events emitted by `Uni` and `Multi` instances. + +== Events + +`Uni` and `Multi` emit _events_. +Your code is going to observe and process these events. + +Most of the time, your code is only interested in item and failure events. +But there are other kinds of events such as cancellation, request, completion, and so on: + +[width="100%",cols=">12%,9%,<15%,<64%",options="header",] +|=== +|Event |Uni / Multi |Direction |Note +|*item* |Uni {plus} Multi |upstream -++>++ downstream |The upstream sent an item. +|*failure* |Uni {plus} Multi |upstream -++>++ downstream |The upstream failed. +|*completion* |Multi |upstream -++>++ downstream |The upstream completed. +|*subscribe* |Uni and Multi |downstream -++>++ upstream |A downstream subscriber is interested in the data. +|*subscription* |Uni and Multi |upstream -++>++ downstream |Event happening after a `subscribe` event to indicate that the upstream acknowledged the subscription. +|*cancellation* |Uni and Multi |downstream -++>++ upstream |A downstream subscriber does not want any more events. +|*overflow* |Multi |upstream -++>++ downstream |The upstream has emitted more than the downstream can handle. +|*request* |Multi |downstream -++>++ upstream |The downstream indicates its capacity to handle `n` items. +|=== + +It’s not rare that you need to look at these various events to understand better what’s going on or implement specific side effects. +For example, you may need to close a resource after a completion event or log a message on failure or cancellation. + +For each kind of event, there is an associated group providing the methods to handle that specific event: `onItem()`, `onFailure()`, `onCompletion()` and so on. +These groups provide two methods to _peek_ at the various events without impacting its distribution: `invoke(...)` and `call(...)`. +It does not transform the received event; it notifies you that something happened and let you react. +Once this _reaction_ completes, the event is propagated downstream or upstream depending on the direction of the event. + +== The `invoke` method + +The invoke method is synchronous and the passed callback does not return anything. +Mutiny invokes the configured callback when the observed stream dispatches the event: + +[source,java] +---- +include::{examples-dir}/java/tutorials/ObserveTest.java[tags=invoke,indent=0] +---- + +As said above, `invoke` is synchronous. +Mutiny invokes the callback and propagates the event downstream when the callback returns. +It blocks the dispatching. + +image::images/diagrams/mermaid-e88e1918cd26.svg[mermaid diagram] + +Of course, we highly recommend you not to block. + +The following snippets show how you can log the different types of events. + +[source,java] +---- +include::{examples-dir}/java/tutorials/ObserveTest.java[tags=invoke-all,indent=0] +---- + +The arrows from the previous code snippet indicate if the event comes from the upstream (source) or downstream (consumer) (see the table above for more details). +The `invoke` method does not change the event, except in one case. +If the callback throws an exception, the downstream does not get the actual event but get a failure event instead. + +When observing the failure event, if the callback throws an exception, Mutiny propagates a `CompositeException` aggregating the original failure and the callback failure. + +== The `call` method + +Unlike `invoke`, `call` is asynchronous, and the callback returns a `Uni++<++?++>++` object. + +`call` is often used when you need to implement asynchronous side-effects, such as closing resources. + +image::images/diagrams/mermaid-bc092005fd3f.svg[mermaid diagram] + +Mutiny does not dispatch the original event downstream until the Uni returned by the callback emits an item: + +[source,java] +---- +include::{examples-dir}/java/tutorials/ObserveTest.java[tags=call,indent=0] +---- + +As shown in the previous snippet, you can use this approach to delay items. +But, the primary use case is about completing asynchronous actions such as calling an asynchronous `close` method on a resource: + +[source,java] +---- +include::{examples-dir}/java/tutorials/ObserveTest.java[tags=close,indent=0] +---- + +Under the hood, Mutiny gets the `Uni` (by invoking the callback) and subscribes to it. +It observes the item or failure event from that Uni. +It discards the item value as only the emission matters in this case. + +If the callback throws an exception or the produced `Uni` produces a failure, Mutiny propagates that failure (or a `CompositeException`) downstream, replacing the original event. + +== Summary + +* The `invoke` and `call` methods are handy when you need to observe a `Uni` or a `Multi` without changing the transiting events. +* Use `invoke` for implementing synchronous side-effects or logging events. +* The asynchronous nature of `call` makes it perfect for implementing asynchronous side-effects, such as closing resources, flushing data, delay items, and so on. + +The following table highlights the key differences: + +[width="100%",cols=">20%,<33%,<47%",options="header",] +|=== +| |`invoke` |`call` +|*Nature* |synchronous |asynchronous +|*Return type* |`void` |`Uni++<++T++>++` +|*Main use cases* |logging, synchronous side-effects |I/O operations, closing resources, flushing data +|=== diff --git a/content/docs/smallrye-mutiny/main/tutorials/retrying.adoc b/content/docs/smallrye-mutiny/main/tutorials/retrying.adoc new file mode 100644 index 000000000..c2ec63ecc --- /dev/null +++ b/content/docs/smallrye-mutiny/main/tutorials/retrying.adoc @@ -0,0 +1,67 @@ += Retrying on failures +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: tutorial,beginner + +It is common to want to retry if something terrible happened. + +You can retry upon failure. +The https://quarkus.io/blog/uni-retry/[How does retry... retries] blog post provides a more detailed overview of the retry mechanism. + +[NOTE] +==== + +If despite multiple attempts, it still fails, the failure is propagated downstream. + +==== + +== Retry multiple times + +To retry on failure, use `onFailure().retry()`: + +[source,java] +---- +include::{examples-dir}/java/tutorials/RetryTest.java[tags=retry-at-most,indent=0] +---- + +You pass the number of retries as a parameter. + +[IMPORTANT] +==== + +While `.onFailure().retry().indefinitely()` is available, it may never terminate, so use it with caution. + +==== + +== Introducing delays + +By default, `retry` retries immediately. +When using remote services, it is often better to delay a bit the attempts. + +Mutiny provides a method to configure an exponential backoff: a growing delay between retries. +Configure the exponential backoff as follows: + +[source,java] +---- +include::{examples-dir}/java/tutorials/RetryTest.java[tags=retry-backoff,indent=0] +---- + +The backoff is configured with the initial and max delay. +Optionally, you can also configure a jitter to add a pinch of randomness to the delay. + +When using exponential backoff, you may not want to configure the max number of attempts (`atMost`), but a deadline. +To do so, use either `expireIn` or `expireAt`. + +== Deciding to retry + +As an alternative to `atMost`, you can also use `until`. +This method accepts a predicate called after every failure. +When used, a backoff should not be used. + +If the predicate returned `true,` it retries. +Otherwise, it stops retrying and propagates the last failure downstream: + +[source,java] +---- +include::{examples-dir}/java/tutorials/RetryTest.java[tags=retry-until,indent=0] +---- diff --git a/content/docs/smallrye-mutiny/main/tutorials/running-workshop-sample.png b/content/docs/smallrye-mutiny/main/tutorials/running-workshop-sample.png new file mode 100644 index 000000000..6eec15382 Binary files /dev/null and b/content/docs/smallrye-mutiny/main/tutorials/running-workshop-sample.png differ diff --git a/content/docs/smallrye-mutiny/main/tutorials/transforming-items-asynchronously.adoc b/content/docs/smallrye-mutiny/main/tutorials/transforming-items-asynchronously.adoc new file mode 100644 index 000000000..a84902200 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/tutorials/transforming-items-asynchronously.adoc @@ -0,0 +1,133 @@ += Transforming items asynchronously +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: tutorial,beginner + +The previous examples illustrated how to transform each item from a stream into another item. +Yet, there are cases where we need to go beyond this, for example: + +* Transforming an item into a `Uni` -- any asynchronous processing such as calling a remote service, interacting with a database, etc +* Transforming an item into a `Multi` -- producing a multi-items stream based on the incoming item, filtering out items, etc + +Having the possibility to transform an item into a stream gives us many opportunities. +To implement such transformations, we use `onItem().transformToUni(Function++<++T, Uni++<++O++>>++)` and `onItem().transformToMulti(Function++<++T, Multi++<++O++>>++)` + +== Uni - Transforming an item into a Uni + +image::images/diagrams/mermaid-f67adb6018b3.svg[mermaid diagram] + +Imagine that you have a `Uni++<++String++>++`, and you want to call a remote service. + +Calling a remote service is an asynchronous action represented by a `Uni`, as in: + +[source,java] +---- +include::{examples-dir}/java/tutorials/TransformItemsAsyncTest.java[tags=service,indent=0] +---- + +To call this service, you need to transform the item received from the first `Uni` into the `Uni` returned by the service: + +[source,java] +---- +include::{examples-dir}/java/tutorials/TransformItemsAsyncTest.java[tags=call,indent=0] +---- + +This snippet _chains_ the first `Uni` with another one. +The returned `Uni` (`result`) emits the result from the remote service or a failure if anything wrong happened: + +[source,java] +---- +include::{examples-dir}/java/tutorials/TransformItemsAsyncTest.java[tags=chain,indent=0] +---- + +== Uni - Transforming an item into a Multi + +The previous example produced a single item. +You may want to transform the received item into a stream which is... a `Multi`! + +[source,java] +---- +include::{examples-dir}/java/tutorials/TransformItemsAsyncTest.java[tags=uni-to-multi,indent=0] +---- + +This code creates a stream of two elements, duplicating the received item. + +[source,java] +---- +include::{examples-dir}/java/tutorials/TransformItemsAsyncTest.java[tags=uni-to-multi-2,indent=0] +---- + +image::images/diagrams/mermaid-3b8d4c58438a.svg[mermaid diagram] + +The produced `Multi` objects can of course be more complicated than that and emit items in an asynchronous fashion. + +== Transforming items from Multi - the _merge_ vs _concatenate_ dilemma + +When transforming items emitted by an upstream `Multi,` we need to answer the following question: how are we going to _merge_ the produced items back? + +Let's take an example. +Imagine a `Multi` emitting the `Cameron` and `Donna` items (in order), and you want to call the `invokeRemoteGreetingService` from above. +It thus calls `invokeRemoteGreetingService("Cameron")` then `invokeRemoteGreetingService("Donna")`. + +The service does not have a constant response time (because of network latency or the load), which means that responses can be interleaved. +Indeed, you may receive `"Hello Donna"` before `"Hello Cameron"`. + +Now, how do you want to handle this case? +Do you need to preserve the order and ensure that the downstream subscriber will always get `"Hello Cameron"` first, or do you accept interleaved responses? + +When transforming items from `Multi` into streams, you need to decide in which order the items emitted by the produced stream are going to be received by the downstream subscriber. +Mutiny offers two possibilities: + +[arabic] +. *Merging* -- it does not preserve the order and emits the items from the produced streams as they come, or +. *Concatenating* -- it maintains and concatenates the streams produced for each item. + +== Multi - Transforming an item into a Uni + +To implement the scenario from the last section, you will use `onItem().transformToUniAndMerge` or `onItem().transformToUniAndConcatenate()` depending on your ordering choice: + +[source,java] +---- +include::{examples-dir}/java/tutorials/TransformItemsAsyncTest.java[tags=merge-concat,indent=0] +---- + +[IMPORTANT] +==== + +* When merging: items from the source `Multi` _may_ be processed *concurrently* depending on the concurrency level that has been set, if any. +* When concatenating: items from the source `Multi` are processed *in order*, waiting for each `Uni` to complete before moving on to the next item. + +==== + +=== Controlling concurrency with merge + +The `merge` method accepts an optional `concurrency` parameter that limits how many inner streams can be subscribed to concurrently: + +[source,java] +---- +include::{examples-dir}/java/tutorials/TransformItemsAsyncTest.java[tags=merge-concurrency,indent=0] +---- + +When not provided, the default concurrency is configured using `Infrastructure.getBufferSizeS()`. + +[WARNING] +.Merge concurrency +==== + +When using merge with limited concurrency, be aware of potential backpressure issues. +*Setting concurrency too low* can cause upstream request starvation if the number of subscribed but not emitting inner streams surpasses the level of concurrency. + +*Unbounded concurrency* eliminates the request starvation issue by removing the limit on the number of subscribed inner streams to merge. + +==== + +== Multi - Transforming an item into a Multi + +`onItem().transformToMultiAndMerge` and `onItem().transformToMultiAndConcatenate` transform incoming items into `Multi` streams. +The produced `Multi` objects are either _merged_ or _concatenated_: + +[source,java] +---- +include::{examples-dir}/java/tutorials/TransformItemsAsyncTest.java[tags=merge-concat-multi,indent=0] +---- + +Like after `transformToUni`, the `merge` method after `transformToMulti` also accepts an optional concurrency parameter with the same considerations regarding backpressure and request starvation when used with infinite streams. diff --git a/content/docs/smallrye-mutiny/main/tutorials/transforming-items.adoc b/content/docs/smallrye-mutiny/main/tutorials/transforming-items.adoc new file mode 100644 index 000000000..f13a92f76 --- /dev/null +++ b/content/docs/smallrye-mutiny/main/tutorials/transforming-items.adoc @@ -0,0 +1,53 @@ += Transforming items +:version-root: ../ +include::{version-root}_attributes.adoc[] +:topics: tutorial,beginner + +Both `Unis` and `Multis` emit _items_. + +One of the most common operations you will do is transforming these items using a _synchronous_ 1-to-1 function. + +To achieve this, you use `onItem().transform(Function++<++T, U++>++)`. +It calls the passed function for each item and produces the result as an item which is propagated downstream. + +image::images/diagrams/mermaid-e6dd95e32a3f.svg[mermaid diagram] + +== Transforming items produced by a Uni + +Let's imagine you have a `Uni++<++String++>++,` and you want to capitalize the received `String`. +Implementing this transformation is done as follows: + +[source,java] +---- +include::{examples-dir}/java/tutorials/TransformItemsTest.java[tags=uni-transform,indent=0] +---- + +== Transforming items produced by a Multi + +The only difference for `Multi` is that the function is called for each item: + +[source,java] +---- +include::{examples-dir}/java/tutorials/TransformItemsTest.java[tags=multi-transform,indent=0] +---- + +The produced items are passed to the downstream subscriber: + +[source,java] +---- +include::{examples-dir}/java/tutorials/TransformItemsTest.java[tags=multi-transform-2,indent=0] +---- + +== What if the transformation failed? + +If the transformation throws an exception, that exception is caught and passed to the downstream subscriber as a _failure_ event. +It also means that the subscriber won't get further item after that failure. + +== Chaining multiple transformations + +You can chain multiple transformations: + +[source,java] +---- +include::{examples-dir}/java/tutorials/TransformItemsTest.java[tags=chain,indent=0] +---- diff --git a/content/docs/smallrye-reactive-messaging/3/_attributes.adoc b/content/docs/smallrye-reactive-messaging/3/_attributes.adoc new file mode 100644 index 000000000..34b36884a --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_attributes.adoc @@ -0,0 +1,26 @@ +:project-version: 3.25.0 +// Attributes shared by every page. smallrye.io overrides the ones it needs when it publishes. +// Pages in subdirectories set :version-root: (../ per level) before including this file. +ifndef::version-root[:version-root:] +:idprefix: +:idseparator: - +:sectanchors: +:examples-dir: {version-root}_examples +:partialsdir: {version-root}_partials +:project-name: SmallRye Reactive Messaging +:javadoc-version: latest +:version: 3.25.0 +:generated-dir: {version-root}_generated + +// Added by smallrye.io (tools/docs/Docs.java sync) +:javadoc-version: 3.25.0 +:relfileprefix: /docs/smallrye-reactive-messaging/3/ +:imagesdir: /docs/smallrye-reactive-messaging/3 +:docs-project: smallrye-reactive-messaging +:docs-version: 3 +:docs-ref: 3.25.0 +:weld-version: 3.1.8.Final +:smallrye-config-version: 2.11.1 +:mutiny-version: 1.7.0 +:camel-version: 3.20.1 +:spec-version: 2.0.1 diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/ack/AutoNack.java b/content/docs/smallrye-reactive-messaging/3/_examples/ack/AutoNack.java new file mode 100644 index 000000000..bdb5a371a --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/ack/AutoNack.java @@ -0,0 +1,55 @@ +package ack; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Uni; + +public class AutoNack { + + // tag::auto-nack[] + @Incoming("data") + @Outgoing("out") + public String process(String s) { + if (s.equalsIgnoreCase("b")) { + // Throwing an exception triggers a nack + throw new IllegalArgumentException("b"); + } + + if (s.equalsIgnoreCase("e")) { + // Returning null would skip the message (it will be acked) + return null; + } + + return s.toUpperCase(); + } + + @Incoming("data") + @Outgoing("out") + public Uni processAsync(String s) { + if (s.equalsIgnoreCase("a")) { + // Returning a failing Uni triggers a nack + return Uni.createFrom().failure(new Exception("a")); + } + + if (s.equalsIgnoreCase("b")) { + // Throwing an exception triggers a nack + throw new IllegalArgumentException("b"); + } + + if (s.equalsIgnoreCase("e")) { + // Returning null would skip the message (it will be acked not nacked) + return Uni.createFrom().nullItem(); + } + + if (s.equalsIgnoreCase("f")) { + // returning `null` is invalid for method returning Unis, the message is nacked + return null; + } + + return Uni.createFrom().item(s.toUpperCase()); + } + + // end::auto-nack[] + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/ack/MessageAckExamples.java b/content/docs/smallrye-reactive-messaging/3/_examples/ack/MessageAckExamples.java new file mode 100644 index 000000000..5b4ddf459 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/ack/MessageAckExamples.java @@ -0,0 +1,52 @@ +package ack; + +import java.util.concurrent.CompletableFuture; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +public class MessageAckExamples { + + // tag::with-payload[] + @Incoming("in") + @Outgoing("out") + public Message process(Message in) { + // The acknowledgement is forwarded, when the consumer + // acknowledges the message, `in` will be acknowledged + return in.withPayload(in.getPayload() + 1); + } + // end::with-payload[] + + public void creation() { + // tag::message-creation[] + Message message = Message.of("hello", () -> { + // called when the consumer acknowledges the message + + // return a CompletionStage completed when the + // acknowledgment of the created message is + // completed. + // For immediate ack use: + return CompletableFuture.completedFuture(null); + + }); + // end::message-creation[] + } + + // tag::process[] + @Incoming("in") + @Outgoing("out") + public Message processAndProduceNewMessage(Message in) { + // The acknowledgement is forwarded, when the consumer + // acknowledges the message, `in` will be acknowledged + return Message.of(in.getPayload() + 1, + () -> { + // Called when the consumer acknowledges the message + // ... + // Don't forget to acknowledge the incoming message: + return in.ack(); + }); + } + // end::process[] + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/ack/NackOnEmitter.java b/content/docs/smallrye-reactive-messaging/3/_examples/ack/NackOnEmitter.java new file mode 100644 index 000000000..a79b27469 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/ack/NackOnEmitter.java @@ -0,0 +1,27 @@ +package ack; + +import java.util.concurrent.CompletionStage; + +import javax.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Emitter; + +public class NackOnEmitter { + + // tag::emitter[] + @Inject + @Channel("data") + Emitter emitter; + + public void emitPayload() { + CompletionStage completionStage = emitter.send("hello"); + completionStage.whenComplete((acked, nacked) -> { + if (nacked != null) { + // the processing has failed + } + }); + } + // end::emitter[] + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/ack/PreAckExamples.java b/content/docs/smallrye-reactive-messaging/3/_examples/ack/PreAckExamples.java new file mode 100644 index 000000000..04b336bde --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/ack/PreAckExamples.java @@ -0,0 +1,20 @@ +package ack; + +import org.eclipse.microprofile.reactive.messaging.Acknowledgment; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +public class PreAckExamples { + + // tag::pre[] + @Incoming("in") + @Outgoing("out") + @Acknowledgment(Acknowledgment.Strategy.PRE_PROCESSING) + public String process(String input) { + // The message wrapping the payload is already acknowledged + // The default would have waited the produced message to be + // acknowledged + return input.toUpperCase(); + } + // end::pre[] +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/ack/StreamAckExamples.java b/content/docs/smallrye-reactive-messaging/3/_examples/ack/StreamAckExamples.java new file mode 100644 index 000000000..9ebb7a815 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/ack/StreamAckExamples.java @@ -0,0 +1,30 @@ +package ack; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; +import org.reactivestreams.Publisher; + +import io.smallrye.mutiny.Multi; + +public class StreamAckExamples { + + // tag::message[] + @Incoming("in") + @Outgoing("out") + public Publisher> transform(Multi> stream) { + return stream + .map(message -> message.withPayload(message.getPayload().toUpperCase())); + } + // end::message[] + + // tag::payload[] + @Incoming("in") + @Outgoing("out") + public Publisher transformPayload(Multi stream) { + return stream + // The incoming messages are already acknowledged + .map(String::toUpperCase); + } + // end::payload[] +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/amqp/customization/ClientProducers.java b/content/docs/smallrye-reactive-messaging/3/_examples/amqp/customization/ClientProducers.java new file mode 100644 index 000000000..7ce1bdd47 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/amqp/customization/ClientProducers.java @@ -0,0 +1,34 @@ +package amqp.customization; + +import javax.enterprise.inject.Produces; + +import io.smallrye.common.annotation.Identifier; +import io.vertx.amqp.AmqpClientOptions; +import io.vertx.core.net.PemKeyCertOptions; +import io.vertx.core.net.PemTrustOptions; + +public class ClientProducers { + + // tag::named[] + @Produces + @Identifier("my-options") + public AmqpClientOptions getNamedOptions() { + // You can use the produced options to configure the TLS connection + PemKeyCertOptions keycert = new PemKeyCertOptions() + .addCertPath("./tls/tls.crt") + .addKeyPath("./tls/tls.key"); + PemTrustOptions trust = new PemTrustOptions().addCertPath("./tlc/ca.crt"); + + return new AmqpClientOptions() + .setSsl(true) + .setPemKeyCertOptions(keycert) + .setPemTrustOptions(trust) + .addEnabledSaslMechanism("EXTERNAL") + .setHostnameVerificationAlgorithm("") + .setConnectTimeout(30000) + .setReconnectInterval(5000) + .setContainerId("my-container"); + } + // end::named[] + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/amqp/inbound/AmqpMetadataExample.java b/content/docs/smallrye-reactive-messaging/3/_examples/amqp/inbound/AmqpMetadataExample.java new file mode 100644 index 000000000..8683ccbb7 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/amqp/inbound/AmqpMetadataExample.java @@ -0,0 +1,27 @@ +package amqp.inbound; + +import java.util.Optional; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.amqp.IncomingAmqpMetadata; +import io.vertx.core.json.JsonObject; + +public class AmqpMetadataExample { + + public void metadata() { + Message incoming = Message.of(12.0); + // tag::code[] + Optional metadata = incoming.getMetadata(IncomingAmqpMetadata.class); + metadata.ifPresent(meta -> { + String address = meta.getAddress(); + String subject = meta.getSubject(); + boolean durable = meta.isDurable(); + // Use io.vertx.core.json.JsonObject + JsonObject properties = meta.getProperties(); + // ... + }); + // end::code[] + } + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/amqp/inbound/AmqpPriceConsumer.java b/content/docs/smallrye-reactive-messaging/3/_examples/amqp/inbound/AmqpPriceConsumer.java new file mode 100644 index 000000000..288b20d2c --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/amqp/inbound/AmqpPriceConsumer.java @@ -0,0 +1,15 @@ +package amqp.inbound; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; + +@ApplicationScoped +public class AmqpPriceConsumer { + + @Incoming("prices") + public void consume(double price) { + // process your price. + } + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/amqp/inbound/AmqpPriceMessageConsumer.java b/content/docs/smallrye-reactive-messaging/3/_examples/amqp/inbound/AmqpPriceMessageConsumer.java new file mode 100644 index 000000000..a1d574b5a --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/amqp/inbound/AmqpPriceMessageConsumer.java @@ -0,0 +1,21 @@ +package amqp.inbound; + +import java.util.concurrent.CompletionStage; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; + +@ApplicationScoped +public class AmqpPriceMessageConsumer { + + @Incoming("prices") + public CompletionStage consume(Message price) { + // process your price. + + // Acknowledge the incoming message, marking the AMQP message as `accepted`. + return price.ack(); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/amqp/outbound/AmqpCloudEventMultipleProcessors.java b/content/docs/smallrye-reactive-messaging/3/_examples/amqp/outbound/AmqpCloudEventMultipleProcessors.java new file mode 100644 index 000000000..ce34a0b41 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/amqp/outbound/AmqpCloudEventMultipleProcessors.java @@ -0,0 +1,39 @@ +package amqp.outbound; + +import java.net.URI; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.reactive.messaging.ce.OutgoingCloudEventMetadata; + +@ApplicationScoped +public class AmqpCloudEventMultipleProcessors { + + @Incoming("source") + @Outgoing("processed") + public Message process(Message in) { + return in.addMetadata(OutgoingCloudEventMetadata.builder() + .withId("id-" + in.getPayload()) + .withType("greeting") + .build()); + } + + @SuppressWarnings("unchecked") + @Incoming("processed") + @Outgoing("cloud-events") + public Message process2(Message in) { + OutgoingCloudEventMetadata metadata = in + .getMetadata(OutgoingCloudEventMetadata.class) + .orElseGet(() -> OutgoingCloudEventMetadata.builder().build()); + + return in.addMetadata(OutgoingCloudEventMetadata.from(metadata) + .withSource(URI.create("source://me")) + .withSubject("test") + .build()); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/amqp/outbound/AmqpCloudEventProcessor.java b/content/docs/smallrye-reactive-messaging/3/_examples/amqp/outbound/AmqpCloudEventProcessor.java new file mode 100644 index 000000000..c5c9e0056 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/amqp/outbound/AmqpCloudEventProcessor.java @@ -0,0 +1,25 @@ +package amqp.outbound; + +import java.net.URI; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.reactive.messaging.ce.OutgoingCloudEventMetadata; + +@ApplicationScoped +public class AmqpCloudEventProcessor { + + @Outgoing("cloud-events") + public Message toCloudEvents(Message in) { + return in.addMetadata(OutgoingCloudEventMetadata.builder() + .withId("id-" + in.getPayload()) + .withType("greetings") + .withSource(URI.create("http://example.com")) + .withSubject("greeting-message") + .build()); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/amqp/outbound/AmqpOutboundDynamicAddressExample.java b/content/docs/smallrye-reactive-messaging/3/_examples/amqp/outbound/AmqpOutboundDynamicAddressExample.java new file mode 100644 index 000000000..35f848796 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/amqp/outbound/AmqpOutboundDynamicAddressExample.java @@ -0,0 +1,26 @@ +package amqp.outbound; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.amqp.OutgoingAmqpMetadata; + +public class AmqpOutboundDynamicAddressExample { + public Message metadata(Message incoming) { + + // tag::code[] + String addressName = selectAddressFromIncommingMessage(incoming); + OutgoingAmqpMetadata metadata = OutgoingAmqpMetadata.builder() + .withAddress(addressName) + .withDurable(true) + .build(); + + // Create a new message from the `incoming` message + // Add `metadata` to the metadata from the `incoming` message. + return incoming.addMetadata(metadata); + // end::code[] + } + + private String selectAddressFromIncommingMessage(Message incoming) { + return "fake"; + } +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/amqp/outbound/AmqpOutboundMetadataExample.java b/content/docs/smallrye-reactive-messaging/3/_examples/amqp/outbound/AmqpOutboundMetadataExample.java new file mode 100644 index 000000000..3eef8b590 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/amqp/outbound/AmqpOutboundMetadataExample.java @@ -0,0 +1,23 @@ +package amqp.outbound; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.amqp.OutgoingAmqpMetadata; + +public class AmqpOutboundMetadataExample { + + public Message metadata(Message incoming) { + + // tag::code[] + OutgoingAmqpMetadata metadata = OutgoingAmqpMetadata.builder() + .withDurable(true) + .withSubject("my-subject") + .build(); + + // Create a new message from the `incoming` message + // Add `metadata` to the metadata from the `incoming` message. + return incoming.addMetadata(metadata); + // end::code[] + } + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/amqp/outbound/AmqpPriceMessageProducer.java b/content/docs/smallrye-reactive-messaging/3/_examples/amqp/outbound/AmqpPriceMessageProducer.java new file mode 100644 index 000000000..2ea144e14 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/amqp/outbound/AmqpPriceMessageProducer.java @@ -0,0 +1,26 @@ +package amqp.outbound; + +import java.time.Duration; +import java.util.Random; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +@ApplicationScoped +public class AmqpPriceMessageProducer { + + private Random random = new Random(); + + @Outgoing("prices") + public Multi> generate() { + // Build an infinite stream of random prices + // It emits a price every second + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .map(x -> Message.of(random.nextDouble())); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/amqp/outbound/AmqpPriceProducer.java b/content/docs/smallrye-reactive-messaging/3/_examples/amqp/outbound/AmqpPriceProducer.java new file mode 100644 index 000000000..7c74eb374 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/amqp/outbound/AmqpPriceProducer.java @@ -0,0 +1,25 @@ +package amqp.outbound; + +import java.time.Duration; +import java.util.Random; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +@ApplicationScoped +public class AmqpPriceProducer { + + private Random random = new Random(); + + @Outgoing("prices") + public Multi generate() { + // Build an infinite stream of random prices + // It emits a price every second + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .map(x -> random.nextDouble()); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/beans/Chain.java b/content/docs/smallrye-reactive-messaging/3/_examples/beans/Chain.java new file mode 100644 index 000000000..8a44b6d55 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/beans/Chain.java @@ -0,0 +1,27 @@ +package beans; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +public class Chain { + + // tag::chain[] + @Outgoing("source") + public Multi generate() { + return Multi.createFrom().items("Hello", "from", "reactive", "messaging"); + } + + @Incoming("source") + @Outgoing("sink") + public String process(String in) { + return in.toUpperCase(); + } + + @Incoming("sink") + public void consume(String processed) { + System.out.println(processed); + } + // end::chain[] +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/beans/MessageProcessingBean.java b/content/docs/smallrye-reactive-messaging/3/_examples/beans/MessageProcessingBean.java new file mode 100644 index 000000000..e357124dd --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/beans/MessageProcessingBean.java @@ -0,0 +1,20 @@ +package beans; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +@ApplicationScoped +public class MessageProcessingBean { + + @Incoming("consumed-channel") + @Outgoing("populated-channel") + public Message process(Message in) { + // Process the payload + String payload = in.getPayload().toUpperCase(); + // Create a new message from `in` and just update the payload + return in.withPayload(payload); + } +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/beans/PayloadProcessingBean.java b/content/docs/smallrye-reactive-messaging/3/_examples/beans/PayloadProcessingBean.java new file mode 100644 index 000000000..f4df71259 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/beans/PayloadProcessingBean.java @@ -0,0 +1,16 @@ +package beans; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +@ApplicationScoped +public class PayloadProcessingBean { + + @Incoming("consumed-channel") + @Outgoing("populated-channel") + public String process(String in) { + return in.toUpperCase(); + } +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/broadcast/BroadcastExamples.java b/content/docs/smallrye-reactive-messaging/3/_examples/broadcast/BroadcastExamples.java new file mode 100644 index 000000000..53a8bb160 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/broadcast/BroadcastExamples.java @@ -0,0 +1,29 @@ +package broadcast; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.reactive.messaging.annotations.Broadcast; + +public class BroadcastExamples { + + // tag::chain[] + @Incoming("in") + @Outgoing("out") + @Broadcast + public int increment(int i) { + return i + 1; + } + + @Incoming("out") + public void consume1(int i) { + //... + } + + @Incoming("out") + public void consume2(int i) { + //... + } + // end::chain[] + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/broadcast/BroadcastWithCountExamples.java b/content/docs/smallrye-reactive-messaging/3/_examples/broadcast/BroadcastWithCountExamples.java new file mode 100644 index 000000000..e227d7747 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/broadcast/BroadcastWithCountExamples.java @@ -0,0 +1,29 @@ +package broadcast; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.reactive.messaging.annotations.Broadcast; + +public class BroadcastWithCountExamples { + + // tag::chain[] + @Incoming("in") + @Outgoing("out") + @Broadcast(2) + public int increment(int i) { + return i + 1; + } + + @Incoming("out") + public void consume1(int i) { + //... + } + + @Incoming("out") + public void consume2(int i) { + //... + } + // end::chain[] + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/camel/api/CamelApi.java b/content/docs/smallrye-reactive-messaging/3/_examples/camel/api/CamelApi.java new file mode 100644 index 000000000..f145050db --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/camel/api/CamelApi.java @@ -0,0 +1,69 @@ +package camel.api; + +import java.util.concurrent.CompletionStage; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; + +import org.apache.camel.CamelContext; +import org.apache.camel.Exchange; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.component.reactive.streams.api.CamelReactiveStreamsService; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; + +public class CamelApi { + + // tag::reactive[] + @Inject + CamelReactiveStreamsService reactiveStreamsService; + // end::reactive[] + + // tag::source[] + @Outgoing("camel") + public Publisher retrieveDataFromCamelRoute() { + return reactiveStreamsService.from("seda:camel"); + } + // end::source[] + + // tag::source-route-builder[] + @ApplicationScoped + static class MyRouteBuilder extends RouteBuilder { + @Inject + CamelReactiveStreamsService reactiveStreamsService; + + @Outgoing("sink") + public Publisher getDataFromCamelRoute() { + return reactiveStreamsService.fromStream("my-stream", String.class); + } + + @Override + public void configure() { + from("seda:camel").process( + exchange -> exchange.getMessage().setBody(exchange.getIn().getBody(String.class).toUpperCase())) + .to("reactive-streams:my-stream"); + } + } + // end::source-route-builder[] + + // tag::sink[] + @Incoming("to-camel") + public Subscriber sendDataToCamelRoute() { + return reactiveStreamsService.subscriber("file:./target?fileName=values.txt&fileExist=append", + String.class); + } + // end::sink[] + + // tag::producer[] + @Inject + CamelContext camel; + + @Incoming("to-camel") + public CompletionStage sink(String value) { + return camel.createProducerTemplate() + .asyncSendBody("file:./target?fileName=values.txt&fileExist=append", value).thenApply(x -> null); + } + // end::producer[] +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/camel/inbound/CamelFileConsumer.java b/content/docs/smallrye-reactive-messaging/3/_examples/camel/inbound/CamelFileConsumer.java new file mode 100644 index 000000000..ab44e3af2 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/camel/inbound/CamelFileConsumer.java @@ -0,0 +1,20 @@ +package camel.inbound; + +import java.io.File; + +import javax.enterprise.context.ApplicationScoped; + +import org.apache.camel.component.file.GenericFile; +import org.eclipse.microprofile.reactive.messaging.Incoming; + +@ApplicationScoped +public class CamelFileConsumer { + + @Incoming("files") + public void consume(GenericFile gf) { + File file = gf.getFile(); + // process the file + + } + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/camel/inbound/CamelFileMessageConsumer.java b/content/docs/smallrye-reactive-messaging/3/_examples/camel/inbound/CamelFileMessageConsumer.java new file mode 100644 index 000000000..7e2e599e0 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/camel/inbound/CamelFileMessageConsumer.java @@ -0,0 +1,23 @@ +package camel.inbound; + +import java.io.File; +import java.util.concurrent.CompletionStage; + +import javax.enterprise.context.ApplicationScoped; + +import org.apache.camel.component.file.GenericFile; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; + +@ApplicationScoped +public class CamelFileMessageConsumer { + + @Incoming("files") + public CompletionStage consume(Message> msg) { + File file = msg.getPayload().getFile(); + // process the file + + return msg.ack(); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/camel/inbound/IncomingCamelMetadataExample.java b/content/docs/smallrye-reactive-messaging/3/_examples/camel/inbound/IncomingCamelMetadataExample.java new file mode 100644 index 000000000..43f85fd85 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/camel/inbound/IncomingCamelMetadataExample.java @@ -0,0 +1,29 @@ +package camel.inbound; + +import java.io.File; +import java.util.Optional; +import java.util.concurrent.CompletionStage; + +import javax.enterprise.context.ApplicationScoped; + +import org.apache.camel.Exchange; +import org.apache.camel.component.file.GenericFile; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.camel.IncomingExchangeMetadata; + +@ApplicationScoped +public class IncomingCamelMetadataExample { + + @Incoming("files") + public CompletionStage consume(Message> msg) { + Optional metadata = msg.getMetadata(IncomingExchangeMetadata.class); + if (metadata.isPresent()) { + // Retrieve the camel exchange: + Exchange exchange = metadata.get().getExchange(); + } + return msg.ack(); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/camel/outbound/CamelOutboundMetadataExample.java b/content/docs/smallrye-reactive-messaging/3/_examples/camel/outbound/CamelOutboundMetadataExample.java new file mode 100644 index 000000000..ee24ada00 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/camel/outbound/CamelOutboundMetadataExample.java @@ -0,0 +1,31 @@ +package camel.outbound; + +import java.time.Duration; +import java.util.Random; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; +import io.smallrye.reactive.messaging.camel.OutgoingExchangeMetadata; + +@ApplicationScoped +public class CamelOutboundMetadataExample { + + private Random random = new Random(); + + @Outgoing("prices") + public Multi> generate() { + // tag::code[] + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .map(x -> random.nextDouble()) + .map(p -> Double.toString(p)) + .map(s -> Message.of(s) + .addMetadata(new OutgoingExchangeMetadata() + .putProperty("my-property", "my-value"))); + // end::code[] + } + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/camel/outbound/CamelPriceMessageProducer.java b/content/docs/smallrye-reactive-messaging/3/_examples/camel/outbound/CamelPriceMessageProducer.java new file mode 100644 index 000000000..5863ba720 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/camel/outbound/CamelPriceMessageProducer.java @@ -0,0 +1,27 @@ +package camel.outbound; + +import java.time.Duration; +import java.util.Random; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +@ApplicationScoped +public class CamelPriceMessageProducer { + + private Random random = new Random(); + + @Outgoing("prices") + public Multi> generate() { + // Build an infinite stream of random prices + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .map(x -> random.nextDouble()) + .map(p -> Double.toString(p)) + .map(Message::of); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/camel/outbound/CamelPriceProducer.java b/content/docs/smallrye-reactive-messaging/3/_examples/camel/outbound/CamelPriceProducer.java new file mode 100644 index 000000000..ddbe8291e --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/camel/outbound/CamelPriceProducer.java @@ -0,0 +1,26 @@ +package camel.outbound; + +import java.time.Duration; +import java.util.Random; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +@ApplicationScoped +public class CamelPriceProducer { + + private Random random = new Random(); + + @Outgoing("prices") + public Multi generate() { + // Build an infinite stream of random prices + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .onOverflow().drop() + .map(x -> random.nextDouble()) + .map(p -> Double.toString(p)); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/camel/processor/CamelProcessor.java b/content/docs/smallrye-reactive-messaging/3/_examples/camel/processor/CamelProcessor.java new file mode 100644 index 000000000..641f33369 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/camel/processor/CamelProcessor.java @@ -0,0 +1,18 @@ +package camel.processor; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +@ApplicationScoped +public class CamelProcessor { + + @Incoming("mynatssubject") + @Outgoing("mykafkatopic") + public byte[] process(byte[] message) { + // do some logic + return message; + } + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/consumption/ConsumptionExamples.java b/content/docs/smallrye-reactive-messaging/3/_examples/consumption/ConsumptionExamples.java new file mode 100644 index 000000000..2094888b6 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/consumption/ConsumptionExamples.java @@ -0,0 +1,70 @@ +package consumption; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.mutiny.Uni; + +public class ConsumptionExamples { + + //tag::message-cs[] + @Incoming("my-channel") + public CompletionStage consumeMessage(Message message) { + handle(message.getPayload()); + return message.ack(); + } + //end::message-cs[] + + //tag::payload-sync[] + @Incoming("my-channel") + public void consumePayload(Price payload) { + // do something + } + //end::payload-sync[] + + //tag::payload-cs[] + @Incoming("my-channel") + public CompletionStage consumePayloadCS(Price payload) { + CompletionStage cs = handleAsync(payload); + return cs; + } + //end::payload-cs[] + + //tag::payload-uni[] + @Incoming("my-channel") + public Uni consumePayloadUni(Price payload) { + return Uni.createFrom().item(payload) + .onItem().invoke(this::handle) + .onItem().ignore().andContinueWithNull(); + } + //end::payload-uni[] + + //tag::message-uni[] + @Incoming("my-channel") + public Uni consumeMessageUni(Message message) { + return Uni.createFrom().item(message) + .onItem().invoke(m -> handle(m.getPayload())) + .onItem().transformToUni(x -> Uni.createFrom().completionStage(message.ack())); + } + //end::message-uni[] + + private CompletionStage handleAsync(Price price) { + return CompletableFuture.completedFuture(null); + } + + @SuppressWarnings("UnusedReturnValue") + private Price handle(Price price) { + return price; + } + + public static class Price { + final double price; + + public Price(double p) { + this.price = p; + } + } +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/converters/ConverterExample.java b/content/docs/smallrye-reactive-messaging/3/_examples/converters/ConverterExample.java new file mode 100644 index 000000000..a9d3b661e --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/converters/ConverterExample.java @@ -0,0 +1,24 @@ +package converters; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +public class ConverterExample { + + // tag::code[] + @Outgoing("persons") + public Multi source() { + return Multi.createFrom().items("Neo", "Morpheus", "Trinity"); + } + + // The messages need to be converted as they are emitted as Message + // and consumed as Message + @Incoming("persons") + public void consume(Person p) { + // ... + } + // end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/converters/MyConverter.java b/content/docs/smallrye-reactive-messaging/3/_examples/converters/MyConverter.java new file mode 100644 index 000000000..15dfa3eaa --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/converters/MyConverter.java @@ -0,0 +1,31 @@ +package converters; + +import java.lang.reflect.Type; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.MessageConverter; + +// tag::code[] +@ApplicationScoped +public class MyConverter implements MessageConverter { + @Override + public boolean canConvert(Message in, Type target) { + // Checks whether this converter can be used to convert + // the incoming message into a message containing a payload + // of the type `target`. + return in.getPayload().getClass().equals(String.class) + && target.equals(Person.class); + } + + @Override + public Message convert(Message in, Type target) { + // Convert the incoming message into the new message. + // It's important to build the new message **from** + // the received one. + return in.withPayload(new Person((String) in.getPayload())); + } +} +// end::code[] diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/decorators/ConsumedMessageDecorator.java b/content/docs/smallrye-reactive-messaging/3/_examples/decorators/ConsumedMessageDecorator.java new file mode 100644 index 000000000..d9471fe0f --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/decorators/ConsumedMessageDecorator.java @@ -0,0 +1,41 @@ +package decorators; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.mutiny.Multi; +import io.smallrye.reactive.messaging.PublisherDecorator; + +// tag::code[] +@ApplicationScoped +public class ConsumedMessageDecorator implements PublisherDecorator { + + private final Map counters = new HashMap<>(); + + @Override + public Multi> decorate(Multi> publisher, String channelName, + boolean isConnector) { + if (isConnector) { + AtomicLong counter = new AtomicLong(); + counters.put(channelName, counter); + return publisher.onItem().invoke(counter::incrementAndGet); + } else { + return publisher; + } + } + + @Override + public int getPriority() { + return 10; + } + + public long getMessageCount(String channel) { + return counters.get(channel).get(); + } +} +// end::code[] diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/emitter/ChannelExamples.java b/content/docs/smallrye-reactive-messaging/3/_examples/emitter/ChannelExamples.java new file mode 100644 index 000000000..47ca26cd1 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/emitter/ChannelExamples.java @@ -0,0 +1,30 @@ +package emitter; + +import javax.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.reactivestreams.Publisher; + +import io.smallrye.mutiny.Multi; + +public class ChannelExamples { + + // tag::channel[] + @Inject + @Channel("my-channel") + Multi streamOfPayloads; + + @Inject + @Channel("my-channel") + Multi> streamOfMessages; + + @Inject + @Channel("my-channel") + Publisher publisherOfPayloads; + + @Inject + @Channel("my-channel") + Publisher> publisherOfMessages; + // end::channel[] +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/emitter/EmitterExamples.java b/content/docs/smallrye-reactive-messaging/3/_examples/emitter/EmitterExamples.java new file mode 100644 index 000000000..ffafb78b2 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/emitter/EmitterExamples.java @@ -0,0 +1,119 @@ +package emitter; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +import javax.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.*; + +import io.smallrye.reactive.messaging.annotations.Broadcast; +import messages.MyMetadata; + +public class EmitterExamples { + + // tag::payload[] + @Inject + @Channel("prices") + Emitter emitterForPrices; + + public void send(double d) { + emitterForPrices.send(d); + } + // end::payload[] + + // tag::cs[] + public void sendAndAwaitAcknowledgement(double d) { + CompletionStage acked = emitterForPrices.send(d); + // sending a payload returns a CompletionStage completed + // when the message is acknowledged + acked.toCompletableFuture().join(); + } + // end::cs[] + + // tag::message[] + public void sendAsMessage(double d) { + emitterForPrices.send(Message.of(d)); + } + // end::message[] + + // tag::message-ack[] + public void sendAsMessageWithAck(double d) { + emitterForPrices.send(Message.of(d, () -> { + // Called when the message is acknowledged. + return CompletableFuture.completedFuture(null); + }, + reason -> { + // Called when the message is acknowledged negatively. + return CompletableFuture.completedFuture(null); + })); + } + // end::message-ack[] + + // tag::message-meta[] + public void sendAsMessageWithAckAndMetadata(double d) { + MyMetadata metadata = new MyMetadata(); + emitterForPrices.send(Message.of(d, Metadata.of(metadata), + () -> { + // Called when the message is acknowledged. + return CompletableFuture.completedFuture(null); + }, + reason -> { + // Called when the message is acknowledged negatively. + return CompletableFuture.completedFuture(null); + })); + } + // end::message-meta[] + + // tag::broadcast[] + @Inject + @Broadcast + @Channel("prices") + Emitter emitter; + + public void emit(double d) { + emitter.send(d); + } + + @Incoming("prices") + public void handle(double d) { + // Handle the new price + } + + @Incoming("prices") + public void audit(double d) { + // Audit the price change + } + // end::broadcast[] + + static class OverflowExample { + // tag::overflow[] + + // Set the max size to 10 and fail if reached + @OnOverflow(value = OnOverflow.Strategy.BUFFER, bufferSize = 10) + @Inject + @Channel("channel") + Emitter emitterWithBuffer; + + // [DANGER ZONE] no limit + @OnOverflow(OnOverflow.Strategy.UNBOUNDED_BUFFER) + @Inject + @Channel("channel") + Emitter danger; + + // Drop the new messages if the size is reached + @OnOverflow(OnOverflow.Strategy.DROP) + @Inject + @Channel("channel") + Emitter dropping; + + // Drop the previously sent messages if the size is reached + @OnOverflow(OnOverflow.Strategy.LATEST) + @Inject + @Channel("channel") + Emitter dropOldMessages; + + // end::overflow[] + } + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/emitter/EmitterFactoryExamples.java b/content/docs/smallrye-reactive-messaging/3/_examples/emitter/EmitterFactoryExamples.java new file mode 100644 index 000000000..3a2c7b915 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/emitter/EmitterFactoryExamples.java @@ -0,0 +1,87 @@ +package emitter; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.Produces; +import javax.enterprise.inject.spi.InjectionPoint; +import javax.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.reactivestreams.Publisher; + +import io.smallrye.reactive.messaging.ChannelRegistry; +import io.smallrye.reactive.messaging.EmitterConfiguration; +import io.smallrye.reactive.messaging.EmitterFactory; +import io.smallrye.reactive.messaging.EmitterType; +import io.smallrye.reactive.messaging.MessagePublisherProvider; +import io.smallrye.reactive.messaging.annotations.EmitterFactoryFor; +import io.smallrye.reactive.messaging.providers.extension.ChannelProducer; + +public class EmitterFactoryExamples { + + // tag::custom-emitter-declaration[] + + public interface CustomEmitter extends EmitterType { + + > void sendAndForget(M msg); + + } + + public static class CustomEmitterImpl implements CustomEmitter, MessagePublisherProvider { + + Publisher> publisher; + + public CustomEmitterImpl(EmitterConfiguration configuration, long defaultBufferSize) { + //... initialize emitter with configuration + } + + @Override + public Publisher> getPublisher() { + return publisher; + } + + @Override + public > void sendAndForget(M msg) { + //... send to stream + } + } + // end::custom-emitter-declaration[] + + // tag::custom-emitter-factory[] + @EmitterFactoryFor(CustomEmitter.class) + @ApplicationScoped + public static class CustomEmitterFactory implements EmitterFactory> { + + @Inject + ChannelRegistry channelRegistry; + + @Override + public CustomEmitterImpl createEmitter(EmitterConfiguration configuration, long defaultBufferSize) { + return new CustomEmitterImpl<>(configuration, defaultBufferSize); + } + + @Produces + @Channel("") // Stream name is ignored during type-safe resolution + CustomEmitter produce(InjectionPoint injectionPoint) { + String channelName = ChannelProducer.getChannelName(injectionPoint); + return channelRegistry.getEmitter(channelName, CustomEmitter.class); + } + } + // end::custom-emitter-factory[] + + // tag::custom-emitter-usage[] + + @Inject + @Channel("custom-emitter-channel") + CustomEmitter customEmitter; + + //... + + public void emitMessage() { + customEmitter.sendAndForget(Message.of("a")); + customEmitter.sendAndForget(Message.of("b")); + customEmitter.sendAndForget(Message.of("c")); + } + // end::custom-emitter-usage[] + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/emitter/MutinyExamples.java b/content/docs/smallrye-reactive-messaging/3/_examples/emitter/MutinyExamples.java new file mode 100644 index 000000000..957c8d8d0 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/emitter/MutinyExamples.java @@ -0,0 +1,33 @@ +package emitter; + +import javax.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; + +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.subscription.Cancellable; +import io.smallrye.reactive.messaging.MutinyEmitter; + +public class MutinyExamples { + // tag::uni[] + @Inject + @Channel("prices") + MutinyEmitter emitter; + + public Uni send(double d) { + return emitter.send(d); + } + // end::uni[] + + // tag::uni-await[] + public void sendAwait(double d) { + emitter.sendAndAwait(d); + } + // end::uni-await[] + + // tag::uni-forget[] + public Cancellable sendForget(double d) { + return emitter.sendAndForget(d); + } + // end::uni-forget[] +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/emitter/MyImperativeBean.java b/content/docs/smallrye-reactive-messaging/3/_examples/emitter/MyImperativeBean.java new file mode 100644 index 000000000..f281c4f4b --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/emitter/MyImperativeBean.java @@ -0,0 +1,24 @@ +package emitter; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; + +// tag::intro[] +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Emitter; + +@ApplicationScoped +public class MyImperativeBean { + + @Inject + @Channel("prices") + Emitter emitter; + + // ... + + public void send(double d) { + emitter.send(d); + } +} + +// end::intro[] diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/generation/GenerationExamples.java b/content/docs/smallrye-reactive-messaging/3/_examples/generation/GenerationExamples.java new file mode 100644 index 000000000..5b160e7e7 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/generation/GenerationExamples.java @@ -0,0 +1,95 @@ +package generation; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.atomic.AtomicInteger; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; +import org.reactivestreams.Publisher; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; + +public class GenerationExamples { + + AtomicInteger counter = new AtomicInteger(); + + // tag::message-sync[] + @Outgoing("my-channel") + public Message generateMessagesSynchronously() { + return Message.of(counter.getAndIncrement()); + } + // end::message-sync[] + + // tag::message-cs[] + @Outgoing("my-channel") + public CompletionStage> generateMessagesAsCompletionStage() { + return asyncClient.poll() + .thenApply(Message::of); + } + // end::message-cs[] + + public static class Price { + final double price; + + public Price(double p) { + this.price = p; + } + } + + private Pollable asyncClient = () -> CompletableFuture.completedFuture(new Price(10.0)); + + private interface Pollable { + CompletionStage poll(); + } + + // tag::message-uni[] + @Outgoing("my-channel") + public Uni> generateMessagesAsync() { + return Uni.createFrom().item(() -> Message.of(counter.getAndIncrement())); + } + // end::message-uni[] + + // tag::message-stream[] + public Publisher> generateMessageStream() { + Multi multi = reactiveClient.getStream(); + return multi.map(Message::of); + } + // end::message-stream[] + + private ReactiveClient reactiveClient = () -> Multi.createFrom().items("a", "b", "c"); + + private interface ReactiveClient { + Multi getStream(); + } + + // tag::payload-sync[] + @Outgoing("my-channel") + public Integer generatePayloadsSynchronously() { + return counter.getAndIncrement(); + } + // end::payload-sync[] + + // tag::payload-cs[] + @Outgoing("my-channel") + public CompletionStage generatePayloadsAsCompletionStage() { + return asyncClient.poll(); + } + // end::payload-cs[] + + // tag::payload-uni[] + @Outgoing("my-channel") + public Uni generatePayloadsAsync() { + return Uni.createFrom().item(() -> counter.getAndIncrement()); + } + // end::payload-uni[] + + // tag::payload-stream[] + @Outgoing("my-channel") + public Multi generatePayloadsStream() { + Multi multi = reactiveClient.getStream(); + return multi; + } + // end::payload-stream[] +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/incomings/IncomingsExamples.java b/content/docs/smallrye-reactive-messaging/3/_examples/incomings/IncomingsExamples.java new file mode 100644 index 000000000..f1d727df7 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/incomings/IncomingsExamples.java @@ -0,0 +1,16 @@ +package incomings; + +import org.eclipse.microprofile.reactive.messaging.Incoming; + +public class IncomingsExamples { + + //tag::code[] + @Incoming("channel-1") + @Incoming("channel-2") + public String process(String s) { + // get messages from channel-1 and channel-2 + return s.toUpperCase(); + } + //end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/interceptors/MyInterceptor.java b/content/docs/smallrye-reactive-messaging/3/_examples/interceptors/MyInterceptor.java new file mode 100644 index 000000000..c46aec971 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/interceptors/MyInterceptor.java @@ -0,0 +1,30 @@ +package interceptors; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.common.annotation.Identifier; +import io.smallrye.reactive.messaging.OutgoingInterceptor; +import io.smallrye.reactive.messaging.OutgoingMessageMetadata; + +@Identifier("channel-a") +@ApplicationScoped +public class MyInterceptor implements OutgoingInterceptor { + + @Override + public Message onMessage(Message message) { + return message.withPayload("changed " + message.getPayload()); + } + + @Override + public void onMessageAck(Message message) { + message.getMetadata(OutgoingMessageMetadata.class) + .ifPresent(m -> m.getResult()); + } + + @Override + public void onMessageNack(Message message, Throwable failure) { + + } +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/jms/inbound/JmsMetadataExample.java b/content/docs/smallrye-reactive-messaging/3/_examples/jms/inbound/JmsMetadataExample.java new file mode 100644 index 000000000..a50728c52 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/jms/inbound/JmsMetadataExample.java @@ -0,0 +1,25 @@ +package jms.inbound; + +import java.util.Optional; + +import javax.jms.Destination; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.jms.IncomingJmsMessageMetadata; + +public class JmsMetadataExample { + + public void metadata() { + Message incoming = Message.of(12.0); + // tag::code[] + Optional metadata = incoming.getMetadata(IncomingJmsMessageMetadata.class); + metadata.ifPresent(meta -> { + long expiration = meta.getExpiration(); + Destination destination = meta.getDestination(); + String value = meta.getStringProperty("my-property"); + }); + // end::code[] + } + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/jms/inbound/JmsPriceConsumer.java b/content/docs/smallrye-reactive-messaging/3/_examples/jms/inbound/JmsPriceConsumer.java new file mode 100644 index 000000000..b519ef72e --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/jms/inbound/JmsPriceConsumer.java @@ -0,0 +1,15 @@ +package jms.inbound; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; + +@ApplicationScoped +public class JmsPriceConsumer { + + @Incoming("prices") + public void consume(double price) { + // process your price. + } + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/jms/inbound/JmsPriceMessageConsumer.java b/content/docs/smallrye-reactive-messaging/3/_examples/jms/inbound/JmsPriceMessageConsumer.java new file mode 100644 index 000000000..615fcc4f6 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/jms/inbound/JmsPriceMessageConsumer.java @@ -0,0 +1,21 @@ +package jms.inbound; + +import java.util.concurrent.CompletionStage; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; + +@ApplicationScoped +public class JmsPriceMessageConsumer { + + @Incoming("prices") + public CompletionStage consume(Message price) { + // process your price. + + // Acknowledge the incoming message + return price.ack(); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/jms/outbound/JmsOutboundMetadataExample.java b/content/docs/smallrye-reactive-messaging/3/_examples/jms/outbound/JmsOutboundMetadataExample.java new file mode 100644 index 000000000..6142bccf7 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/jms/outbound/JmsOutboundMetadataExample.java @@ -0,0 +1,23 @@ +package jms.outbound; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.jms.JmsProperties; +import io.smallrye.reactive.messaging.jms.OutgoingJmsMessageMetadata; + +public class JmsOutboundMetadataExample { + + public Message metadata(Message incoming) { + + // tag::code[] + OutgoingJmsMessageMetadata metadata = OutgoingJmsMessageMetadata.builder() + .withProperties(JmsProperties.builder().with("some-property", "some-value").build()) + .build(); + + // Create a new message from the `incoming` message + // Add `metadata` to the metadata from the `incoming` message. + return incoming.addMetadata(metadata); + // end::code[] + } + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/jms/outbound/JmsPriceMessageProducer.java b/content/docs/smallrye-reactive-messaging/3/_examples/jms/outbound/JmsPriceMessageProducer.java new file mode 100644 index 000000000..b54aa1c65 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/jms/outbound/JmsPriceMessageProducer.java @@ -0,0 +1,26 @@ +package jms.outbound; + +import java.time.Duration; +import java.util.Random; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +@ApplicationScoped +public class JmsPriceMessageProducer { + + private Random random = new Random(); + + @Outgoing("prices") + public Multi> generate() { + // Build an infinite stream of random prices + // It emits a price every second + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .map(x -> Message.of(random.nextDouble())); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/jms/outbound/JmsPriceProducer.java b/content/docs/smallrye-reactive-messaging/3/_examples/jms/outbound/JmsPriceProducer.java new file mode 100644 index 000000000..8c795d444 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/jms/outbound/JmsPriceProducer.java @@ -0,0 +1,25 @@ +package jms.outbound; + +import java.time.Duration; +import java.util.Random; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +@ApplicationScoped +public class JmsPriceProducer { + + private Random random = new Random(); + + @Outgoing("prices") + public Multi generate() { + // Build an infinite stream of random prices + // It emits a price every second + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .map(x -> random.nextDouble()); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/kafka/companion/ConsumerTest.java b/content/docs/smallrye-reactive-messaging/3/_examples/kafka/companion/ConsumerTest.java new file mode 100644 index 000000000..fe0e60637 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/kafka/companion/ConsumerTest.java @@ -0,0 +1,45 @@ +package kafka.companion; + +import static org.testcontainers.shaded.org.awaitility.Awaitility.await; + +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; + +import org.apache.kafka.common.TopicPartition; +import org.junit.jupiter.api.Test; + +import io.smallrye.reactive.messaging.kafka.companion.ConsumerBuilder; +import io.smallrye.reactive.messaging.kafka.companion.test.KafkaCompanionTestBase; + +public class ConsumerTest extends KafkaCompanionTestBase { + + @Test + public void fromTopics() { + // tag::topics[] + companion.consumeIntegers().fromTopics("topic1", "topic2"); + // end::topics[] + } + + @Test + public void fromOffset() { + // tag::offsets[] + Map offsets = new HashMap<>(); + offsets.put(new TopicPartition("topic1", 0), 100L); + offsets.put(new TopicPartition("topic2", 0), 100L); + companion.consumeIntegers().fromOffsets(offsets, Duration.ofSeconds(10)); + // end::offsets[] + } + + @Test + public void committed() { + // tag::committed[] + ConsumerBuilder consumer = companion.consumeIntegers() + .withAutoCommit(); + consumer.fromTopics("topic"); + // ... + await().untilAsserted(consumer::waitForAssignment); + consumer.committed(new TopicPartition("topic", 1)); + // end::committed[] + } +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/kafka/companion/KafkaTest.java b/content/docs/smallrye-reactive-messaging/3/_examples/kafka/companion/KafkaTest.java new file mode 100644 index 000000000..e10717dc0 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/kafka/companion/KafkaTest.java @@ -0,0 +1,76 @@ +package kafka.companion; + +import java.time.Duration; + +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.apache.kafka.common.serialization.Deserializer; +import org.apache.kafka.common.serialization.Serializer; +import org.junit.jupiter.api.Assertions; + +import io.smallrye.reactive.messaging.kafka.companion.ConsumerBuilder; +import io.smallrye.reactive.messaging.kafka.companion.ConsumerTask; +import io.smallrye.reactive.messaging.kafka.companion.KafkaCompanion; +import io.smallrye.reactive.messaging.kafka.companion.ProducerBuilder; + +public class KafkaTest { + + public void newCompanion() { + // tag::companion[] + // Create companion with bootstrap servers and API timeout (default is 10 seconds) + KafkaCompanion companion = new KafkaCompanion("localhost:9092", Duration.ofSeconds(5)); + + // Create producer and start producer task + ProducerBuilder producer = companion.produceIntegers() + .withClientId("my-producer") + .withProp("max.block.ms", "5000"); + producer.usingGenerator(i -> new ProducerRecord<>("topic", i), 100); + + // Create consumer and start consumer task + ConsumerBuilder consumer = companion.consumeIntegers() + .withGroupId("my-group") + .withCommitAsyncWhen(record -> true); + ConsumerTask records = consumer.fromTopics("topic", Duration.ofSeconds(10)); + // Await completion and assert consumed record count + Assertions.assertEquals(records.awaitCompletion().count(), 100); + // end::companion[] + } + + public void serdes() { + // tag::serdes[] + KafkaCompanion companion = new KafkaCompanion("localhost:9092"); + + // Register serde to the companion + companion.registerSerde(Orders.class, new OrdersSerializer(), new OrdersDeserializer()); + + // Companion will configure consumer accordingly + ConsumerTask orders = companion.consume(Integer.class, Orders.class) + .fromTopics("orders", 1000).awaitCompletion(); + + for (ConsumerRecord order : orders) { + // ... check consumed records + } + // end::serdes[] + } + + public static class Orders { + + } + + public static class OrdersSerializer implements Serializer { + + @Override + public byte[] serialize(String s, Orders orders) { + return new byte[0]; + } + } + + public static class OrdersDeserializer implements Deserializer { + + @Override + public Orders deserialize(String s, byte[] bytes) { + return null; + } + } + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/kafka/companion/KafkaWithBaseTest.java b/content/docs/smallrye-reactive-messaging/3/_examples/kafka/companion/KafkaWithBaseTest.java new file mode 100644 index 000000000..63bd8634a --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/kafka/companion/KafkaWithBaseTest.java @@ -0,0 +1,32 @@ +package kafka.companion; + +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import io.smallrye.reactive.messaging.kafka.companion.ConsumerTask; +import io.smallrye.reactive.messaging.kafka.companion.ProducerTask; +import io.smallrye.reactive.messaging.kafka.companion.test.KafkaCompanionTestBase; + +// tag::code[] +public class KafkaWithBaseTest extends KafkaCompanionTestBase { + + @Test + public void testWithBase() { + // companion is created by the base class + + // produce 100 records to messages topic + ProducerTask producerTask = companion.produceIntegers() + .usingGenerator(i -> new ProducerRecord<>("messages", i), 100); + long msgCount = producerTask.awaitCompletion().count(); + Assertions.assertEquals(msgCount, 100); + + // consume 100 records from messages topic + ConsumerTask consumerTask = companion.consumeIntegers() + .fromTopics("messages", 100); + ConsumerRecord lastRecord = consumerTask.awaitCompletion().getLastRecord(); + Assertions.assertEquals(lastRecord.value(), 99); + } +} +// end::code[] diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/kafka/companion/OffsetsTest.java b/content/docs/smallrye-reactive-messaging/3/_examples/kafka/companion/OffsetsTest.java new file mode 100644 index 000000000..f7b00fa6b --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/kafka/companion/OffsetsTest.java @@ -0,0 +1,36 @@ +package kafka.companion; + +import java.util.Collection; + +import org.apache.kafka.clients.admin.ConsumerGroupListing; +import org.apache.kafka.clients.admin.OffsetSpec; +import org.apache.kafka.common.TopicPartition; +import org.junit.jupiter.api.Test; + +import io.smallrye.reactive.messaging.kafka.companion.KafkaCompanion; +import io.smallrye.reactive.messaging.kafka.companion.test.KafkaCompanionTestBase; + +public class OffsetsTest extends KafkaCompanionTestBase { + + @Test + public void offsets() { + // tag::offsets[] + TopicPartition topicPartition = KafkaCompanion.tp("topic", 1); + long latestOffset = companion.offsets().get(topicPartition, OffsetSpec.latest()).offset(); + // end::offsets[] + } + + @Test + public void consumerGroups() { + // tag::consumerGroups[] + Collection consumerGroups = companion.consumerGroups().list(); + for (ConsumerGroupListing consumerGroup : consumerGroups) { + // check consumer groups + } + + TopicPartition topicPartition = KafkaCompanion.tp("topic", 1); + companion.consumerGroups().resetOffsets("consumer-group", topicPartition); + // end::consumerGroups[] + } + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/kafka/companion/ProducerTest.java b/content/docs/smallrye-reactive-messaging/3/_examples/kafka/companion/ProducerTest.java new file mode 100644 index 000000000..a60561829 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/kafka/companion/ProducerTest.java @@ -0,0 +1,34 @@ +package kafka.companion; + +import org.apache.kafka.clients.producer.ProducerRecord; +import org.junit.jupiter.api.Test; + +import io.smallrye.reactive.messaging.kafka.companion.test.KafkaCompanionTestBase; + +public class ProducerTest extends KafkaCompanionTestBase { + + @Test + public void fromRecords() { + // tag::records[] + companion.produce(byte[].class).fromRecords( + new ProducerRecord<>("topic1", "k1", "1".getBytes()), + new ProducerRecord<>("topic1", "k2", "2".getBytes()), + new ProducerRecord<>("topic1", "k3", "3".getBytes())); + // end::records[] + } + + @Test + public void fromGenerator() { + // tag::generator[] + companion.produceIntegers().usingGenerator(i -> new ProducerRecord<>("topic", i), 10); + // end::generator[] + } + + @Test + public void fromCsv() { + // tag::csv[] + companion.produceStrings().fromCsv("records.csv"); + // end::csv[] + } + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/kafka/companion/TopicsTest.java b/content/docs/smallrye-reactive-messaging/3/_examples/kafka/companion/TopicsTest.java new file mode 100644 index 000000000..01e048611 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/kafka/companion/TopicsTest.java @@ -0,0 +1,20 @@ +package kafka.companion; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import io.smallrye.reactive.messaging.kafka.companion.test.KafkaCompanionTestBase; + +public class TopicsTest extends KafkaCompanionTestBase { + + @Test + public void topics() { + // tag::code[] + companion.topics().create("topic1", 1); + companion.topics().createAndWait("topic2", 3); + Assertions.assertEquals(companion.topics().list().size(), 2); + + companion.topics().delete("topic1"); + // end::code[] + } +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/kafka/inbound/Converters.java b/content/docs/smallrye-reactive-messaging/3/_examples/kafka/inbound/Converters.java new file mode 100644 index 000000000..60282e376 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/kafka/inbound/Converters.java @@ -0,0 +1,30 @@ +package kafka.inbound; + +import javax.enterprise.context.ApplicationScoped; + +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.eclipse.microprofile.reactive.messaging.Incoming; + +import io.smallrye.reactive.messaging.kafka.Record; + +@ApplicationScoped +public class Converters { + + // tag::code[] + @Incoming("topic-a") + public void consume(Record record) { + String key = record.key(); // Can be `null` if the incoming record has no key + String value = record.value(); // Can be `null` if the incoming record has no value + } + + @Incoming("topic-b") + public void consume(ConsumerRecord record) { + String key = record.key(); // Can be `null` if the incoming record has no key + String value = record.value(); // Can be `null` if the incoming record has no value + String topic = record.topic(); + int partition = record.partition(); + // ... + } + // end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/kafka/inbound/KafkaCheckpointExample.java b/content/docs/smallrye-reactive-messaging/3/_examples/kafka/inbound/KafkaCheckpointExample.java new file mode 100644 index 000000000..c677cd843 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/kafka/inbound/KafkaCheckpointExample.java @@ -0,0 +1,31 @@ +package kafka.inbound; + +import java.util.concurrent.CompletionStage; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; + +import io.smallrye.reactive.messaging.kafka.KafkaRecord; +import io.smallrye.reactive.messaging.kafka.commit.CheckpointMetadata; + +@ApplicationScoped +public class KafkaCheckpointExample { + + // tag::code[] + @Incoming("prices") + public CompletionStage consume(KafkaRecord record) { + // Get the `CheckpointMetadata` from the incoming message + CheckpointMetadata checkpoint = CheckpointMetadata.fromMessage(record); + + // `CheckpointMetadata` allows transforming the processing state + // Applies the given function, starting from the value `0.0` when no previous state exists + checkpoint.transform(0.0, current -> current + record.getPayload(), /* persistOnAck */ true); + + // `persistOnAck` flag set to true, ack will persist the processing state + // associated with the latest offset (per partition). + return record.ack(); + } + // end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/kafka/inbound/KafkaCustomCommit.java b/content/docs/smallrye-reactive-messaging/3/_examples/kafka/inbound/KafkaCustomCommit.java new file mode 100644 index 000000000..84b82c217 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/kafka/inbound/KafkaCustomCommit.java @@ -0,0 +1,60 @@ +package kafka.inbound; + +import java.util.Collection; +import java.util.function.BiConsumer; + +import javax.enterprise.context.ApplicationScoped; + +import org.apache.kafka.common.TopicPartition; + +import io.smallrye.common.annotation.Identifier; +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.kafka.IncomingKafkaRecord; +import io.smallrye.reactive.messaging.kafka.KafkaConnectorIncomingConfiguration; +import io.smallrye.reactive.messaging.kafka.KafkaConsumer; +import io.smallrye.reactive.messaging.kafka.commit.KafkaCommitHandler; +import io.vertx.mutiny.core.Vertx; + +public class KafkaCustomCommit implements KafkaCommitHandler { + + @Override + public Uni handle(IncomingKafkaRecord record) { + // called on message ack + return Uni.createFrom().voidItem(); + } + + @Override + public Uni> received(IncomingKafkaRecord record) { + // called before message processing + return Uni.createFrom().item(record); + } + + @Override + public void terminate(boolean graceful) { + // called on channel shutdown + } + + @Override + public void partitionsAssigned(Collection partitions) { + // called on partitions assignment + } + + @Override + public void partitionsRevoked(Collection partitions) { + // called on partitions revoked + } + + @ApplicationScoped + @Identifier("custom") + public static class Factory implements KafkaCommitHandler.Factory { + + @Override + public KafkaCommitHandler create(KafkaConnectorIncomingConfiguration config, + Vertx vertx, + KafkaConsumer consumer, + BiConsumer reportFailure) { + return new KafkaCustomCommit(/* ... */); + } + } + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/kafka/inbound/KafkaDeadLetterExample.java b/content/docs/smallrye-reactive-messaging/3/_examples/kafka/inbound/KafkaDeadLetterExample.java new file mode 100644 index 000000000..9130d8ef2 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/kafka/inbound/KafkaDeadLetterExample.java @@ -0,0 +1,30 @@ +package kafka.inbound; + +import java.nio.charset.StandardCharsets; +import java.util.concurrent.CompletionStage; + +import javax.enterprise.context.ApplicationScoped; + +import org.apache.kafka.common.header.internals.RecordHeaders; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Metadata; + +import io.smallrye.reactive.messaging.kafka.KafkaRecord; +import io.smallrye.reactive.messaging.kafka.api.OutgoingKafkaRecordMetadata; + +@ApplicationScoped +public class KafkaDeadLetterExample { + + // tag::code[] + @Incoming("in") + public CompletionStage consume(KafkaRecord message) { + return message.nack(new Exception("Failed!"), Metadata.of( + OutgoingKafkaRecordMetadata.builder() + .withKey("failed-record") + .withHeaders(new RecordHeaders() + .add("my-header", "my-header-value".getBytes(StandardCharsets.UTF_8))) + .build())); + } + // end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/kafka/inbound/KafkaMetadataExample.java b/content/docs/smallrye-reactive-messaging/3/_examples/kafka/inbound/KafkaMetadataExample.java new file mode 100644 index 000000000..8400fe0df --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/kafka/inbound/KafkaMetadataExample.java @@ -0,0 +1,36 @@ +package kafka.inbound; + +import java.time.Instant; + +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.kafka.api.IncomingKafkaRecordMetadata; + +public class KafkaMetadataExample { + + @SuppressWarnings("unchecked") + public void metadata() { + Message incoming = Message.of(12.0); + // tag::code[] + IncomingKafkaRecordMetadata metadata = incoming.getMetadata(IncomingKafkaRecordMetadata.class) + .orElse(null); + if (metadata != null) { + // The topic + String topic = metadata.getTopic(); + + // The key + String key = metadata.getKey(); + + // The timestamp + Instant timestamp = metadata.getTimestamp(); + + // The underlying record + ConsumerRecord record = metadata.getRecord(); + + // ... + } + // end::code[] + } + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/kafka/inbound/KafkaPriceConsumer.java b/content/docs/smallrye-reactive-messaging/3/_examples/kafka/inbound/KafkaPriceConsumer.java new file mode 100644 index 000000000..e8f951059 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/kafka/inbound/KafkaPriceConsumer.java @@ -0,0 +1,15 @@ +package kafka.inbound; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; + +@ApplicationScoped +public class KafkaPriceConsumer { + + @Incoming("prices") + public void consume(double price) { + // process your price. + } + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/kafka/inbound/KafkaPriceMessageConsumer.java b/content/docs/smallrye-reactive-messaging/3/_examples/kafka/inbound/KafkaPriceMessageConsumer.java new file mode 100644 index 000000000..81c9a1436 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/kafka/inbound/KafkaPriceMessageConsumer.java @@ -0,0 +1,21 @@ +package kafka.inbound; + +import java.util.concurrent.CompletionStage; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; + +@ApplicationScoped +public class KafkaPriceMessageConsumer { + + @Incoming("prices") + public CompletionStage consume(Message price) { + // process your price. + + // Acknowledge the incoming message (commit the offset) + return price.ack(); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/kafka/inbound/KafkaRebalancedConsumer.java b/content/docs/smallrye-reactive-messaging/3/_examples/kafka/inbound/KafkaRebalancedConsumer.java new file mode 100644 index 000000000..155ef895f --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/kafka/inbound/KafkaRebalancedConsumer.java @@ -0,0 +1,23 @@ +package kafka.inbound; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Acknowledgment; +import org.eclipse.microprofile.reactive.messaging.Incoming; + +import io.smallrye.reactive.messaging.kafka.IncomingKafkaRecord; + +@ApplicationScoped +public class KafkaRebalancedConsumer { + + @Incoming("rebalanced-example") + @Acknowledgment(Acknowledgment.Strategy.NONE) + public CompletionStage consume(IncomingKafkaRecord message) { + // We don't need to ACK messages because in this example we set offset during consumer re-balance + return CompletableFuture.completedFuture(null); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/kafka/inbound/KafkaRebalancedConsumerRebalanceListener.java b/content/docs/smallrye-reactive-messaging/3/_examples/kafka/inbound/KafkaRebalancedConsumerRebalanceListener.java new file mode 100644 index 000000000..be10d7e48 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/kafka/inbound/KafkaRebalancedConsumerRebalanceListener.java @@ -0,0 +1,49 @@ +package kafka.inbound; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Logger; + +import javax.enterprise.context.ApplicationScoped; + +import org.apache.kafka.clients.consumer.Consumer; +import org.apache.kafka.clients.consumer.OffsetAndTimestamp; + +import io.smallrye.common.annotation.Identifier; +import io.smallrye.reactive.messaging.kafka.KafkaConsumerRebalanceListener; + +@ApplicationScoped +@Identifier("rebalanced-example.rebalancer") +public class KafkaRebalancedConsumerRebalanceListener implements KafkaConsumerRebalanceListener { + + private static final Logger LOGGER = Logger.getLogger(KafkaRebalancedConsumerRebalanceListener.class.getName()); + + /** + * When receiving a list of partitions will search for the earliest offset within 10 minutes + * and seek the consumer to it. + * + * @param consumer underlying consumer + * @param partitions set of assigned topic partitions + */ + @Override + public void onPartitionsAssigned(Consumer consumer, + Collection partitions) { + long now = System.currentTimeMillis(); + long shouldStartAt = now - 600_000L; //10 minute ago + + Map request = new HashMap<>(); + for (org.apache.kafka.common.TopicPartition partition : partitions) { + LOGGER.info("Assigned " + partition); + request.put(partition, shouldStartAt); + } + Map offsets = consumer + .offsetsForTimes(request); + for (Map.Entry position : offsets.entrySet()) { + long target = position.getValue() == null ? 0L : position.getValue().offset(); + LOGGER.info("Seeking position " + target + " for " + position.getKey()); + consumer.seek(position.getKey(), target); + } + } + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/kafka/inbound/KafkaRecordBatchExample.java b/content/docs/smallrye-reactive-messaging/3/_examples/kafka/inbound/KafkaRecordBatchExample.java new file mode 100644 index 000000000..4553a6e1f --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/kafka/inbound/KafkaRecordBatchExample.java @@ -0,0 +1,45 @@ +package kafka.inbound; + +import java.time.Instant; +import java.util.concurrent.CompletionStage; + +import javax.enterprise.context.ApplicationScoped; + +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.clients.consumer.ConsumerRecords; +import org.apache.kafka.common.TopicPartition; +import org.eclipse.microprofile.reactive.messaging.Incoming; + +import io.smallrye.reactive.messaging.kafka.KafkaRecord; +import io.smallrye.reactive.messaging.kafka.KafkaRecordBatch; +import io.smallrye.reactive.messaging.kafka.api.IncomingKafkaRecordMetadata; + +@ApplicationScoped +public class KafkaRecordBatchExample { + + // tag::code[] + @Incoming("prices") + public CompletionStage consumeMessage(KafkaRecordBatch records) { + for (KafkaRecord record : records) { + record.getMetadata(IncomingKafkaRecordMetadata.class).ifPresent(metadata -> { + int partition = metadata.getPartition(); + long offset = metadata.getOffset(); + Instant timestamp = metadata.getTimestamp(); + //... process messages + }); + } + // ack will commit the latest offsets (per partition) of the batch. + return records.ack(); + } + + @Incoming("prices") + public void consumeRecords(ConsumerRecords records) { + for (TopicPartition partition : records.partitions()) { + for (ConsumerRecord record : records.records(partition)) { + //... process messages + } + } + } + // end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/kafka/inbound/KafkaRecordBatchPayloadExample.java b/content/docs/smallrye-reactive-messaging/3/_examples/kafka/inbound/KafkaRecordBatchPayloadExample.java new file mode 100644 index 000000000..79e23002b --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/kafka/inbound/KafkaRecordBatchPayloadExample.java @@ -0,0 +1,21 @@ +package kafka.inbound; + +import java.util.List; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; + +@ApplicationScoped +public class KafkaRecordBatchPayloadExample { + + // tag::code[] + @Incoming("prices") + public void consume(List prices) { + for (double price : prices) { + // process price + } + } + // end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/kafka/inbound/MyCheckpointStateStore.java b/content/docs/smallrye-reactive-messaging/3/_examples/kafka/inbound/MyCheckpointStateStore.java new file mode 100644 index 000000000..b91d74306 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/kafka/inbound/MyCheckpointStateStore.java @@ -0,0 +1,68 @@ +package kafka.inbound; + +import java.util.Collection; +import java.util.Map; + +import javax.enterprise.context.ApplicationScoped; + +import org.apache.kafka.clients.consumer.ConsumerConfig; +import org.apache.kafka.common.TopicPartition; + +import io.smallrye.common.annotation.Identifier; +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.kafka.KafkaConnectorIncomingConfiguration; +import io.smallrye.reactive.messaging.kafka.KafkaConsumer; +import io.smallrye.reactive.messaging.kafka.commit.CheckpointStateStore; +import io.smallrye.reactive.messaging.kafka.commit.ProcessingState; +import io.vertx.mutiny.core.Vertx; + +public class MyCheckpointStateStore implements CheckpointStateStore { + + private final String consumerGroupId; + private final Class stateType; + + public MyCheckpointStateStore(String consumerGroupId, Class stateType) { + this.consumerGroupId = consumerGroupId; + this.stateType = stateType; + } + + /** + * Can be used with + * {@code mp.reactive.messaging.incoming.my-channel.commit-strategy=checkpoint} + * {@code mp.reactive.messaging.incoming.my-channel.checkpoint.state-store=my-store} + */ + @ApplicationScoped + @Identifier("my-store") + public static class Factory implements CheckpointStateStore.Factory { + + @Override + public CheckpointStateStore create(KafkaConnectorIncomingConfiguration config, + Vertx vertx, + KafkaConsumer consumer, + Class stateType /* if configured, otherwise null */) { + String consumerGroupId = (String) consumer.configuration().get(ConsumerConfig.GROUP_ID_CONFIG); + return new MyCheckpointStateStore(consumerGroupId, stateType); + } + } + + @Override + public Uni>> fetchProcessingState(Collection partitions) { + // Called on Vert.x event loop + // Return a Uni completing with the map of topic-partition to processing state + // The Uni will be subscribed also on Vert.x event loop + return Uni.createFrom().nullItem(); + } + + @Override + public Uni persistProcessingState(Map> state) { + // Called on Vert.x event loop + // Return a Uni completing with void when the given states are persisted + // The Uni will be subscribed also on Vert.x event loop + return Uni.createFrom().voidItem(); + } + + @Override + public void close() { + /* Called when channel is closing, no-op by default */ + } +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/kafka/outbound/KafkaCloudEventMultipleProcessors.java b/content/docs/smallrye-reactive-messaging/3/_examples/kafka/outbound/KafkaCloudEventMultipleProcessors.java new file mode 100644 index 000000000..c396e1145 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/kafka/outbound/KafkaCloudEventMultipleProcessors.java @@ -0,0 +1,39 @@ +package kafka.outbound; + +import java.net.URI; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.reactive.messaging.ce.OutgoingCloudEventMetadata; + +@ApplicationScoped +public class KafkaCloudEventMultipleProcessors { + + @Incoming("source") + @Outgoing("processed") + public Message process(Message in) { + return in.addMetadata(OutgoingCloudEventMetadata.builder() + .withId("id-" + in.getPayload()) + .withType("greeting") + .build()); + } + + @SuppressWarnings("unchecked") + @Incoming("processed") + @Outgoing("cloud-events") + public Message process2(Message in) { + OutgoingCloudEventMetadata metadata = in + .getMetadata(OutgoingCloudEventMetadata.class) + .orElseGet(() -> OutgoingCloudEventMetadata.builder().build()); + + return in.addMetadata(OutgoingCloudEventMetadata.from(metadata) + .withSource(URI.create("source://me")) + .withSubject("test") + .build()); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/kafka/outbound/KafkaCloudEventProcessor.java b/content/docs/smallrye-reactive-messaging/3/_examples/kafka/outbound/KafkaCloudEventProcessor.java new file mode 100644 index 000000000..140e72b91 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/kafka/outbound/KafkaCloudEventProcessor.java @@ -0,0 +1,25 @@ +package kafka.outbound; + +import java.net.URI; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.reactive.messaging.ce.OutgoingCloudEventMetadata; + +@ApplicationScoped +public class KafkaCloudEventProcessor { + + @Outgoing("cloud-events") + public Message toCloudEvents(Message in) { + return in.addMetadata(OutgoingCloudEventMetadata.builder() + .withId("id-" + in.getPayload()) + .withType("greetings") + .withSource(URI.create("http://example.com")) + .withSubject("greeting-message") + .build()); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/kafka/outbound/KafkaExactlyOnceProcessor.java b/content/docs/smallrye-reactive-messaging/3/_examples/kafka/outbound/KafkaExactlyOnceProcessor.java new file mode 100644 index 000000000..4ca729fda --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/kafka/outbound/KafkaExactlyOnceProcessor.java @@ -0,0 +1,31 @@ +package kafka.outbound; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Incoming; + +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.kafka.KafkaRecord; +import io.smallrye.reactive.messaging.kafka.KafkaRecordBatch; +import io.smallrye.reactive.messaging.kafka.transactions.KafkaTransactions; + +@ApplicationScoped +public class KafkaExactlyOnceProcessor { + + @Inject + @Channel("tx-out-example") + KafkaTransactions txProducer; + + @Incoming("in-channel") + public Uni emitInTransaction(KafkaRecordBatch batch) { + return txProducer.withTransaction(batch, emitter -> { + for (KafkaRecord record : batch) { + emitter.send(KafkaRecord.of(record.getKey(), record.getPayload() + 1)); + } + return Uni.createFrom().voidItem(); + }); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/kafka/outbound/KafkaOutboundDynamicTopicExample.java b/content/docs/smallrye-reactive-messaging/3/_examples/kafka/outbound/KafkaOutboundDynamicTopicExample.java new file mode 100644 index 000000000..cca404431 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/kafka/outbound/KafkaOutboundDynamicTopicExample.java @@ -0,0 +1,25 @@ +package kafka.outbound; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.kafka.api.OutgoingKafkaRecordMetadata; + +public class KafkaOutboundDynamicTopicExample { + + public Message metadata(Message incoming) { + // tag::code[] + String topicName = selectTopicFromIncommingMessage(incoming); + OutgoingKafkaRecordMetadata metadata = OutgoingKafkaRecordMetadata. builder() + .withTopic(topicName) + .build(); + + // Create a new message from the `incoming` message + // Add `metadata` to the metadata from the `incoming` message. + return incoming.addMetadata(metadata); + // end::code[] + } + + private String selectTopicFromIncommingMessage(Message incoming) { + return "fake"; + } +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/kafka/outbound/KafkaOutboundMetadataExample.java b/content/docs/smallrye-reactive-messaging/3/_examples/kafka/outbound/KafkaOutboundMetadataExample.java new file mode 100644 index 000000000..645f290d2 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/kafka/outbound/KafkaOutboundMetadataExample.java @@ -0,0 +1,25 @@ +package kafka.outbound; + +import org.apache.kafka.common.header.internals.RecordHeaders; +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.kafka.api.OutgoingKafkaRecordMetadata; + +public class KafkaOutboundMetadataExample { + + public Message metadata(Message incoming) { + // tag::code[] + // Creates an OutgoingKafkaRecordMetadata + // The type parameter is the type of the record's key + OutgoingKafkaRecordMetadata metadata = OutgoingKafkaRecordMetadata. builder() + .withKey("my-key") + .withHeaders(new RecordHeaders().add("my-header", "value".getBytes())) + .build(); + + // Create a new message from the `incoming` message + // Add `metadata` to the metadata from the `incoming` message. + return incoming.addMetadata(metadata); + // end::code[] + } + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/kafka/outbound/KafkaPriceMessageProducer.java b/content/docs/smallrye-reactive-messaging/3/_examples/kafka/outbound/KafkaPriceMessageProducer.java new file mode 100644 index 000000000..5491facf8 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/kafka/outbound/KafkaPriceMessageProducer.java @@ -0,0 +1,26 @@ +package kafka.outbound; + +import java.time.Duration; +import java.util.Random; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +@ApplicationScoped +public class KafkaPriceMessageProducer { + + private final Random random = new Random(); + + @Outgoing("prices-out") + public Multi> generate() { + // Build an infinite stream of random prices + // It emits a price every second + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .map(x -> Message.of(random.nextDouble())); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/kafka/outbound/KafkaPriceProcessor.java b/content/docs/smallrye-reactive-messaging/3/_examples/kafka/outbound/KafkaPriceProcessor.java new file mode 100644 index 000000000..2a1a14ab7 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/kafka/outbound/KafkaPriceProcessor.java @@ -0,0 +1,19 @@ +package kafka.outbound; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +@ApplicationScoped +public class KafkaPriceProcessor { + + // tag::code[] + @Incoming("in") + @Outgoing("out") + public double process(int in) { + return in * 0.88; + } + // end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/kafka/outbound/KafkaPriceProducer.java b/content/docs/smallrye-reactive-messaging/3/_examples/kafka/outbound/KafkaPriceProducer.java new file mode 100644 index 000000000..34283190a --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/kafka/outbound/KafkaPriceProducer.java @@ -0,0 +1,25 @@ +package kafka.outbound; + +import java.time.Duration; +import java.util.Random; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +@ApplicationScoped +public class KafkaPriceProducer { + + private final Random random = new Random(); + + @Outgoing("prices-out") + public Multi generate() { + // Build an infinite stream of random prices + // It emits a price every second + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .map(x -> random.nextDouble()); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/kafka/outbound/KafkaProducerRecordExample.java b/content/docs/smallrye-reactive-messaging/3/_examples/kafka/outbound/KafkaProducerRecordExample.java new file mode 100644 index 000000000..adda9623e --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/kafka/outbound/KafkaProducerRecordExample.java @@ -0,0 +1,18 @@ +package kafka.outbound; + +import javax.enterprise.context.ApplicationScoped; + +import org.apache.kafka.clients.producer.ProducerRecord; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +@ApplicationScoped +public class KafkaProducerRecordExample { + + // tag::code[] + @Outgoing("out") + public ProducerRecord generate() { + return new ProducerRecord<>("my-topic", "key", "value"); + } + // end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/kafka/outbound/KafkaRecordExample.java b/content/docs/smallrye-reactive-messaging/3/_examples/kafka/outbound/KafkaRecordExample.java new file mode 100644 index 000000000..415688dd0 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/kafka/outbound/KafkaRecordExample.java @@ -0,0 +1,21 @@ +package kafka.outbound; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.reactive.messaging.kafka.Record; + +@ApplicationScoped +public class KafkaRecordExample { + + // tag::code[] + @Incoming("in") + @Outgoing("out") + public Record process(String in) { + return Record.of("my-key", in); + } + // end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/kafka/outbound/KafkaTransactionalProducer.java b/content/docs/smallrye-reactive-messaging/3/_examples/kafka/outbound/KafkaTransactionalProducer.java new file mode 100644 index 000000000..b50777c0d --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/kafka/outbound/KafkaTransactionalProducer.java @@ -0,0 +1,28 @@ +package kafka.outbound; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; + +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.kafka.KafkaRecord; +import io.smallrye.reactive.messaging.kafka.transactions.KafkaTransactions; + +@ApplicationScoped +public class KafkaTransactionalProducer { + + @Inject + @Channel("tx-out-example") + KafkaTransactions txProducer; + + public Uni emitInTransaction() { + return txProducer.withTransaction(emitter -> { + emitter.send(KafkaRecord.of(1, "a")); + emitter.send(KafkaRecord.of(2, "b")); + emitter.send(KafkaRecord.of(3, "c")); + return Uni.createFrom().voidItem(); + }); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/kafka/outbound/ProducerInterceptorBeanExample.java b/content/docs/smallrye-reactive-messaging/3/_examples/kafka/outbound/ProducerInterceptorBeanExample.java new file mode 100644 index 000000000..a321feed8 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/kafka/outbound/ProducerInterceptorBeanExample.java @@ -0,0 +1,37 @@ +package kafka.outbound; + +import java.util.Map; + +import javax.enterprise.context.ApplicationScoped; + +import org.apache.kafka.clients.producer.ProducerInterceptor; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.apache.kafka.clients.producer.RecordMetadata; + +import io.smallrye.common.annotation.Identifier; + +@ApplicationScoped +@Identifier("my-producer-interceptor") +public class ProducerInterceptorBeanExample implements ProducerInterceptor { + + @Override + public ProducerRecord onSend(ProducerRecord producerRecord) { + // called before send + return producerRecord; + } + + @Override + public void onAcknowledgement(RecordMetadata recordMetadata, Exception e) { + // called on send acknowledgement callback + } + + @Override + public void close() { + // called on client close + } + + @Override + public void configure(Map map) { + // called on client configuration + } +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/merge/MergeExamples.java b/content/docs/smallrye-reactive-messaging/3/_examples/merge/MergeExamples.java new file mode 100644 index 000000000..fbf4d4ac8 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/merge/MergeExamples.java @@ -0,0 +1,31 @@ +package merge; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.reactive.messaging.annotations.Merge; + +public class MergeExamples { + + // tag::chain[] + @Incoming("in1") + @Outgoing("out") + public int increment(int i) { + return i + 1; + } + + @Incoming("in2") + @Outgoing("out") + public int multiply(int i) { + return i * 2; + } + + @Incoming("out") + @Merge + public void getAll(int i) { + //... + } + + // end::chain[] + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/messages/MessageExamples.java b/content/docs/smallrye-reactive-messaging/3/_examples/messages/MessageExamples.java new file mode 100644 index 000000000..15dbf7e0f --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/messages/MessageExamples.java @@ -0,0 +1,96 @@ +package messages; + +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Metadata; + +@SuppressWarnings({ "unused" }) +public class MessageExamples { + + public void example1() { + Message message = Message.of("hello", Metadata.of(new MyMetadata())); + // tag::message[] + String payload = message.getPayload(); + Optional metadata = message.getMetadata(MyMetadata.class); + // end::message[] + } + + public void creation() { + Price price = new Price(20.5); + // tag::creation[] + // Create a simple message wrapping a payload + Message m1 = Message.of(price); + + // Create a message with metadata + Message m2 = Message.of(price, Metadata.of(new PriceMetadata())); + + // Create a message with several metadata + Message m3 = Message.of(price, + Metadata.of(new PriceMetadata(), new MyMetadata())); + + // Create a message with an acknowledgement callback + Message m4 = Message.of(price, () -> { + // Called when the message is acknowledged by the next consumer. + return CompletableFuture.completedFuture(null); + }); + + // Create a message with both metadata and acknowledgement callback + Message m5 = Message.of(price, + Metadata.of(new PriceMetadata()), + () -> { + // Called when the message is acknowledged by the next consumer. + return CompletableFuture.completedFuture(null); + }); + // end::creation[] + } + + public void copy() { + Price price = new Price(20.5); + Message message = Message.of(price); + // tag::copy[] + + // Create a new message with a new payload but with the same metadata + Message m1 = message.withPayload(new Price(12.4)); + + // Create a new message with a new payload and add another metadata + Message m2 = message + .withPayload(new Price(15.0)) + .withMetadata(Metadata.of(new PriceMetadata())); + + // Create a new message with a new payload and a custom acknowledgement + Message m3 = message + .withPayload(new Price(15.0)) + .withAck(() -> + // acknowledge the incoming message + message.ack() + .thenAccept(x -> { + // do something + })); + // end::copy[] + } + + public void nullPayload() { + // tag::null-payload[] + // Create a message with null payload + Message m6 = Message.of(null, Metadata.of(new PriceMetadata())); + // end::null-payload[] + } + + public static class Price { + final double price; + + public Price(double p) { + this.price = p; + } + } + + static class PriceMetadata { + final long timestamp; + + public PriceMetadata() { + this.timestamp = System.currentTimeMillis(); + } + } +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/mqtt/customization/ClientProducers.java b/content/docs/smallrye-reactive-messaging/3/_examples/mqtt/customization/ClientProducers.java new file mode 100644 index 000000000..09e3a2793 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/mqtt/customization/ClientProducers.java @@ -0,0 +1,32 @@ +package mqtt.customization; + +import javax.enterprise.inject.Produces; + +import io.smallrye.common.annotation.Identifier; +import io.smallrye.reactive.messaging.mqtt.session.MqttClientSessionOptions; +import io.vertx.core.net.PemKeyCertOptions; +import io.vertx.core.net.PemTrustOptions; + +public class ClientProducers { + + // tag::named[] + @Produces + @Identifier("my-options") + public MqttClientSessionOptions getOptions() { + // You can use the produced options to configure the TLS connection + PemKeyCertOptions keycert = new PemKeyCertOptions() + .addCertPath("./tls/tls.crt") + .addKeyPath("./tls/tls.key"); + PemTrustOptions trust = new PemTrustOptions().addCertPath("./tlc/ca.crt"); + + return new MqttClientSessionOptions() + .setSsl(true) + .setPemKeyCertOptions(keycert) + .setPemTrustOptions(trust) + .setHostnameVerificationAlgorithm("") + .setConnectTimeout(30000) + .setReconnectInterval(5000); + } + // end::named[] + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/mqtt/inbound/MqttPriceConsumer.java b/content/docs/smallrye-reactive-messaging/3/_examples/mqtt/inbound/MqttPriceConsumer.java new file mode 100644 index 000000000..357015762 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/mqtt/inbound/MqttPriceConsumer.java @@ -0,0 +1,17 @@ +package mqtt.inbound; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; + +@ApplicationScoped +public class MqttPriceConsumer { + + @Incoming("prices") + public void consume(byte[] raw) { + double price = Double.parseDouble(new String(raw)); + + // process your price. + } + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/mqtt/inbound/MqttPriceMessageConsumer.java b/content/docs/smallrye-reactive-messaging/3/_examples/mqtt/inbound/MqttPriceMessageConsumer.java new file mode 100644 index 000000000..54507e6c5 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/mqtt/inbound/MqttPriceMessageConsumer.java @@ -0,0 +1,21 @@ +package mqtt.inbound; + +import java.util.concurrent.CompletionStage; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; + +@ApplicationScoped +public class MqttPriceMessageConsumer { + + @Incoming("prices") + public CompletionStage consume(Message price) { + // process your price. + + // Acknowledge the incoming message + return price.ack(); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/mqtt/outbound/MqttPriceMessageProducer.java b/content/docs/smallrye-reactive-messaging/3/_examples/mqtt/outbound/MqttPriceMessageProducer.java new file mode 100644 index 000000000..6a6634b81 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/mqtt/outbound/MqttPriceMessageProducer.java @@ -0,0 +1,26 @@ +package mqtt.outbound; + +import java.time.Duration; +import java.util.Random; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +@ApplicationScoped +public class MqttPriceMessageProducer { + + private Random random = new Random(); + + @Outgoing("prices") + public Multi> generate() { + // Build an infinite stream of random prices + // It emits a price every second + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .map(x -> Message.of(random.nextDouble())); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/mqtt/outbound/MqttPriceProducer.java b/content/docs/smallrye-reactive-messaging/3/_examples/mqtt/outbound/MqttPriceProducer.java new file mode 100644 index 000000000..14e3b8a7c --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/mqtt/outbound/MqttPriceProducer.java @@ -0,0 +1,25 @@ +package mqtt.outbound; + +import java.time.Duration; +import java.util.Random; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +@ApplicationScoped +public class MqttPriceProducer { + + private Random random = new Random(); + + @Outgoing("prices") + public Multi generate() { + // Build an infinite stream of random prices + // It emits a price every second + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .map(x -> random.nextDouble()); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/processing/ProcessingExamples.java b/content/docs/smallrye-reactive-messaging/3/_examples/processing/ProcessingExamples.java new file mode 100644 index 000000000..548722816 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/processing/ProcessingExamples.java @@ -0,0 +1,72 @@ +package processing; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Uni; + +public class ProcessingExamples { + + // tag::process-message[] + @Incoming("in") + @Outgoing("out") + public Message processMessage(Message in) { + return in.withPayload(Integer.toString(in.getPayload())); + } + // end::process-message[] + + // tag::process-message-cs[] + @Incoming("in") + @Outgoing("out") + public CompletionStage> processMessageCS(Message in) { + CompletionStage cs = invokeService(in.getPayload()); + return cs.thenApply(in::withPayload); + } + // end::process-message-cs[] + + // tag::process-message-uni[] + @Incoming("in") + @Outgoing("out") + public Uni> processMessageUni(Message in) { + return invokeService(in.getPayload()) + .map(in::withPayload); + } + // end::process-message-uni[] + + // tag::process-payload[] + @Incoming("in") + @Outgoing("out") + public String processPayload(int in) { + return Integer.toString(in); + } + // end::process-payload[] + + // tag::process-payload-cs[] + @Incoming("in") + @Outgoing("out") + public CompletionStage processPayloadCS(int in) { + return invokeService(in); + } + // end::process-payload-cs[] + + // tag::process-payload-uni[] + @Incoming("in") + @Outgoing("out") + public Uni processPayload(String in) { + return invokeService(in); + } + // end::process-payload-uni[] + + private CompletionStage invokeService(int param) { + return CompletableFuture.completedFuture("ok"); + } + + private Uni invokeService(String param) { + return Uni.createFrom().item("ok"); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/processing/StreamExamples.java b/content/docs/smallrye-reactive-messaging/3/_examples/processing/StreamExamples.java new file mode 100644 index 000000000..c33bfecd8 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/processing/StreamExamples.java @@ -0,0 +1,41 @@ +package processing; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; + +public class StreamExamples { + + // tag::processing-stream-message[] + @Incoming("in") + @Outgoing("out") + public Multi> processMessageStream(Multi> stream) { + return stream + .onItem().transformToUni(message -> invokeService(message.getPayload()) + .onFailure().recoverWithItem("fallback") + .onItem().transform(message::withPayload)) + .concatenate(); + + } + // end::processing-stream-message[] + + // tag::processing-stream-payload[] + @Incoming("in") + @Outgoing("out") + public Multi processPayloadStream(Multi stream) { + return stream + .onItem().transformToUni(payload -> invokeService(payload) + .onFailure().recoverWithItem("fallback")) + .concatenate(); + + } + // end::processing-stream-payload[] + + private Uni invokeService(int i) { + return Uni.createFrom().item("ok " + i); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/quickstart/ReactiveMessagingExample.java b/content/docs/smallrye-reactive-messaging/3/_examples/quickstart/ReactiveMessagingExample.java new file mode 100644 index 000000000..5030ad490 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/quickstart/ReactiveMessagingExample.java @@ -0,0 +1,35 @@ +package quickstart; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +@ApplicationScoped +public class ReactiveMessagingExample { + + @Outgoing("source") + public Multi source() { + return Multi.createFrom().items("hello", "from", "SmallRye", "reactive", "messaging"); + } + + @Incoming("source") + @Outgoing("processed-a") + public String toUpperCase(String payload) { + return payload.toUpperCase(); + } + + @Incoming("processed-a") + @Outgoing("processed-b") + public Multi filter(Multi input) { + return input.select().where(item -> item.length() > 4); + } + + @Incoming("processed-b") + public void sink(String word) { + System.out.println(">> " + word); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/rabbitmq/customization/RabbitMQProducers.java b/content/docs/smallrye-reactive-messaging/3/_examples/rabbitmq/customization/RabbitMQProducers.java new file mode 100644 index 000000000..1168356ba --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/rabbitmq/customization/RabbitMQProducers.java @@ -0,0 +1,34 @@ +package rabbitmq.customization; + +import javax.enterprise.inject.Produces; + +import io.smallrye.common.annotation.Identifier; +import io.vertx.core.net.PemKeyCertOptions; +import io.vertx.core.net.PemTrustOptions; +import io.vertx.rabbitmq.RabbitMQOptions; + +public class RabbitMQProducers { + + // tag::named[] + @Produces + @Identifier("my-named-options") + public RabbitMQOptions getNamedOptions() { + // You can use the produced options to configure the TLS connection + PemKeyCertOptions keycert = new PemKeyCertOptions() + .addCertPath("./tls/tls.crt") + .addKeyPath("./tls/tls.key"); + PemTrustOptions trust = new PemTrustOptions().addCertPath("./tlc/ca.crt"); + + return (RabbitMQOptions) new RabbitMQOptions() + .setUser("admin") + .setPassword("test") + .setSsl(true) + .setPemKeyCertOptions(keycert) + .setPemTrustOptions(trust) + .setHostnameVerificationAlgorithm("") + .setConnectTimeout(30000) + .setReconnectInterval(5000); + } + // end::named[] + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/rabbitmq/inbound/RabbitMQMetadataExample.java b/content/docs/smallrye-reactive-messaging/3/_examples/rabbitmq/inbound/RabbitMQMetadataExample.java new file mode 100644 index 000000000..c5b826b20 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/rabbitmq/inbound/RabbitMQMetadataExample.java @@ -0,0 +1,36 @@ +package rabbitmq.inbound; + +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Map; +import java.util.Optional; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.rabbitmq.IncomingRabbitMQMetadata; + +public class RabbitMQMetadataExample { + + public void metadata(final Message incomingMessage) { + // tag::code[] + final Optional metadata = incomingMessage.getMetadata(IncomingRabbitMQMetadata.class); + metadata.ifPresent(meta -> { + final Optional contentEncoding = meta.getContentEncoding(); + final Optional contentType = meta.getContentType(); + final Optional correlationId = meta.getCorrelationId(); + final Optional timestamp = meta.getTimestamp(ZoneId.systemDefault()); + final Optional priority = meta.getPriority(); + final Optional replyTo = meta.getReplyTo(); + final Optional userId = meta.getUserId(); + + // Access a single String-valued header + final Optional stringHeader = meta.getHeader("my-header", String.class); + + // Access all headers + final Map headers = meta.getHeaders(); + // ... + }); + // end::code[] + } + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/rabbitmq/inbound/RabbitMQPriceConsumer.java b/content/docs/smallrye-reactive-messaging/3/_examples/rabbitmq/inbound/RabbitMQPriceConsumer.java new file mode 100644 index 000000000..f74179004 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/rabbitmq/inbound/RabbitMQPriceConsumer.java @@ -0,0 +1,15 @@ +package rabbitmq.inbound; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; + +@ApplicationScoped +public class RabbitMQPriceConsumer { + + @Incoming("prices") + public void consume(String price) { + // process your price. + } + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/rabbitmq/inbound/RabbitMQPriceMessageConsumer.java b/content/docs/smallrye-reactive-messaging/3/_examples/rabbitmq/inbound/RabbitMQPriceMessageConsumer.java new file mode 100644 index 000000000..57de47064 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/rabbitmq/inbound/RabbitMQPriceMessageConsumer.java @@ -0,0 +1,21 @@ +package rabbitmq.inbound; + +import java.util.concurrent.CompletionStage; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; + +@ApplicationScoped +public class RabbitMQPriceMessageConsumer { + + @Incoming("prices") + public CompletionStage consume(Message price) { + // process your price. + + // Acknowledge the incoming message, marking the RabbitMQ message as `accepted`. + return price.ack(); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/rabbitmq/outbound/RabbitMQOutboundMetadataExample.java b/content/docs/smallrye-reactive-messaging/3/_examples/rabbitmq/outbound/RabbitMQOutboundMetadataExample.java new file mode 100644 index 000000000..225a6f0a4 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/rabbitmq/outbound/RabbitMQOutboundMetadataExample.java @@ -0,0 +1,26 @@ +package rabbitmq.outbound; + +import java.time.ZonedDateTime; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Metadata; + +import io.smallrye.reactive.messaging.rabbitmq.OutgoingRabbitMQMetadata; + +public class RabbitMQOutboundMetadataExample { + + public Message metadata(Message incoming) { + + // tag::code[] + final OutgoingRabbitMQMetadata metadata = new OutgoingRabbitMQMetadata.Builder() + .withHeader("my-header", "xyzzy") + .withRoutingKey("urgent") + .withTimestamp(ZonedDateTime.now()) + .build(); + + // Add `metadata` to the metadata of the outgoing message. + return Message.of("Hello", Metadata.of(metadata)); + // end::code[] + } + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/rabbitmq/outbound/RabbitMQPriceMessageProducer.java b/content/docs/smallrye-reactive-messaging/3/_examples/rabbitmq/outbound/RabbitMQPriceMessageProducer.java new file mode 100644 index 000000000..7685f47c9 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/rabbitmq/outbound/RabbitMQPriceMessageProducer.java @@ -0,0 +1,33 @@ +package rabbitmq.outbound; + +import java.time.Duration; +import java.time.ZonedDateTime; +import java.util.Random; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Metadata; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; +import io.smallrye.reactive.messaging.rabbitmq.OutgoingRabbitMQMetadata; + +@ApplicationScoped +public class RabbitMQPriceMessageProducer { + + private Random random = new Random(); + + @Outgoing("prices") + public Multi> generate() { + // Build an infinite stream of random prices + // It emits a price every second + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .map(x -> Message.of(random.nextDouble(), + Metadata.of(new OutgoingRabbitMQMetadata.Builder() + .withRoutingKey("normal") + .withTimestamp(ZonedDateTime.now()) + .build()))); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/rabbitmq/outbound/RabbitMQPriceProducer.java b/content/docs/smallrye-reactive-messaging/3/_examples/rabbitmq/outbound/RabbitMQPriceProducer.java new file mode 100644 index 000000000..fd8e656f2 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/rabbitmq/outbound/RabbitMQPriceProducer.java @@ -0,0 +1,25 @@ +package rabbitmq.outbound; + +import java.time.Duration; +import java.util.Random; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +@ApplicationScoped +public class RabbitMQPriceProducer { + + private Random random = new Random(); + + @Outgoing("prices") + public Multi generate() { + // Build an infinite stream of random prices + // It emits a price every second + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .map(x -> random.nextDouble()); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/skip/SingleSkip.java b/content/docs/smallrye-reactive-messaging/3/_examples/skip/SingleSkip.java new file mode 100644 index 000000000..f7209220b --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/skip/SingleSkip.java @@ -0,0 +1,59 @@ +package skip; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Uni; + +public class SingleSkip { + + // tag::skip[] + // Skip when processing payload synchronously - returning `null` + @Incoming("in") + @Outgoing("out") + public String processPayload(String s) { + if (s.equalsIgnoreCase("skip")) { + return null; + } + return s.toUpperCase(); + } + + // Skip when processing message synchronously - returning `null` + @Incoming("in") + @Outgoing("out") + public Message processMessage(Message m) { + String s = m.getPayload(); + if (s.equalsIgnoreCase("skip")) { + m.ack(); + return null; + } + return m.withPayload(s.toUpperCase()); + } + + // Skip when processing payload asynchronously - returning a `Uni` with a `null` value + @Incoming("in") + @Outgoing("out") + public Uni processPayloadAsync(String s) { + if (s.equalsIgnoreCase("skip")) { + // Important, you must not return `null`, but a `null` content + return Uni.createFrom().nullItem(); + } + return Uni.createFrom().item(s.toUpperCase()); + } + + // Skip when processing message asynchronously - returning a `Uni` with a `null` value + @Incoming("in") + @Outgoing("out") + public Uni> processMessageAsync(Message m) { + String s = m.getPayload(); + if (s.equalsIgnoreCase("skip")) { + m.ack(); + return Uni.createFrom().nullItem(); + } + return Uni.createFrom().item(m.withPayload(s.toUpperCase())); + } + + // end::skip[] + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/skip/StreamSkip.java b/content/docs/smallrye-reactive-messaging/3/_examples/skip/StreamSkip.java new file mode 100644 index 000000000..2108a1077 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/skip/StreamSkip.java @@ -0,0 +1,48 @@ +package skip; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +public class StreamSkip { + + // tag::skip[] + @Incoming("in") + @Outgoing("out-1") + public Multi processPayload(String s) { + if (s.equalsIgnoreCase("skip")) { + return Multi.createFrom().empty(); + } + return Multi.createFrom().item(s.toUpperCase()); + } + + @Incoming("in") + @Outgoing("out-2") + public Multi> processMessage(Message m) { + String s = m.getPayload(); + if (s.equalsIgnoreCase("skip")) { + return Multi.createFrom().empty(); + } + return Multi.createFrom().item(m.withPayload(s.toUpperCase())); + } + + @Incoming("in") + @Outgoing("out-3") + public Multi processPayloadStream(Multi stream) { + return stream + .select().where(s -> !s.equalsIgnoreCase("skip")) + .onItem().transform(String::toUpperCase); + } + + @Incoming("in") + @Outgoing("out-4") + public Multi> processMessageStream(Multi> stream) { + return stream + .select().where(m -> !m.getPayload().equalsIgnoreCase("skip")) + .onItem().transform(m -> m.withPayload(m.getPayload().toUpperCase())); + } + // end::skip[] + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/testing/MyTest.java b/content/docs/smallrye-reactive-messaging/3/_examples/testing/MyTest.java new file mode 100644 index 000000000..0ee8efd15 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/testing/MyTest.java @@ -0,0 +1,51 @@ +package testing; + +import javax.enterprise.inject.Any; +import javax.inject.Inject; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import io.smallrye.reactive.messaging.providers.connectors.InMemoryConnector; +import io.smallrye.reactive.messaging.providers.connectors.InMemorySink; +import io.smallrye.reactive.messaging.providers.connectors.InMemorySource; + +public class MyTest { + + // 1. Switch the channels to the in-memory connector: + @BeforeAll + public static void switchMyChannels() { + InMemoryConnector.switchIncomingChannelsToInMemory("prices"); + InMemoryConnector.switchOutgoingChannelsToInMemory("processed-prices"); + } + + // 2. Don't forget to reset the channel after the tests: + @AfterAll + public static void revertMyChannels() { + InMemoryConnector.clear(); + } + + // 3. Inject the in-memory connector in your test, + // or use the bean manager to retrieve the instance + @Inject + @Any + InMemoryConnector connector; + + @Test + void test() { + // 4. Retrieves the in-memory source to send message + InMemorySource prices = connector.source("prices"); + // 5. Retrieves the in-memory sink to check what is received + InMemorySink results = connector.sink("processed-prices"); + + // 6. Send fake messages: + prices.send(1); + prices.send(2); + prices.send(3); + + // 7. Check you have received the expected messages + Assertions.assertEquals(3, results.received().size()); + } +} diff --git a/content/docs/smallrye-reactive-messaging/3/_examples/testing/MyTestSetup.java b/content/docs/smallrye-reactive-messaging/3/_examples/testing/MyTestSetup.java new file mode 100644 index 000000000..5472d654d --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_examples/testing/MyTestSetup.java @@ -0,0 +1,23 @@ +package testing; + +import java.util.HashMap; +import java.util.Map; + +import io.smallrye.reactive.messaging.providers.connectors.InMemoryConnector; + +public class MyTestSetup { + + // tag::code[] + public Map start() { + Map env = new HashMap<>(); + env.putAll(InMemoryConnector.switchIncomingChannelsToInMemory("prices")); + env.putAll(InMemoryConnector.switchOutgoingChannelsToInMemory("my-data-stream")); + return env; + } + + public void stop() { + InMemoryConnector.clear(); + } + // end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/3/_generated/smallrye-amqp-incoming.adoc b/content/docs/smallrye-reactive-messaging/3/_generated/smallrye-amqp-incoming.adoc new file mode 100644 index 000000000..5953591ec --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_generated/smallrye-amqp-incoming.adoc @@ -0,0 +1,27 @@ +[cols="<,<,^,^,<",options="header",] +|=== +|Attribute (_alias_) |Description |Type |Mandatory |Default +|_address_ |The AMQP address. If not set, the channel name is used |string |false | +|_auto-acknowledgement_ |Whether the received AMQP messages must be acknowledged when received |boolean |false |`false` +|_broadcast_ |Whether the received AMQP messages must be dispatched to multiple _subscribers_ |boolean |false |`false` +|_capabilities_ |A comma-separated list of capabilities proposed by the sender or receiver client. |string |false | +|_client-options-name_ _(amqp-client-options-name)_ |The name of the AMQP Client Option bean used to customize the AMQP client configuration |string |false | +|_cloud-events_ |Enables (default) or disables the Cloud Event support. If enabled on an _incoming_ channel, the connector analyzes the incoming records and try to create Cloud Event metadata. If enabled on an _outgoing_, the connector sends the outgoing messages as Cloud Event if the message includes Cloud Event Metadata. |boolean |false |`true` +|_connect-timeout_ _(amqp-connect-timeout)_ |The connection timeout in milliseconds |int |false |`1000` +|_container-id_ |The AMQP container id |string |false | +|_durable_ |Whether AMQP subscription is durable |boolean |false |`false` +|_failure-strategy_ |Specify the failure strategy to apply when a message produced from an AMQP message is nacked. Accepted values are `fail` (default), `accept`, `release`, `reject`, `modified-failed`, `modified-failed-undeliverable-here` |string |false |`fail` +|_health-timeout_ |The max number of seconds to wait to determine if the connection with the broker is still established for the readiness check. After that threshold, the check is considered as failed. |int |false |`3` +|_host_ _(amqp-host)_ |The broker hostname |string |false |`localhost` +|_link-name_ |The name of the link. If not set, the channel name is used. |string |false | +|_password_ _(amqp-password)_ |The password used to authenticate to the broker |string |false | +|_port_ _(amqp-port)_ |The broker port |int |false |`5672` +|_reconnect-attempts_ _(amqp-reconnect-attempts)_ |The number of reconnection attempts |int |false |`100` +|_reconnect-interval_ _(amqp-reconnect-interval)_ |The interval in second between two reconnection attempts |int |false |`10` +|_selector_ |Sets a message selector. This attribute is used to define an `apache.org:selector-filter:string` filter on the source terminus, using SQL-based syntax to request the server filters which messages are delivered to the receiver (if supported by the server in question). Precise functionality supported and syntax needed can vary depending on the server. |string |false | +|_sni-server-name_ _(amqp-sni-server-name)_ |If set, explicitly override the hostname to use for the TLS SNI server name |string |false | +|_tracing-enabled_ |Whether tracing is enabled (default) or disabled |boolean |false |`true` +|_use-ssl_ _(amqp-use-ssl)_ |Whether the AMQP connection uses SSL/TLS |boolean |false |`false` +|_username_ _(amqp-username)_ |The username used to authenticate to the broker |string |false | +|_virtual-host_ _(amqp-virtual-host)_ |If set, configure the hostname value used for the connection AMQP Open frame and TLS SNI server name (if TLS is in use) |string |false | +|=== diff --git a/content/docs/smallrye-reactive-messaging/3/_generated/smallrye-amqp-outgoing.adoc b/content/docs/smallrye-reactive-messaging/3/_generated/smallrye-amqp-outgoing.adoc new file mode 100644 index 000000000..bac41b94e --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_generated/smallrye-amqp-outgoing.adoc @@ -0,0 +1,34 @@ +[cols="<,<,^,^,<",options="header",] +|=== +|Attribute (_alias_) |Description |Type |Mandatory |Default +|_address_ |The AMQP address. If not set, the channel name is used |string |false | +|_capabilities_ |A comma-separated list of capabilities proposed by the sender or receiver client. |string |false | +|_client-options-name_ _(amqp-client-options-name)_ |The name of the AMQP Client Option bean used to customize the AMQP client configuration |string |false | +|_cloud-events_ |Enables (default) or disables the Cloud Event support. If enabled on an _incoming_ channel, the connector analyzes the incoming records and try to create Cloud Event metadata. If enabled on an _outgoing_, the connector sends the outgoing messages as Cloud Event if the message includes Cloud Event Metadata. |boolean |false |`true` +|_cloud-events-data-content-type_ _(cloud-events-default-data-content-type)_ |Configure the default `datacontenttype` attribute of the outgoing Cloud Event. Requires `cloud-events` to be set to `true`. This value is used if the message does not configure the `datacontenttype` attribute itself |string |false | +|_cloud-events-data-schema_ _(cloud-events-default-data-schema)_ |Configure the default `dataschema` attribute of the outgoing Cloud Event. Requires `cloud-events` to be set to `true`. This value is used if the message does not configure the `dataschema` attribute itself |string |false | +|_cloud-events-insert-timestamp_ _(cloud-events-default-timestamp)_ |Whether or not the connector should insert automatically the `time` attribute into the outgoing Cloud Event. Requires `cloud-events` to be set to `true`. This value is used if the message does not configure the `time` attribute itself |boolean |false |`true` +|_cloud-events-mode_ |The Cloud Event mode (`structured` or `binary` (default)). Indicates how are written the cloud events in the outgoing record |string |false |`binary` +|_cloud-events-source_ _(cloud-events-default-source)_ |Configure the default `source` attribute of the outgoing Cloud Event. Requires `cloud-events` to be set to `true`. This value is used if the message does not configure the `source` attribute itself |string |false | +|_cloud-events-subject_ _(cloud-events-default-subject)_ |Configure the default `subject` attribute of the outgoing Cloud Event. Requires `cloud-events` to be set to `true`. This value is used if the message does not configure the `subject` attribute itself |string |false | +|_cloud-events-type_ _(cloud-events-default-type)_ |Configure the default `type` attribute of the outgoing Cloud Event. Requires `cloud-events` to be set to `true`. This value is used if the message does not configure the `type` attribute itself |string |false | +|_connect-timeout_ _(amqp-connect-timeout)_ |The connection timeout in milliseconds |int |false |`1000` +|_container-id_ |The AMQP container id |string |false | +|_credit-retrieval-period_ |The period (in milliseconds) between two attempts to retrieve the credits granted by the broker. This time is used when the sender run out of credits. |int |false |`2000` +|_durable_ |Whether sent AMQP messages are marked durable |boolean |false |`false` +|_health-timeout_ |The max number of seconds to wait to determine if the connection with the broker is still established for the readiness check. After that threshold, the check is considered as failed. |int |false |`3` +|_host_ _(amqp-host)_ |The broker hostname |string |false |`localhost` +|_link-name_ |The name of the link. If not set, the channel name is used. |string |false | +|_merge_ |Whether the connector should allow multiple upstreams |boolean |false |`false` +|_password_ _(amqp-password)_ |The password used to authenticate to the broker |string |false | +|_port_ _(amqp-port)_ |The broker port |int |false |`5672` +|_reconnect-attempts_ _(amqp-reconnect-attempts)_ |The number of reconnection attempts |int |false |`100` +|_reconnect-interval_ _(amqp-reconnect-interval)_ |The interval in second between two reconnection attempts |int |false |`10` +|_sni-server-name_ _(amqp-sni-server-name)_ |If set, explicitly override the hostname to use for the TLS SNI server name |string |false | +|_tracing-enabled_ |Whether tracing is enabled (default) or disabled |boolean |false |`true` +|_ttl_ |The time-to-live of the send AMQP messages. 0 to disable the TTL |long |false |`0` +|_use-anonymous-sender_ |Whether or not the connector should use an anonymous sender. Default value is `true` if the broker supports it, `false` otherwise. If not supported, it is not possible to dynamically change the destination address. |boolean |false | +|_use-ssl_ _(amqp-use-ssl)_ |Whether the AMQP connection uses SSL/TLS |boolean |false |`false` +|_username_ _(amqp-username)_ |The username used to authenticate to the broker |string |false | +|_virtual-host_ _(amqp-virtual-host)_ |If set, configure the hostname value used for the connection AMQP Open frame and TLS SNI server name (if TLS is in use) |string |false | +|=== diff --git a/content/docs/smallrye-reactive-messaging/3/_generated/smallrye-camel-incoming.adoc b/content/docs/smallrye-reactive-messaging/3/_generated/smallrye-camel-incoming.adoc new file mode 100644 index 000000000..e14c417cf --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_generated/smallrye-camel-incoming.adoc @@ -0,0 +1,6 @@ +[cols="<,<,^,^,<",options="header",] +|=== +|Attribute (_alias_) |Description |Type |Mandatory |Default +|_endpoint-uri_ |The URI of the Camel endpoint (read from or written to) |string |true | +|_failure-strategy_ |Specify the failure strategy to apply when a message produced from a Camel exchange is nacked. Values can be `fail` (default) or `ignore` |string |false |`fail` +|=== diff --git a/content/docs/smallrye-reactive-messaging/3/_generated/smallrye-camel-outgoing.adoc b/content/docs/smallrye-reactive-messaging/3/_generated/smallrye-camel-outgoing.adoc new file mode 100644 index 000000000..ae1860580 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_generated/smallrye-camel-outgoing.adoc @@ -0,0 +1,6 @@ +[cols="<,<,^,^,<",options="header",] +|=== +|Attribute (_alias_) |Description |Type |Mandatory |Default +|_endpoint-uri_ |The URI of the Camel endpoint (read from or written to) |string |true | +|_merge_ |Whether the connector should allow multiple upstreams |boolean |false |`false` +|=== diff --git a/content/docs/smallrye-reactive-messaging/3/_generated/smallrye-jms-incoming.adoc b/content/docs/smallrye-reactive-messaging/3/_generated/smallrye-jms-incoming.adoc new file mode 100644 index 000000000..dc667f20c --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_generated/smallrye-jms-incoming.adoc @@ -0,0 +1,15 @@ +[cols="<,<,^,^,<",options="header",] +|=== +|Attribute (_alias_) |Description |Type |Mandatory |Default +|_broadcast_ |Whether or not the JMS message should be dispatched to multiple consumers |boolean |false |`false` +|_client-id_ |The client id |String |false | +|_connection-factory-name_ |The name of the JMS connection factory (`javax.jms.ConnectionFactory`) to be used. If not set, it uses any exposed JMS connection factory |String |false | +|_destination_ |The name of the JMS destination. If not set the name of the channel is used |String |false | +|_destination-type_ |The type of destination. It can be either `queue` or `topic` |string |false |`queue` +|_durable_ |Set to `true` to use a durable subscription |boolean |false |`false` +|_no-local_ |Enable or disable local delivery |boolean |false |`false` +|_password_ |The password to connect to to the JMS server |String |false | +|_selector_ |The JMS selector |String |false | +|_session-mode_ |The session mode. Accepted values are AUTO++_++ACKNOWLEDGE, SESSION++_++TRANSACTED, CLIENT++_++ACKNOWLEDGE, DUPS++_++OK++_++ACKNOWLEDGE |String |false |`AUTO++_++ACKNOWLEDGE` +|_username_ |The username to connect to to the JMS server |String |false | +|=== diff --git a/content/docs/smallrye-reactive-messaging/3/_generated/smallrye-jms-outgoing.adoc b/content/docs/smallrye-reactive-messaging/3/_generated/smallrye-jms-outgoing.adoc new file mode 100644 index 000000000..113db8c65 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_generated/smallrye-jms-outgoing.adoc @@ -0,0 +1,21 @@ +[cols="<,<,^,^,<",options="header",] +|=== +|Attribute (_alias_) |Description |Type |Mandatory |Default +|_client-id_ |The client id |String |false | +|_connection-factory-name_ |The name of the JMS connection factory (`javax.jms.ConnectionFactory`) to be used. If not set, it uses any exposed JMS connection factory |String |false | +|_correlation-id_ |The JMS Message correlation id |string |false | +|_delivery-delay_ |The delivery delay |long |false | +|_delivery-mode_ |The delivery mode. Either `persistent` or `non++_++persistent` |string |false | +|_destination_ |The name of the JMS destination. If not set the name of the channel is used |String |false | +|_destination-type_ |The type of destination. It can be either `queue` or `topic` |string |false |`queue` +|_disable-message-id_ |Omit the message id in the outbound JMS message |boolean |false | +|_disable-message-timestamp_ |Omit the message timestamp in the outbound JMS message |boolean |false | +|_merge_ |Whether the connector should allow multiple upstreams |boolean |false |`false` +|_password_ |The password to connect to to the JMS server |String |false | +|_priority_ |The JMS Message priority |int |false | +|_reply-to_ |The reply to destination if any |string |false | +|_reply-to-destination-type_ |The type of destination for the response. It can be either `queue` or `topic` |string |false |`queue` +|_session-mode_ |The session mode. Accepted values are AUTO++_++ACKNOWLEDGE, SESSION++_++TRANSACTED, CLIENT++_++ACKNOWLEDGE, DUPS++_++OK++_++ACKNOWLEDGE |String |false |`AUTO++_++ACKNOWLEDGE` +|_ttl_ |The JMS Message time-to-live |long |false | +|_username_ |The username to connect to to the JMS server |String |false | +|=== diff --git a/content/docs/smallrye-reactive-messaging/3/_generated/smallrye-kafka-incoming.adoc b/content/docs/smallrye-reactive-messaging/3/_generated/smallrye-kafka-incoming.adoc new file mode 100644 index 000000000..d3096b597 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_generated/smallrye-kafka-incoming.adoc @@ -0,0 +1,50 @@ +[cols="<,<,^,^,<",options="header",] +|=== +|Attribute (_alias_) |Description |Type |Mandatory |Default +|_auto.offset.reset_ |What to do when there is no initial offset in Kafka.Accepted values are earliest, latest and none |string |false |`latest` +|_batch_ |Whether the Kafka records are consumed in batch. The channel injection point must consume a compatible type, such as `List++<++Payload++>++` or `KafkaRecordBatch++<++Payload++>++`. |boolean |false |`false` +|_bootstrap.servers_ _(kafka.bootstrap.servers)_ |A comma-separated list of host:port to use for establishing the initial connection to the Kafka cluster. |string |false |`localhost:9092` +|_broadcast_ |Whether the Kafka records should be dispatched to multiple consumer |boolean |false |`false` +|_checkpoint.state-store_ |While using the `checkpoint` commit-strategy, the name set in `@Identifier` of a bean that implements `io.smallrye.reactive.messaging.kafka.StateStore.Factory` to specify the state store implementation. |string |false | +|_checkpoint.state-type_ |While using the `checkpoint` commit-strategy, the fully qualified type name of the state object to persist in the state store. When provided, it can be used by the state store implementation to help persisting the processing state object. |string |false | +|_checkpoint.unsynced-state-max-age.ms_ |While using the `checkpoint` commit-strategy, specify the max age in milliseconds that the processing state must be persisted before the connector is marked as unhealthy. Setting this attribute to 0 disables this monitoring. |int |false |`10000` +|_client-id-prefix_ |Prefix for Kafka client `client.id` attribute. If defined configured or generated `client.id` will be prefixed with the given value. |string |false | +|_cloud-events_ |Enables (default) or disables the Cloud Event support. If enabled on an _incoming_ channel, the connector analyzes the incoming records and try to create Cloud Event metadata. If enabled on an _outgoing_, the connector sends the outgoing messages as Cloud Event if the message includes Cloud Event Metadata. |boolean |false |`true` +|_commit-strategy_ |Specify the commit strategy to apply when a message produced from a record is acknowledged. Values can be `latest`, `ignore` or `throttled`. If `enable.auto.commit` is true then the default is `ignore` otherwise it is `throttled` |string |false | +|_consumer-rebalance-listener.name_ |The name set in `@Identifier` of a bean that implements `io.smallrye.reactive.messaging.kafka.KafkaConsumerRebalanceListener`. If set, this rebalance listener is applied to the consumer. |string |false | +|_dead-letter-queue.key.serializer_ |When the `failure-strategy` is set to `dead-letter-queue` indicates the key serializer to use. If not set the serializer associated to the key deserializer is used |string |false | +|_dead-letter-queue.producer-client-id_ |When the `failure-strategy` is set to `dead-letter-queue` indicates what client id the generated producer should use. Defaults is `kafka-dead-letter-topic-producer-$client-id` |string |false | +|_dead-letter-queue.topic_ |When the `failure-strategy` is set to `dead-letter-queue` indicates on which topic the record is sent. Defaults is `dead-letter-topic-$channel` |string |false | +|_dead-letter-queue.value.serializer_ |When the `failure-strategy` is set to `dead-letter-queue` indicates the value serializer to use. If not set the serializer associated to the value deserializer is used |string |false | +|_enable.auto.commit_ |If enabled, consumer's offset will be periodically committed in the background by the underlying Kafka client, ignoring the actual processing outcome of the records. It is recommended to NOT enable this setting and let Reactive Messaging handles the commit. |boolean |false |`false` +|_fail-on-deserialization-failure_ |When no deserialization failure handler is set and a deserialization failure happens, report the failure and mark the application as unhealthy. If set to `false` and a deserialization failure happens, a `null` value is forwarded. |boolean |false |`true` +|_failure-strategy_ |Specify the failure strategy to apply when a message produced from a record is acknowledged negatively (nack). Values can be `fail` (default), `ignore`, or `dead-letter-queue` |string |false |`fail` +|_fetch.min.bytes_ |The minimum amount of data the server should return for a fetch request. The default setting of 1 byte means that fetch requests are answered as soon as a single byte of data is available or the fetch request times out waiting for data to arrive. |int |false |`1` +|_graceful-shutdown_ |Whether or not a graceful shutdown should be attempted when the application terminates. |boolean |false |`true` +|_group.id_ |A unique string that identifies the consumer group the application belongs to. If not set, a unique, generated id is used |string |false | +|_health-enabled_ |Whether health reporting is enabled (default) or disabled |boolean |false |`true` +|_health-readiness-enabled_ |Whether readiness health reporting is enabled (default) or disabled |boolean |false |`true` +|_health-readiness-timeout_ |_deprecated_ - During the readiness health check, the connector connects to the broker and retrieves the list of topics. This attribute specifies the maximum duration (in ms) for the retrieval. If exceeded, the channel is considered not-ready. Deprecated: Use 'health-topic-verification-timeout' instead. |long |false | +|_health-readiness-topic-verification_ |_deprecated_ - Whether the readiness check should verify that topics exist on the broker. Default to false. Enabling it requires an admin connection. Deprecated: Use 'health-topic-verification-enabled' instead. |boolean |false | +|_health-topic-verification-enabled_ |Whether the startup and readiness check should verify that topics exist on the broker. Default to false. Enabling it requires an admin client connection. |boolean |false |`false` +|_health-topic-verification-timeout_ |During the startup and readiness health check, the connector connects to the broker and retrieves the list of topics. This attribute specifies the maximum duration (in ms) for the retrieval. If exceeded, the channel is considered not-ready. |long |false |`2000` +|_kafka-configuration_ |Identifier of a CDI bean that provides the default Kafka consumer/producer configuration for this channel. The channel configuration can still override any attribute. The bean must have a type of Map++<++String, Object++>++ and must use the @io.smallrye.common.annotation.Identifier qualifier to set the identifier. |string |false | +|_key-deserialization-failure-handler_ |The name set in `@Identifier` of a bean that implements `io.smallrye.reactive.messaging.kafka.DeserializationFailureHandler`. If set, deserialization failure happening when deserializing keys are delegated to this handler which may retry or provide a fallback value. |string |false | +|_key.deserializer_ |The deserializer classname used to deserialize the record's key |string |false |`org.apache.kafka.common.serialization.StringDeserializer` +|_lazy-client_ |Whether Kafka client is created lazily or eagerly. |boolean |false |`false` +|_max-queue-size-factor_ |Multiplier factor to determine maximum number of records queued for processing, using `max.poll.records` ++*++ `max-queue-size-factor`. Defaults to 2. In `batch` mode `max.poll.records` is considered `1`. |int |false |`2` +|_partitions_ |The number of partitions to be consumed concurrently. The connector creates the specified amount of Kafka consumers. It should match the number of partition of the targeted topic |int |false |`1` +|_pattern_ |Indicate that the `topic` property is a regular expression. Must be used with the `topic` property. Cannot be used with the `topics` property |boolean |false |`false` +|_pause-if-no-requests_ |Whether the polling must be paused when the application does not request items and resume when it does. This allows implementing back-pressure based on the application capacity. Note that polling is not stopped, but will not retrieve any records when paused. |boolean |false |`true` +|_poll-timeout_ |The polling timeout in milliseconds. When polling records, the poll will wait at most that duration before returning records. Default is 1000ms |int |false |`1000` +|_requests_ |When `partitions` is greater than 1, this attribute allows configuring how many records are requested by each consumers every time. |int |false |`128` +|_retry_ |Whether or not the connection to the broker is re-attempted in case of failure |boolean |false |`true` +|_retry-attempts_ |The maximum number of reconnection before failing. -1 means infinite retry |int |false |`-1` +|_retry-max-wait_ |The max delay (in seconds) between 2 reconnects |int |false |`30` +|_throttled.unprocessed-record-max-age.ms_ |While using the `throttled` commit-strategy, specify the max age in milliseconds that an unprocessed message can be before the connector is marked as unhealthy. Setting this attribute to 0 disables this monitoring. |int |false |`60000` +|_topic_ |The consumed / populated Kafka topic. If neither this property nor the `topics` properties are set, the channel name is used |string |false | +|_topics_ |A comma-separating list of topics to be consumed. Cannot be used with the `topic` or `pattern` properties |string |false | +|_tracing-enabled_ |Whether tracing is enabled (default) or disabled |boolean |false |`true` +|_value-deserialization-failure-handler_ |The name set in `@Identifier` of a bean that implements `io.smallrye.reactive.messaging.kafka.DeserializationFailureHandler`. If set, deserialization failure happening when deserializing values are delegated to this handler which may retry or provide a fallback value. |string |false | +|_value.deserializer_ |The deserializer classname used to deserialize the record's value |string |true | +|=== diff --git a/content/docs/smallrye-reactive-messaging/3/_generated/smallrye-kafka-outgoing.adoc b/content/docs/smallrye-reactive-messaging/3/_generated/smallrye-kafka-outgoing.adoc new file mode 100644 index 000000000..d4206a2a9 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_generated/smallrye-kafka-outgoing.adoc @@ -0,0 +1,40 @@ +[cols="<,<,^,^,<",options="header",] +|=== +|Attribute (_alias_) |Description |Type |Mandatory |Default +|_acks_ |The number of acknowledgments the producer requires the leader to have received before considering a request complete. This controls the durability of records that are sent. Accepted values are: 0, 1, all |string |false |`1` +|_bootstrap.servers_ _(kafka.bootstrap.servers)_ |A comma-separated list of host:port to use for establishing the initial connection to the Kafka cluster. |string |false |`localhost:9092` +|_buffer.memory_ |The total bytes of memory the producer can use to buffer records waiting to be sent to the server. |long |false |`33554432` +|_client-id-prefix_ |Prefix for Kafka client `client.id` attribute. If defined configured or generated `client.id` will be prefixed with the given value. |string |false | +|_close-timeout_ |The amount of milliseconds waiting for a graceful shutdown of the Kafka producer |int |false |`10000` +|_cloud-events_ |Enables (default) or disables the Cloud Event support. If enabled on an _incoming_ channel, the connector analyzes the incoming records and try to create Cloud Event metadata. If enabled on an _outgoing_, the connector sends the outgoing messages as Cloud Event if the message includes Cloud Event Metadata. |boolean |false |`true` +|_cloud-events-data-content-type_ _(cloud-events-default-data-content-type)_ |Configure the default `datacontenttype` attribute of the outgoing Cloud Event. Requires `cloud-events` to be set to `true`. This value is used if the message does not configure the `datacontenttype` attribute itself |string |false | +|_cloud-events-data-schema_ _(cloud-events-default-data-schema)_ |Configure the default `dataschema` attribute of the outgoing Cloud Event. Requires `cloud-events` to be set to `true`. This value is used if the message does not configure the `dataschema` attribute itself |string |false | +|_cloud-events-insert-timestamp_ _(cloud-events-default-timestamp)_ |Whether or not the connector should insert automatically the `time` attribute into the outgoing Cloud Event. Requires `cloud-events` to be set to `true`. This value is used if the message does not configure the `time` attribute itself |boolean |false |`true` +|_cloud-events-mode_ |The Cloud Event mode (`structured` or `binary` (default)). Indicates how are written the cloud events in the outgoing record |string |false |`binary` +|_cloud-events-source_ _(cloud-events-default-source)_ |Configure the default `source` attribute of the outgoing Cloud Event. Requires `cloud-events` to be set to `true`. This value is used if the message does not configure the `source` attribute itself |string |false | +|_cloud-events-subject_ _(cloud-events-default-subject)_ |Configure the default `subject` attribute of the outgoing Cloud Event. Requires `cloud-events` to be set to `true`. This value is used if the message does not configure the `subject` attribute itself |string |false | +|_cloud-events-type_ _(cloud-events-default-type)_ |Configure the default `type` attribute of the outgoing Cloud Event. Requires `cloud-events` to be set to `true`. This value is used if the message does not configure the `type` attribute itself |string |false | +|_health-enabled_ |Whether health reporting is enabled (default) or disabled |boolean |false |`true` +|_health-readiness-enabled_ |Whether readiness health reporting is enabled (default) or disabled |boolean |false |`true` +|_health-readiness-timeout_ |_deprecated_ - During the readiness health check, the connector connects to the broker and retrieves the list of topics. This attribute specifies the maximum duration (in ms) for the retrieval. If exceeded, the channel is considered not-ready. Deprecated: Use 'health-topic-verification-timeout' instead. |long |false | +|_health-readiness-topic-verification_ |_deprecated_ - Whether the readiness check should verify that topics exist on the broker. Default to false. Enabling it requires an admin connection. Deprecated: Use 'health-topic-verification-enabled' instead. |boolean |false | +|_health-topic-verification-enabled_ |Whether the startup and readiness check should verify that topics exist on the broker. Default to false. Enabling it requires an admin client connection. |boolean |false |`false` +|_health-topic-verification-timeout_ |During the startup and readiness health check, the connector connects to the broker and retrieves the list of topics. This attribute specifies the maximum duration (in ms) for the retrieval. If exceeded, the channel is considered not-ready. |long |false |`2000` +|_interceptor-bean_ |The name set in `@Identifier` of a bean that implements `org.apache.kafka.clients.producer.ProducerInterceptor`. If set, the identified bean will be used as the producer interceptor. |string |false | +|_kafka-configuration_ |Identifier of a CDI bean that provides the default Kafka consumer/producer configuration for this channel. The channel configuration can still override any attribute. The bean must have a type of Map++<++String, Object++>++ and must use the @io.smallrye.common.annotation.Identifier qualifier to set the identifier. |string |false | +|_key_ |A key to used when writing the record |string |false | +|_key-serialization-failure-handler_ |The name set in `@Identifier` of a bean that implements `io.smallrye.reactive.messaging.kafka.SerializationFailureHandler`. If set, serialization failure happening when serializing keys are delegated to this handler which may provide a fallback value. |string |false | +|_key.serializer_ |The serializer classname used to serialize the record's key |string |false |`org.apache.kafka.common.serialization.StringSerializer` +|_lazy-client_ |Whether Kafka client is created lazily or eagerly. |boolean |false |`false` +|_max-inflight-messages_ |The maximum number of messages to be written to Kafka concurrently. It limits the number of messages waiting to be written and acknowledged by the broker. You can set this attribute to `0` remove the limit |long |false |`1024` +|_merge_ |Whether the connector should allow multiple upstreams |boolean |false |`false` +|_partition_ |The target partition id. -1 to let the client determine the partition |int |false |`-1` +|_propagate-headers_ |A comma-separating list of incoming record headers to be propagated to the outgoing record |string |false |++``++ +|_propagate-record-key_ |Propagate incoming record key to the outgoing record |boolean |false |`false` +|_retries_ |If set to a positive number, the connector will try to resend any record that was not delivered successfully (with a potentially transient error) until the number of retries is reached. If set to 0, retries are disabled. If not set, the connector tries to resend any record that failed to be delivered (because of a potentially transient error) during an amount of time configured by `delivery.timeout.ms`. |long |false |`2147483647` +|_topic_ |The consumed / populated Kafka topic. If neither this property nor the `topics` properties are set, the channel name is used |string |false | +|_tracing-enabled_ |Whether tracing is enabled (default) or disabled |boolean |false |`true` +|_value-serialization-failure-handler_ |The name set in `@Identifier` of a bean that implements `io.smallrye.reactive.messaging.kafka.SerializationFailureHandler`. If set, serialization failure happening when serializing values are delegated to this handler which may provide a fallback value. |string |false | +|_value.serializer_ |The serializer classname used to serialize the payload |string |true | +|_waitForWriteCompletion_ |Whether the client waits for Kafka to acknowledge the written record before acknowledging the message |boolean |false |`true` +|=== diff --git a/content/docs/smallrye-reactive-messaging/3/_generated/smallrye-mqtt-incoming.adoc b/content/docs/smallrye-reactive-messaging/3/_generated/smallrye-mqtt-incoming.adoc new file mode 100644 index 000000000..00fa8bd42 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_generated/smallrye-mqtt-incoming.adoc @@ -0,0 +1,37 @@ +[cols="<,<,^,^,<",options="header",] +|=== +|Attribute (_alias_) |Description |Type |Mandatory |Default +|_auto-clean-session_ |Set to start with a clean session (`true` by default) |boolean |false |`true` +|_auto-generated-client-id_ |Set if the MQTT client must generate clientId automatically |boolean |false |`true` +|_auto-keep-alive_ |Set if the MQTT client must handle `PINGREQ` automatically |boolean |false |`true` +|_broadcast_ |Whether or not the messages should be dispatched to multiple consumers |boolean |false |`false` +|_buffer-size_ |The size buffer of incoming messages waiting to be processed |int |false |`128` +|_client-id_ |Set the client identifier |string |false | +|_client-options-name_ _(mqtt-client-options-name)_ |The name of the MQTT Client Option bean (`io.smallrye.reactive.messaging.mqtt.session.MqttClientSessionOptions`) used to customize the MQTT client configuration |string |false | +|_connect-timeout-seconds_ |Set the connect timeout (in seconds) |int |false |`60` +|_failure-strategy_ |Specify the failure strategy to apply when a message produced from a MQTT message is nacked. Values can be `fail` (default), or `ignore` |string |false |`fail` +|_health-enabled_ |Whether health reporting is enabled (default) or disabled |boolean |false |`true` +|_host_ |Set the MQTT server host name/IP |string |true | +|_keep-alive-seconds_ |Set the keep alive timeout in seconds |int |false |`30` +|_max-inflight-queue_ |Set max count of unacknowledged messages |int |false |`10` +|_max-message-size_ |Set max MQTT message size in bytes |int |false |`8092` +|_password_ |Set the password to connect to the server |string |false | +|_port_ |Set the MQTT server port. Default to 8883 if ssl is enabled, or 1883 without ssl |int |false | +|_qos_ |Set the QoS level when subscribing to the topic or when sending a message |int |false |`0` +|_reconnect-interval-seconds_ |Set the reconnect interval in seconds |int |false |`1` +|_server-name_ |Set the SNI server name |string |false | +|_ssl_ |Set whether SSL/TLS is enabled |boolean |false |`false` +|_ssl.keystore.location_ |Set the keystore location. In case of `pem` type this is the server ca cert path |string |false | +|_ssl.keystore.password_ |Set the keystore password. In case of `pem` type this is the key path |string |false | +|_ssl.keystore.type_ |Set the keystore type ++[++`pkcs12`, `jks`, `pem`++]++ |string |false |`pkcs12` +|_ssl.truststore.location_ |Set the truststore location. In case of `pem` type this is the client cert path |string |false | +|_ssl.truststore.password_ |Set the truststore password. In case of `pem` type this is not necessary |string |false | +|_ssl.truststore.type_ |Set the truststore type ++[++`pkcs12`, `jks`, `pem`++]++ |string |false |`pkcs12` +|_topic_ |Set the MQTT topic. If not set, the channel name is used |string |false | +|_trust-all_ |Set whether all server certificates should be trusted |boolean |false |`false` +|_unsubscribe-on-disconnection_ |This flag restore the old behavior to unsubscribe from the broken on disconnection |boolean |false |`false` +|_username_ |Set the username to connect to the server |string |false | +|_will-flag_ |Set if will information are provided on connection |boolean |false |`false` +|_will-qos_ |Set the QoS level for the will message |int |false |`0` +|_will-retain_ |Set if the will message must be retained |boolean |false |`false` +|=== diff --git a/content/docs/smallrye-reactive-messaging/3/_generated/smallrye-mqtt-outgoing.adoc b/content/docs/smallrye-reactive-messaging/3/_generated/smallrye-mqtt-outgoing.adoc new file mode 100644 index 000000000..4770791bb --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_generated/smallrye-mqtt-outgoing.adoc @@ -0,0 +1,35 @@ +[cols="<,<,^,^,<",options="header",] +|=== +|Attribute (_alias_) |Description |Type |Mandatory |Default +|_auto-clean-session_ |Set to start with a clean session (`true` by default) |boolean |false |`true` +|_auto-generated-client-id_ |Set if the MQTT client must generate clientId automatically |boolean |false |`true` +|_auto-keep-alive_ |Set if the MQTT client must handle `PINGREQ` automatically |boolean |false |`true` +|_client-id_ |Set the client identifier |string |false | +|_client-options-name_ _(mqtt-client-options-name)_ |The name of the MQTT Client Option bean (`io.smallrye.reactive.messaging.mqtt.session.MqttClientSessionOptions`) used to customize the MQTT client configuration |string |false | +|_connect-timeout-seconds_ |Set the connect timeout (in seconds) |int |false |`60` +|_health-enabled_ |Whether health reporting is enabled (default) or disabled |boolean |false |`true` +|_host_ |Set the MQTT server host name/IP |string |true | +|_keep-alive-seconds_ |Set the keep alive timeout in seconds |int |false |`30` +|_max-inflight-queue_ |Set max count of unacknowledged messages |int |false |`10` +|_max-message-size_ |Set max MQTT message size in bytes |int |false |`8092` +|_merge_ |Whether the connector should allow multiple upstreams |boolean |false |`false` +|_password_ |Set the password to connect to the server |string |false | +|_port_ |Set the MQTT server port. Default to 8883 if ssl is enabled, or 1883 without ssl |int |false | +|_qos_ |Set the QoS level when subscribing to the topic or when sending a message |int |false |`0` +|_reconnect-interval-seconds_ |Set the reconnect interval in seconds |int |false |`1` +|_server-name_ |Set the SNI server name |string |false | +|_ssl_ |Set whether SSL/TLS is enabled |boolean |false |`false` +|_ssl.keystore.location_ |Set the keystore location. In case of `pem` type this is the server ca cert path |string |false | +|_ssl.keystore.password_ |Set the keystore password. In case of `pem` type this is the key path |string |false | +|_ssl.keystore.type_ |Set the keystore type ++[++`pkcs12`, `jks`, `pem`++]++ |string |false |`pkcs12` +|_ssl.truststore.location_ |Set the truststore location. In case of `pem` type this is the client cert path |string |false | +|_ssl.truststore.password_ |Set the truststore password. In case of `pem` type this is not necessary |string |false | +|_ssl.truststore.type_ |Set the truststore type ++[++`pkcs12`, `jks`, `pem`++]++ |string |false |`pkcs12` +|_topic_ |Set the MQTT topic. If not set, the channel name is used |string |false | +|_trust-all_ |Set whether all server certificates should be trusted |boolean |false |`false` +|_unsubscribe-on-disconnection_ |This flag restore the old behavior to unsubscribe from the broken on disconnection |boolean |false |`false` +|_username_ |Set the username to connect to the server |string |false | +|_will-flag_ |Set if will information are provided on connection |boolean |false |`false` +|_will-qos_ |Set the QoS level for the will message |int |false |`0` +|_will-retain_ |Set if the will message must be retained |boolean |false |`false` +|=== diff --git a/content/docs/smallrye-reactive-messaging/3/_generated/smallrye-rabbitmq-incoming.adoc b/content/docs/smallrye-reactive-messaging/3/_generated/smallrye-rabbitmq-incoming.adoc new file mode 100644 index 000000000..900e11c5f --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_generated/smallrye-rabbitmq-incoming.adoc @@ -0,0 +1,61 @@ +[cols="<,<,^,^,<",options="header",] +|=== +|Attribute (_alias_) |Description |Type |Mandatory |Default +|_auto-acknowledgement_ |Whether the received RabbitMQ messages must be acknowledged when received; if true then delivery constitutes acknowledgement |boolean |false |`false` +|_auto-bind-dlq_ |Whether to automatically declare the DLQ and bind it to the binder DLX |boolean |false |`false` +|_automatic-recovery-enabled_ |Whether automatic connection recovery is enabled |boolean |false |`false` +|_automatic-recovery-on-initial-connection_ |Whether automatic recovery on initial connections is enabled |boolean |false |`true` +|_broadcast_ |Whether the received RabbitMQ messages must be dispatched to multiple _subscribers_ |boolean |false |`false` +|_client-options-name_ _(rabbitmq-client-options-name)_ |The name of the RabbitMQ Client Option bean used to customize the RabbitMQ client configuration |string |false | +|_connection-count_ |The number of RabbitMQ connections to create for consuming from this queue. This might be necessary to consume from a sharded queue with a single client. |int |false |`1` +|_connection-timeout_ |The TCP connection timeout (ms); 0 is interpreted as no timeout |int |false |`60000` +|_content-type-override_ |Override the content++_++type attribute of the incoming message, should be a valid MINE type |string |false | +|_credentials-provider-name_ _(rabbitmq-credentials-provider-name)_ |The name of the RabbitMQ Credentials Provider bean used to provide dynamic credentials to the RabbitMQ client |string |false | +|_dead-letter-exchange_ |A DLX to assign to the queue. Relevant only if auto-bind-dlq is true |string |false |`DLX` +|_dead-letter-exchange-type_ |The type of the DLX to assign to the queue. Relevant only if auto-bind-dlq is true |string |false |`direct` +|_dead-letter-queue-mode_ |If automatically declare DLQ, we can choose different modes of DLQ ++[++lazy, default++]++ |string |false | +|_dead-letter-queue-name_ |The name of the DLQ; if not supplied will default to the queue name with '.dlq' appended |string |false | +|_dead-letter-queue-type_ |If automatically declare DLQ, we can choose different types of DLQ ++[++quorum, classic, stream++]++ |string |false | +|_dead-letter-routing-key_ |A dead letter routing key to assign to the queue; if not supplied will default to the queue name |string |false | +|_dlx.declare_ |Whether to declare the dead letter exchange binding. Relevant only if auto-bind-dlq is true; set to false if these are expected to be set up independently |boolean |false |`false` +|_exchange.auto-delete_ |Whether the exchange should be deleted after use |boolean |false |`false` +|_exchange.declare_ |Whether to declare the exchange; set to false if the exchange is expected to be set up independently |boolean |false |`true` +|_exchange.durable_ |Whether the exchange is durable |boolean |false |`true` +|_exchange.name_ |The exchange that messages are published to or consumed from. If not set, the channel name is used. If set to "", the default exchange is used. |string |false | +|_exchange.type_ |The exchange type: direct, fanout, headers or topic (default) |string |false |`topic` +|_failure-strategy_ |The failure strategy to apply when a RabbitMQ message is nacked. Accepted values are `fail`, `accept`, `reject` (default) |string |false |`reject` +|_handshake-timeout_ |The AMQP 0-9-1 protocol handshake timeout (ms) |int |false |`10000` +|_host_ _(rabbitmq-host)_ |The broker hostname |string |false |`localhost` +|_include-properties_ |Whether to include properties when a broker message is passed on the event bus |boolean |false |`false` +|_keep-most-recent_ |Whether to discard old messages instead of recent ones |boolean |false |`false` +|_max-incoming-internal-queue-size_ |The maximum size of the incoming internal queue |int |false |`500000` +|_max-outstanding-messages_ |The maximum number of outstanding/unacknowledged messages being processed by the connector at a time; must be a positive number |int |false | +|_network-recovery-interval_ |How long (ms) will automatic recovery wait before attempting to reconnect |int |false |`5000` +|_password_ _(rabbitmq-password)_ |The password used to authenticate to the broker |string |false | +|_port_ _(rabbitmq-port)_ |The broker port |int |false |`5672` +|_queue.auto-delete_ |Whether the queue should be deleted after use |boolean |false |`false` +|_queue.declare_ |Whether to declare the queue and binding; set to false if these are expected to be set up independently |boolean |false |`true` +|_queue.durable_ |Whether the queue is durable |boolean |false |`true` +|_queue.exclusive_ |Whether the queue is for exclusive use |boolean |false |`false` +|_queue.name_ |The queue from which messages are consumed. |string |true | +|_queue.single-active-consumer_ |If set to true, only one consumer can actively consume messages |boolean |false | +|_queue.ttl_ |If specified, the time (ms) for which a message can remain in the queue undelivered before it is dead |long |false | +|_queue.x-max-priority_ |Define priority level queue consumer |int |false | +|_queue.x-queue-mode_ |If automatically declare queue, we can choose different modes of queue ++[++lazy, default++]++ |string |false | +|_queue.x-queue-type_ |If automatically declare queue, we can choose different types of queue ++[++quorum, classic, stream++]++ |string |false | +|_reconnect-attempts_ _(rabbitmq-reconnect-attempts)_ |The number of reconnection attempts |int |false |`100` +|_reconnect-interval_ _(rabbitmq-reconnect-interval)_ |The interval (in seconds) between two reconnection attempts |int |false |`10` +|_requested-channel-max_ |The initially requested maximum channel number |int |false |`2047` +|_requested-heartbeat_ |The initially requested heartbeat interval (seconds), zero for none |int |false |`60` +|_routing-keys_ |A comma-separated list of routing keys to bind the queue to the exchange |string |false |`#` +|_ssl_ _(rabbitmq-ssl)_ |Whether or not the connection should use SSL |boolean |false |`false` +|_tracing.attribute-headers_ |A comma-separated list of headers that should be recorded as span attributes. Relevant only if tracing.enabled=true |string |false |++``++ +|_tracing.enabled_ |Whether tracing is enabled (default) or disabled |boolean |false |`true` +|_trust-all_ _(rabbitmq-trust-all)_ |Whether to skip trust certificate verification |boolean |false |`false` +|_trust-store-password_ _(rabbitmq-trust-store-password)_ |The password of the JKS trust store |string |false | +|_trust-store-path_ _(rabbitmq-trust-store-path)_ |The path to a JKS trust store |string |false | +|_use-nio_ |Whether usage of NIO Sockets is enabled |boolean |false |`false` +|_user_ |The user name to use when connecting to the broker |string |false |`guest` +|_username_ _(rabbitmq-username)_ |The username used to authenticate to the broker |string |false | +|_virtual-host_ _(rabbitmq-virtual-host)_ |The virtual host to use when connecting to the broker |string |false |`/` +|=== diff --git a/content/docs/smallrye-reactive-messaging/3/_generated/smallrye-rabbitmq-outgoing.adoc b/content/docs/smallrye-reactive-messaging/3/_generated/smallrye-rabbitmq-outgoing.adoc new file mode 100644 index 000000000..5cf3f86f0 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_generated/smallrye-rabbitmq-outgoing.adoc @@ -0,0 +1,38 @@ +[cols="<,<,^,^,<",options="header",] +|=== +|Attribute (_alias_) |Description |Type |Mandatory |Default +|_automatic-recovery-enabled_ |Whether automatic connection recovery is enabled |boolean |false |`false` +|_automatic-recovery-on-initial-connection_ |Whether automatic recovery on initial connections is enabled |boolean |false |`true` +|_client-options-name_ _(rabbitmq-client-options-name)_ |The name of the RabbitMQ Client Option bean used to customize the RabbitMQ client configuration |string |false | +|_connection-timeout_ |The TCP connection timeout (ms); 0 is interpreted as no timeout |int |false |`60000` +|_credentials-provider-name_ _(rabbitmq-credentials-provider-name)_ |The name of the RabbitMQ Credentials Provider bean used to provide dynamic credentials to the RabbitMQ client |string |false | +|_default-routing-key_ |The default routing key to use when sending messages to the exchange |string |false |++``++ +|_default-ttl_ |If specified, the time (ms) sent messages can remain in queues undelivered before they are dead |long |false | +|_exchange.auto-delete_ |Whether the exchange should be deleted after use |boolean |false |`false` +|_exchange.declare_ |Whether to declare the exchange; set to false if the exchange is expected to be set up independently |boolean |false |`true` +|_exchange.durable_ |Whether the exchange is durable |boolean |false |`true` +|_exchange.name_ |The exchange that messages are published to or consumed from. If not set, the channel name is used. If set to "", the default exchange is used. |string |false | +|_exchange.type_ |The exchange type: direct, fanout, headers or topic (default) |string |false |`topic` +|_handshake-timeout_ |The AMQP 0-9-1 protocol handshake timeout (ms) |int |false |`10000` +|_host_ _(rabbitmq-host)_ |The broker hostname |string |false |`localhost` +|_include-properties_ |Whether to include properties when a broker message is passed on the event bus |boolean |false |`false` +|_max-inflight-messages_ |The maximum number of messages to be written to RabbitMQ concurrently; must be a positive number |long |false |`1024` +|_max-outgoing-internal-queue-size_ |The maximum size of the outgoing internal queue |int |false | +|_network-recovery-interval_ |How long (ms) will automatic recovery wait before attempting to reconnect |int |false |`5000` +|_password_ _(rabbitmq-password)_ |The password used to authenticate to the broker |string |false | +|_port_ _(rabbitmq-port)_ |The broker port |int |false |`5672` +|_reconnect-attempts_ _(rabbitmq-reconnect-attempts)_ |The number of reconnection attempts |int |false |`100` +|_reconnect-interval_ _(rabbitmq-reconnect-interval)_ |The interval (in seconds) between two reconnection attempts |int |false |`10` +|_requested-channel-max_ |The initially requested maximum channel number |int |false |`2047` +|_requested-heartbeat_ |The initially requested heartbeat interval (seconds), zero for none |int |false |`60` +|_ssl_ _(rabbitmq-ssl)_ |Whether or not the connection should use SSL |boolean |false |`false` +|_tracing.attribute-headers_ |A comma-separated list of headers that should be recorded as span attributes. Relevant only if tracing.enabled=true |string |false |++``++ +|_tracing.enabled_ |Whether tracing is enabled (default) or disabled |boolean |false |`true` +|_trust-all_ _(rabbitmq-trust-all)_ |Whether to skip trust certificate verification |boolean |false |`false` +|_trust-store-password_ _(rabbitmq-trust-store-password)_ |The password of the JKS trust store |string |false | +|_trust-store-path_ _(rabbitmq-trust-store-path)_ |The path to a JKS trust store |string |false | +|_use-nio_ |Whether usage of NIO Sockets is enabled |boolean |false |`false` +|_user_ |The user name to use when connecting to the broker |string |false |`guest` +|_username_ _(rabbitmq-username)_ |The username used to authenticate to the broker |string |false | +|_virtual-host_ _(rabbitmq-virtual-host)_ |The virtual host to use when connecting to the broker |string |false |`/` +|=== diff --git a/content/docs/smallrye-reactive-messaging/3/_nav.yml b/content/docs/smallrye-reactive-messaging/3/_nav.yml new file mode 100644 index 000000000..4f72a47d2 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/_nav.yml @@ -0,0 +1,124 @@ +title: SmallRye Reactive Messaging +nav: +- title: Home + page: index +- title: Core + items: + - title: Getting Started + page: getting-started + - title: Concepts + items: + - title: Introduction + page: concepts/concepts + - title: Development Model + page: concepts/model + - title: Emitters and Channel + page: concepts/emitter + - title: Connectors + page: concepts/connectors + - title: Acknowledgement + page: concepts/acknowledgement + - title: Blocking Processing + page: concepts/blocking + - title: Method Signatures + page: concepts/signatures + - title: Skipping Messages + page: concepts/skipping + - title: Message Converters + page: concepts/converters + - title: Channel Decorators + page: concepts/decorators + - title: Broadcast + page: concepts/broadcast + - title: Merge channels + page: concepts/merge + - title: '@Incomings' + page: concepts/incomings + - title: Testing + page: concepts/testing + - title: Logging + page: concepts/logging + - title: Advanced Configuration + page: concepts/advanced-config + - title: Message Context + page: concepts/message-context +- title: Kafka + items: + - page: kafka/kafka + - title: Receiving records + page: kafka/receiving-kafka-records + - title: Writing records + page: kafka/writing-kafka-records + - title: Health Checks + page: kafka/health + - title: Using Avro + page: kafka/avro-configuration + - title: Using Protobuf + page: kafka/protobuf-configuration + - title: Rebalance Listeners + page: kafka/consumer-rebalance-listener + - title: Kerberos authentication + page: kafka/kerberos + - title: Accessing the client + page: kafka/client-service + - title: Customizing Default Kafka Configuration + page: kafka/default-configuration + - title: Test Companion for Kafka + page: kafka/test-companion + - title: Kafka Transactions and Exactly-Once Processing + page: kafka/transactions +- title: AMQP 1.0 + items: + - page: amqp/amqp + - title: Receiving messages + page: amqp/receiving-amqp-messages + - title: Sending messages + page: amqp/sending-amqp-messages + - title: Health Checks + page: amqp/health + - title: Client Customization + page: amqp/client-customization + - title: Using RabbitMQ + page: amqp/rabbitmq +- title: RabbitMQ + items: + - page: rabbitmq/rabbitmq + - title: Receiving messages + page: rabbitmq/receiving-messages-from-rabbitmq + - title: Sending messages + page: rabbitmq/sending-messages-to-rabbitmq + - title: Health Checks + page: rabbitmq/rabbitmq-health + - title: Client Customization + page: rabbitmq/rabbitmq-client-customization + - title: Connecting to managed instances + page: rabbitmq/rabbitmq-cloud +- title: Apache Camel + items: + - page: camel/camel + - title: Receiving messages from Camel + page: camel/receiving-messages-from-camel + - title: Sending messages to Camel + page: camel/sending-messages-to-camel + - title: Implementing Camel processor + page: camel/camel-processor + - title: Using existing Camel routes + page: camel/using-existing-routes +- title: JMS + items: + - page: jms/jms + - title: Receiving JMS messages + page: jms/receiving-jms-messages + - title: Sending JMS messages + page: jms/sending-jms-messages + - title: Advanced configuration + page: jms/advanced-jms +- title: MQTT + items: + - page: mqtt/mqtt + - title: Receiving MQTT messages + page: mqtt/receiving-mqtt-messages + - title: Sending MQTT messages + page: mqtt/sending-messages-to-mqtt + - title: Customizing the MQTT client + page: mqtt/client-customization diff --git a/content/docs/smallrye-reactive-messaging/3/amqp/amqp.adoc b/content/docs/smallrye-reactive-messaging/3/amqp/amqp.adoc new file mode 100644 index 000000000..d43eef8f8 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/amqp/amqp.adoc @@ -0,0 +1,49 @@ += AMQP 1.0 Connector +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The AMQP Connector adds support for AMQP 1.0 to Reactive Messaging. + +Advanced Message Queuing Protocol 1.0 (AMQP 1.0) is an open standard for +passing business messages between applications or organizations. + +With this connector, your application can: + +* receive messages from an AMQP Broker or Router. +* send `Message` to an AMQP _address_ + +The AMQP connector is based on the https://vertx.io/docs/vertx-amqp-client/java/[Vert.x AMQP Client]. + +Using the AMQP connector + +To use the AMQP Connector, add the following dependency to your project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.reactive + smallrye-reactive-messaging-amqp + {project-version} + +---- + +The connector name is: `smallrye-amqp`. + +So, to indicate that a channel is managed by this connector you need: + +[source,properties] +---- +# Inbound +mp.messaging.incoming.[channel-name].connector=smallrye-amqp + +# Outbound +mp.messaging.outgoing.[channel-name].connector=smallrye-amqp +---- + +[IMPORTANT] +.RabbitMQ +==== + +To use RabbitMQ, refer to xref:#amqp-rabbitmq[Using RabbitMQ]. + +==== diff --git a/content/docs/smallrye-reactive-messaging/3/amqp/client-customization.adoc b/content/docs/smallrye-reactive-messaging/3/amqp/client-customization.adoc new file mode 100644 index 000000000..4dbd659ec --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/amqp/client-customization.adoc @@ -0,0 +1,35 @@ += Customizing the underlying AMQP client +:version-root: ../ +include::{version-root}_attributes.adoc[] + +You can customize the underlying AMQP Client configuration by +_producing_ an instance of +https://vertx.io/docs/apidocs/io/vertx/amqp/AmqpClientOptions.html[`AmqpClientOptions`]: + +[source,java] +---- +include::{examples-dir}/amqp/customization/ClientProducers.java[tags=named,indent=0] +---- + +This instance is retrieved and used to configure the client used by the +connector. You need to indicate the name of the client using the +`client-options-name` attribute: + +[source,properties] +---- +mp.messaging.incoming.prices.client-options-name=my-named-options +---- + +== Client capabilities + +Both incoming and outgoing AMQP channels can be configured with a list +of capabilities to declare during sender and receiver connections with +the AMQP broker. Note that supported capability names are broker +specific. + +[source,properties] +---- +mp.messaging.incoming.sink.capabilities=temporary-topic +... +mp.messaging.outgoing.source.capabilities=shared +---- diff --git a/content/docs/smallrye-reactive-messaging/3/amqp/health.adoc b/content/docs/smallrye-reactive-messaging/3/amqp/health.adoc new file mode 100644 index 000000000..79ec6eb9f --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/amqp/health.adoc @@ -0,0 +1,34 @@ += Health reporting +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The AMQP connector reports the startup, liveness, and readiness of each +inbound (Receiving messages) and outbound (sending messages) channel +managed by the connector: + +* Startup :: For both inbound and outbound, the startup probe reports _OK_ when the +connection with the broker is established, and the AMQP senders and +receivers are opened (the links are attached to the broker). +* Liveness :: For both inbound and outbound, the liveness check verifies that the +connection is established. The check still returns _OK_ if the +connection got cut, but we are attempting a reconnection. +* Readiness :: For the inbound, it checks that the connection is established and the +receiver is opened. Unlike the liveness check, this probe reports _KO_ +until the connection is re-established. For the outbound, it checks that +the connection is established and the sender is opened. Unlike the +liveness check, this probe reports _KO_ until the connection is +re-established. + +[NOTE] +==== + +To disable health reporting, set the `health-enabled` attribute for the +channel to `false`. + +==== + +Note that a message processing failures _nacks_ the message, which is +then handled by the failure-strategy. It is the responsibility of the +`failure-strategy` to report the failure and influence the outcome of +the checks. The `fail` failure strategy reports the failure, and so the +check will report the fault. diff --git a/content/docs/smallrye-reactive-messaging/3/amqp/rabbitmq.adoc b/content/docs/smallrye-reactive-messaging/3/amqp/rabbitmq.adoc new file mode 100644 index 000000000..3103b19e2 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/amqp/rabbitmq.adoc @@ -0,0 +1,38 @@ += Using RabbitMQ +:version-root: ../ +include::{version-root}_attributes.adoc[] + +This connector is for AMQP 1.0. RabbitMQ implements AMQP 0.9.1. RabbitMQ +does not provide AMQP 1.0 by default, but there is a plugin for it. To +use RabbitMQ with this connector, enable and configure the https://github.com/rabbitmq/rabbitmq-amqp1.0/blob/v3.8.x/README.md[AMQP 1.0 +plugin]. + +Despite the plugin, a few features won’t work with RabbitMQ. Thus, we +recommend the following configurations. + +To receive messages from RabbitMQ: + +* Set `durable` to `false` + +[source,properties] +---- +mp.messaging.incoming.prices.connector=smallrye-amqp +mp.messaging.incoming.prices.durable=false +---- + +To send messages to RabbitMQ: + +* set the destination `address` (anonymous sender are not supported) + +[source,properties] +---- +mp.messaging.outgoing.generated-price.connector=smallrye-amqp +mp.messaging.outgoing.generated-price.address=prices +---- + +It’s not possible to change the destination dynamically (using message +metadata) when using RabbitMQ. The connector automatically detects that +the broker does not support anonymous sender (See +http://docs.oasis-open.org/amqp/anonterm/v1.0/anonterm-v1.0.html). + +Alternatively, you can use the xref:rabbitmq/rabbitmq.adoc[RabbitMQ connector]. diff --git a/content/docs/smallrye-reactive-messaging/3/amqp/receiving-amqp-messages.adoc b/content/docs/smallrye-reactive-messaging/3/amqp/receiving-amqp-messages.adoc new file mode 100644 index 000000000..751056c73 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/amqp/receiving-amqp-messages.adoc @@ -0,0 +1,257 @@ += Receiving messages from AMQP +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The AMQP connector lets you retrieve messages from an https://www.amqp.org/product/architecture[AMQP broker or +router]. The AMQP connector +retrieves _AMQP Messages_ and maps each of them into Reactive Messaging +`Messages`. + +== Example + +Let’s imagine you have an AMQP broker (such as https://activemq.apache.org/components/artemis/[Apache ActiveMQ +Artemis]) running, and +accessible using the `amqp:5672` address (by default it would use +`localhost:5672`). Configure your application to receive AMQP Messages +on the `prices` channel as follows: + +[source,properties] +---- +amqp-host=amqp # <1> +amqp-port=5672 # <2> +amqp-username=my-username # <3> +amqp-password=my-password # <4> + +mp.messaging.incoming.prices.connector=smallrye-amqp # <5> +---- + +[arabic] +. Configures the broker/router host name. You can do it per channel +(using the `host` attribute) or globally using `amqp-host` +. Configures the broker/router port. You can do it per channel (using +the `port` attribute) or globally using `amqp-port`. The default is +`5672`. +. Configures the broker/router username if required. You can do it per +channel (using the `username` attribute) or globally using +`amqp-username`. +. Configures the broker/router password if required. You can do it per +channel (using the `password` attribute) or globally using +`amqp-password`. +. Instructs the `prices` channel to be managed by the AMQP connector + +[NOTE] +==== + +You don’t need to set the AMQP _address_. By default, it uses the +channel name (`prices`). You can configure the `address` attribute to +override it. + +==== + +Then, your application receives `Message++<++Double++>++`. You can consume the +payload directly: + +[source,java] +---- +include::{examples-dir}/amqp/inbound/AmqpPriceConsumer.java[indent=0] +---- + +Or, you can retrieve the `Message++<++Double++>++`: + +[source,java] +---- +include::{examples-dir}/amqp/inbound/AmqpPriceMessageConsumer.java[indent=0] +---- + +== Deserialization + +The connector converts incoming AMQP Messages into Reactive Messaging +`Message++<++T++>++` instances. `T` depends on the _body_ of the received AMQP +Message. + +The http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-types-v1.0-os.html[AMQP Type +System] +defines the supported types. + +[width="100%",cols="66%,34%",options="header",] +|=== +|AMQP Body Type |`++<++T++>++` +|AMQP Value containing a http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-types-v1.0-os.html#section-primitive-type-definitions[AMQP Primitive Type] |the corresponding Java type +|AMQP Value using the `Binary` type |`byte++[]++` +|AMQP Sequence |`List` +|AMQP Data (with binary content) and the `content-type` is set to `application/json` |https://vertx.io/docs/apidocs/io/vertx/core/json/JsonObject.html[`JsonObject`] +|AMQP Data with a different `content-type` |`byte++[]++` +|=== + +If you send objects with this AMQP connector (outbound connector), it +gets encoded as JSON and sent as binary. The `content-type` is set to +`application/json`. You can receive this payload using (Vert.x) JSON +Objects, and then map it to the object class you want: + +[source,java] +---- +@ApplicationScoped +public static class Generator { + + @Outgoing("to-amqp") + public Multi prices() { // <1> + AtomicInteger count = new AtomicInteger(); + return Multi.createFrom().ticks().every(Duration.ofMillis(1000)) + .map(l -> new Price().setPrice(count.incrementAndGet())) + .onOverflow().drop(); + } + +} + +@ApplicationScoped +public static class Consumer { + + List prices = new CopyOnWriteArrayList<>(); + + @Incoming("from-amqp") + public void consume(JsonObject p) { // <2> + Price price = p.mapTo(Price.class); // <3> + prices.add(price); + } + + public List list() { + return prices; + } +} +---- + +[arabic] +. The `Price` instances are automatically encoded to JSON by the +connector +. You can receive it using a `JsonObject` +. Then, you can reconstruct the instance using the `mapTo` method + +== Inbound Metadata + +Messages coming from AMQP contains an instance of link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-amqp/{javadoc-version}/io/smallrye/reactive/messaging/amqp/IncomingAmqpMetadata.html[`IncomingAmqpMetadata`] + +[source,java] +---- +include::{examples-dir}/amqp/inbound/AmqpMetadataExample.java[tags=code,indent=0] +---- + +== Acknowledgement + +When a Reactive Messaging `Message` associated with an AMQP Message is +acknowledged, it informs the broker that the message has been +_accepted_. + +== Failure Management + +If a message produced from an AMQP message is _nacked_, a failure +strategy is applied. The AMQP connector supports six strategies: + +* `fail` - fail the application; no more AMQP messages will be +processed (default). The AMQP message is marked as rejected. +* `accept` - this strategy marks the AMQP message as _accepted_. The +processing continues ignoring the failure. Refer to the http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-messaging-v1.0-os.html#type-accepted[accepted +delivery state +documentation]. +* `release` - this strategy marks the AMQP message as _released_. The +processing continues with the next message. The broker can redeliver +the message. Refer to the http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-messaging-v1.0-os.html#type-released[released delivery state +documentation]. +* `reject` - this strategy marks the AMQP message as rejected. The +processing continues with the next message. Refer to the http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-messaging-v1.0-os.html#type-rejected[rejected +delivery state +documentation]. +* `modified-failed` - this strategy marks the AMQP message as +_modified_ and indicates that it failed (with the `delivery-failed` +attribute). The processing continues with the next message, but the +broker may attempt to redeliver the message. Refer to the http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-messaging-v1.0-os.html#type-modified[modified +delivery state +documentation] +* `modified-failed-undeliverable-here` - this strategy marks the AMQP +message as _modified_ and indicates that it failed (with the +`delivery-failed` attribute). It also indicates that the application +cannot process the message, meaning that the broker will not attempt +to redeliver the message to this node. The processing continues with +the next message. Refer to the http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-messaging-v1.0-os.html#type-modified[modified delivery state +documentation] + +== Configuration Reference + +include::{generated-dir}/smallrye-amqp-incoming.adoc[indent=0] + +You can also pass any property supported by the https://vertx.io/docs/vertx-amqp-client/java/[Vert.x AMQP +client] as attribute. + +To use an existing _address_ or _queue_, you need to configure the +`address`, `container-id` and, optionally, the `link-name` attributes. +For example, if you have an Apache Artemis broker configured with: + +[source,xml] +---- + + +
people
+ true + artemis +
+
+---- + +You need the following configuration: + +[source,properties] +---- +mp.messaging.incoming.people.connector=smallrye-amqp +mp.messaging.incoming.people.durable=true +mp.messaging.incoming.people.address=people +mp.messaging.incoming.people.container-id=people +---- + +You may need to configure the `link-name` attribute, if the queue name +is not the channel name: + +[source,properties] +---- +mp.messaging.incoming.people-in.connector=smallrye-amqp +mp.messaging.incoming.people-in.durable=true +mp.messaging.incoming.people-in.address=people +mp.messaging.incoming.people-in.container-id=people +mp.messaging.incoming.people-in.link-name=people +---- + +== Receiving Cloud Events + +The AMQP connector supports https://cloudevents.io/[Cloud Events]. +When the connector detects a _structured_ or _binary_ Cloud Events, it +adds a link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/io/smallrye/reactive/messaging/ce/IncomingCloudEventMetadata.html[`IncomingCloudEventMetadata`] +into the metadata of the `Message`. `IncomingCloudEventMetadata` +contains accessors to the mandatory and optional Cloud Event attributes. + +If the connector cannot extract the Cloud Event metadata, it sends the +Message without the metadata. + +=== Binary Cloud Events + +For `binary` Cloud Events, *all* mandatory Cloud Event attributes must +be set in the AMQP application properties, prefixed by `cloudEvents:` +(as mandated by the https://github.com/cloudevents/spec/blob/v1.0.1/amqp-protocol-binding.md[protocol +binding]). +The connector considers headers starting with the `cloudEvents:` prefix +but not listed in the specification as extensions. You can access them +using the `getExtension` method from `IncomingCloudEventMetadata`. + +The `datacontenttype` attribute is mapped to the `content-type` header +of the record. + +=== Structured Cloud Events + +For `structured` Cloud Events, the event is encoded in the record’s +value. Only JSON is supported, so your event must be encoded as JSON in +the record’s value. + +Structured Cloud Event must set the `content-type` header of the record +to `application/cloudevents{plus}json; charset=UTF-8`. The message body must +be a valid JSON object containing at least all the mandatory Cloud +Events attributes. + +If the record is a structured Cloud Event, the created Message’s payload +is the Cloud Event `data`. diff --git a/content/docs/smallrye-reactive-messaging/3/amqp/sending-amqp-messages.adoc b/content/docs/smallrye-reactive-messaging/3/amqp/sending-amqp-messages.adoc new file mode 100644 index 000000000..2dc73dee1 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/amqp/sending-amqp-messages.adoc @@ -0,0 +1,248 @@ += Sending messages to AMQP +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The AMQP connector can write Reactive Messaging `Messages` as AMQP +Messages. + +== Example + +Let’s imagine you have an AMQP broker (such as https://activemq.apache.org/components/artemis/[Apache ActiveMQ +Artemis]) running, and +accessible using the `amqp:5672` address (by default it would use +`localhost:5672`). Configure your application to send the messages from +the `prices` channel as AMQP Message as follows: + +[source,properties] +---- +amqp-host=amqp # <1> +amqp-port=5672 # <2> +amqp-username=my-username # <3> +amqp-password=my-password # <4> + +mp.messaging.outgoing.prices.connector=smallrye-amqp # <5> +---- + +[arabic] +. Configures the broker/router host name. You can do it per channel +(using the `host` attribute) or globally using `amqp-host` +. Configures the broker/router port. You can do it per channel (using +the `port` attribute) or globally using `amqp-port`. The default is +`5672`. +. Configures the broker/router username if required. You can do it per +channel (using the `username` attribute) or globally using +`amqp-username`. +. Configures the broker/router password if required. You can do it per +channel (using the `password` attribute) or globally using +`amqp-password`. +. Instructs the `prices` channel to be managed by the AMQP connector + +[NOTE] +==== + +You don’t need to set the _address_. By default, it uses the channel +name (`prices`). You can configure the `address` attribute to override +it. + +==== + +Then, your application must send `Message++<++Double++>++` to the `prices` +channel. It can use `double` payloads as in the following snippet: + +[source,java] +---- +include::{examples-dir}/amqp/outbound/AmqpPriceProducer.java[indent=0] +---- + +Or, you can send `Message++<++Double++>++`: + +[source,java] +---- +include::{examples-dir}/amqp/outbound/AmqpPriceMessageProducer.java[indent=0] +---- + +== Serialization + +When receiving a `Message++<++T++>++`, the connector convert the message into an +AMQP Message. The payload is converted to the AMQP Message _body_. + +[width="100%",cols="50%,50%",options="header",] +|=== +|`T` |AMQP Message Body +|primitive types or `String` |AMQP Value with the payload +|`Instant` or `UUID` |AMQP Value using the corresponding AMQP Type +|https://vertx.io/docs/apidocs/io/vertx/core/json/JsonObject.html[`JsonObject`] or https://vertx.io/docs/apidocs/io/vertx/core/json/JsonArray.html[`JsonArray`] |AMQP Data using a binary content. The `content-type` is set to `application/json` +|`io.vertx.mutiny.core.buffer.Buffer` |AMQP Data using a binary content. No `content-type` set +|Any other class |The payload is converted to JSON (using a Json Mapper). The result is wrapped into AMQP Data using a *binary* content. The `content-type` is set to `application/json` +|=== + +If the message payload cannot be serialized to JSON, the message is +_nacked_. + +== Outbound Metadata + +When sending `Messages`, you can add an instance of +link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-amqp/{javadoc-version}/io/smallrye/reactive/messaging/amqp/OutgoingAmqpMetadata.html[`OutgoingAmqpMetadata`] +to influence how the message is going to be sent to AMQP. For example, you +can configure the subjects, properties: + +[source,java] +---- +include::{examples-dir}/amqp/outbound/AmqpOutboundMetadataExample.java[tags=code,indent=0] +---- + +== Dynamic address names + +Sometimes it is desirable to select the destination of a message +dynamically. In this case, you should not configure the address inside +your application configuration file, but instead, use the outbound +metadata to set the address. + +For example, you can send to a dynamic address based on the incoming +message: + +[source,java] +---- +include::{examples-dir}/amqp/outbound/AmqpOutboundDynamicAddressExample.java[tags=code,indent=0] +---- + +[NOTE] +==== + +To be able to set the address per message, the connector is using an +_anonymous sender_. + +==== + +== Acknowledgement + +By default, the Reactive Messaging `Message` is acknowledged when the +broker acknowledged the message. When using routers, this +acknowledgement may not be enabled. In this case, configure the +`auto-acknowledgement` attribute to acknowledge the message as soon as +it has been sent to the router. + +If an AMQP message is rejected/released/modified by the broker (or +cannot be sent successfully), the message is nacked. + +== Back Pressure and Credits + +The back-pressure is handled by AMQP _credits_. The outbound connector +only requests the amount of allowed credits. When the amount of credits +reaches 0, it waits (in a non-blocking fashion) until the broker grants +more credits to the AMQP sender. + +== Configuration Reference + +include::{generated-dir}/smallrye-amqp-outgoing.adoc[indent=0] + +You can also pass any property supported by the https://vertx.io/docs/vertx-amqp-client/java/[Vert.x AMQP +client] as attribute. + +== Using existing destinations + +To use an existing _address_ or _queue_, you need to configure the +`address`, `container-id` and, optionally, the `link-name` attributes. +For example, if you have an Apache Artemis broker configured with: + +[source,xml] +---- + + +
people
+ true + artemis +
+
+---- + +You need the following configuration: + +[source,properties] +---- +mp.messaging.outgoing.people.connector=smallrye-amqp +mp.messaging.outgoing.people.durable=true +mp.messaging.outgoing.people.address=people +mp.messaging.outgoing.people.container-id=people +---- + +You may need to configure the `link-name` attribute, if the queue name +is not the channel name: + +[source,properties] +---- +mp.messaging.outgoing.people-out.connector=smallrye-amqp +mp.messaging.outgoing.people-out.durable=true +mp.messaging.outgoing.people-out.address=people +mp.messaging.outgoing.people-out.container-id=people +mp.messaging.outgoing.people-out.link-name=people +---- + +To use a `MULTICAST` queue, you need to provide the _FQQN_ +(Fully-qualified queue name) instead of just the name of the queue: + +[source,properties] +---- +mp.messaging.outgoing.people-out.connector=smallrye-amqp +mp.messaging.outgoing.people-out.durable=true +mp.messaging.outgoing.people-out.address=foo +mp.messaging.outgoing.people-out.container-id=foo + +mp.messaging.incoming.people-out.connector=smallrye-amqp +mp.messaging.incoming.people-out.durable=true +mp.messaging.incoming.people-out.address=foo::bar # Note the syntax: address-name::queue-name +mp.messaging.incoming.people-out.container-id=bar +mp.messaging.incoming.people-out.link-name=people +---- + +More details about the AMQP Address model can be found on https://activemq.apache.org/components/artemis/documentation/2.0.0/address-model.html[the Artemis +documentation]. + +== Sending Cloud Events + +The AMQP connector supports https://cloudevents.io/[Cloud Events]. The +connector sends the outbound record as Cloud Events if: + +* the message metadata contains an +`io.smallrye.reactive.messaging.ce.OutgoingCloudEventMetadata` +instance, +* the channel configuration defines the `cloud-events-type` and +`cloud-events-source` attributes. + +You can create +`io.smallrye.reactive.messaging.ce.OutgoingCloudEventMetadata` instances +using: + +[source,java] +---- +include::{examples-dir}/amqp/outbound/AmqpCloudEventProcessor.java[indent=0] +---- + +If the metadata does not contain an id, the connector generates one +(random UUID). The `type` and `source` can be configured per message or +at the channel level using the `cloud-events-type` and +`cloud-events-source` attributes. Other attributes are also +configurable. + +The metadata can be contributed by multiple methods, however, you must +always retrieve the already existing metadata to avoid overriding the +values: + +[source,java] +---- +include::{examples-dir}/amqp/outbound/AmqpCloudEventMultipleProcessors.java[indent=0] +---- + +By default, the connector sends the Cloud Events using the _binary_ +format. You can write _structured_ Cloud Events by setting the +`cloud-events-mode` to `structured`. Only JSON is supported, so the +created records had its `content-type` header set to +`application/cloudevents{plus}json; charset=UTF-8` + +[NOTE] +==== + +you can disable the Cloud Event support by setting the `cloud-events` +attribute to `false` + +==== diff --git a/content/docs/smallrye-reactive-messaging/3/camel/camel-processor.adoc b/content/docs/smallrye-reactive-messaging/3/camel/camel-processor.adoc new file mode 100644 index 000000000..20327120f --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/camel/camel-processor.adoc @@ -0,0 +1,37 @@ += The processor pattern using Camel +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Using the processor pattern, you can consume on a channel using a Camel +component, and produce on a channel using another Camel component. In +that case, the headers present in the incoming metadata will be +forwarded in the outgoing metadata. + +== Example + +Let’s imagine you want to read messages from a Nats subject, process it +and produce a message on a Kafka topic. + +[source,properties] +---- +mp.messaging.incoming.mynatssubject.connector=smallrye-camel # <1> +mp.messaging.incoming.mynatssubject.endpoint-uri=nats:mynatssubject # <2> +mp.messaging.outgoing.mykafkatopic.connector=smallrye-camel # <3> +mp.messaging.outgoing.mykafkatopic.endpoint-uri=kafka:mykafkatopic# <4> + +camel.component.nats.servers=127.0.0.1:5555 # <5> +camel.component.kafka.brokers=127.0.0.1:9092 # <6> +---- + +[arabic] +. Sets the connector for the `mynatssubject` channel +. Configures the `endpoint-uri` for nats subject +. Sets the connector for the `mykafkatopic` channel +. Configures the `endpoint-uri` for the kafka topic +. Sets the URL of the nats server to use +. Sets the URL of a kafka broker to use + +[source,java] +---- +include::{examples-dir}/camel/processor/CamelProcessor.java[indent=0] +---- diff --git a/content/docs/smallrye-reactive-messaging/3/camel/camel.adoc b/content/docs/smallrye-reactive-messaging/3/camel/camel.adoc new file mode 100644 index 000000000..0ffebef57 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/camel/camel.adoc @@ -0,0 +1,67 @@ += Apache Camel Connector +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The Camel connector adds support for Apache Camel to Reactive Messaging. + +https://camel.apache.org/[Camel] is an open source integration +framework let you integrate various systems consuming or producing data. +Camel implements the Enterprise Integration Patterns and provides +several hundred of components used to access databases, message queues, +APIs or basically https://camel.apache.org/components/latest/[anything under the +sun]. + +== Introduction + +Camel is not a messaging broker. But, it allows your Reactive Messaging +application to retrieve data from almost anything and send data to +almost anything. + +So if you want to send Reactive Messaging `Message` to Telegram or +retrieve data from Salesforce or SAP, this is the connector you need. + +One of the Camel cornerstone is the `endpoint` and its `uri` encoding +the connection to an external system. For example, +`file:orders/?delete=true&charset=utf-8` instructs Camel to read the +files from the `orders` directory. URI format and parameters are listed +on the component documentation, such as the https://camel.apache.org/components/latest/file-component.html[File +component]. + +== Using the camel connector + +To you the camel Connector, add the following dependency to your +project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.reactive + smallrye-reactive-messaging-camel + {project-version} + +---- + +You will also need the dependency of the Camel component you are using. +For example, if you want to process files, you would need to add the +Camel File Component artifact: + +[source,xml,subs="attributes+"] +---- + + org.apache.camel + camel-file + {camel-version} + +---- + +The connector name is: `smallrye-camel`. + +So, to indicate that a channel is managed by this connector you need: + +.... +# Inbound +mp.messaging.incoming.[channel-name].connector=smallrye-camel + +# Outbound +mp.messaging.outgoing.[channel-name].connector=smallrye-camel +.... diff --git a/content/docs/smallrye-reactive-messaging/3/camel/receiving-messages-from-camel.adoc b/content/docs/smallrye-reactive-messaging/3/camel/receiving-messages-from-camel.adoc new file mode 100644 index 000000000..65338aedc --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/camel/receiving-messages-from-camel.adoc @@ -0,0 +1,89 @@ += Retrieving data using Camel +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Camel provides many components. To keep this documentation focused on +the integration with Camel, we use the https://camel.apache.org/components/latest/file-component.html[File +component]. +This component let use read files from a directory. So the connector +configured with this component creates a `Message` for each file located +in the directory. As soon as a file is dropped in the directory, a new +`Message` is created. + +== Example + +Let’s imagine you want to read the files from the `orders` directory and +send them to the `files` channel. Configuring the Camel connector to +gets the _file_ from this directory only requires 2 properties: + +[source,properties] +---- +mp.messaging.incoming.files.connector=smallrye-camel # <1> +mp.messaging.incoming.files.endpoint-uri=file:orders/?delete=true&charset=utf-8 # <2> +---- + +[arabic] +. Sets the connector for the `files` channel +. Configures the `endpoint-uri` + +Then, your application receives `Message++<++GenericFile++<++File++>>++`. + +[NOTE] +==== + +The Camel File component produces +`org.apache.camel.component.file.GenericFile` instances. You can +retrieve the actual `File` using `getFile()`. + +==== + +You can consume the payload directly: + +[source,java] +---- +include::{examples-dir}/camel/inbound/CamelFileConsumer.java[indent=0] +---- + +You can also retrieve the `Message++<++GenericFile++<++File++>>++`: + +[source,java] +---- +include::{examples-dir}/camel/inbound/CamelFileMessageConsumer.java[indent=0] +---- + +== Deserialization + +Each Camel component is producing specific objects. As we have seen, the +File component produces `GenericFile`. + +Refer to the component documentation to check which type is produced. + +== Inbound Metadata + +Messages coming from Camel contains an instance of +link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-camel/{javadoc-version}/io/smallrye/reactive/messaging/camel/IncomingExchangeMetadata.html[`IncomingExchangeMetadata`] +in the metadata. + +[source,java] +---- +include::{examples-dir}/camel/inbound/IncomingCamelMetadataExample.java[indent=0] +---- + +This object lets you retrieve the Camel `Exchange`. + +== Failure Management + +If a message produced from a Camel exchange is _nacked_, a failure +strategy is applied. The Camel connector supports 3 strategies: + +* `fail` - fail the application, no more MQTT messages will be +processed. (default) The offset of the record that has not been +processed correctly is not committed. +* `ignore` - the failure is logged, but the processing continue. + +In both cases, the `exchange` is marked as rollback only and the nack +reason is attached to the exchange. + +== Configuration Reference + +include::{generated-dir}/smallrye-camel-incoming.adoc[indent=0] diff --git a/content/docs/smallrye-reactive-messaging/3/camel/sending-messages-to-camel.adoc b/content/docs/smallrye-reactive-messaging/3/camel/sending-messages-to-camel.adoc new file mode 100644 index 000000000..8bf614150 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/camel/sending-messages-to-camel.adoc @@ -0,0 +1,76 @@ += Sending data with Camel +:version-root: ../ +include::{version-root}_attributes.adoc[] + +You can use the Camel connector to send data to almost any type of +system. + +To keep this document focused on the Camel connector, we use the Camel +File component. However, the connector can be used with any Camel +component. + +== Example + +Let’s imagine you want to write generated prices into files. Configure +your application to write the messages from the `prices` channel into a +files as follows: + +[source,properties] +---- +mp.messaging.outgoing.prices.connector=smallrye-camel # <1> +mp.messaging.outgoing.prices.endpoint-uri=file:prices/?fileName=${date:now:yyyyMMddssSS}.txt&charset=utf-8 # <2> +---- + +[arabic] +. Sets the connector for the `prices` channel +. Configure the `endpoint-uri` to write into files in the `prices` +directory + +[IMPORTANT] +==== + +Depending on your implementation of MicroProfile Reactive Messaging, the +`$` may need to be escaped as follows: `$$++{++...}` + +==== + +Then, your application must send `Message++<++String++>++` to the `prices` +channel. It can use `String` payloads as in the following snippet: + +[source,java] +---- +include::{examples-dir}/camel/outbound/CamelPriceProducer.java[indent=0] +---- + +Or, you can send `Message++<++Double++>++`: + +[source,java] +---- +include::{examples-dir}/camel/outbound/CamelPriceMessageProducer.java[indent=0] +---- + +== Serialization + +The serialization is handled by the Camel component. Refer to the Camel +documentation to check which payload type is supported by the component. + +== Outbound Metadata + +When sending `Messages`, you can add an instance +of link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-camel/{javadoc-version}/io/smallrye/reactive/messaging/camel/OutgoingExchangeMetadata.html[`OutgoingExchangeMetadata`] +to the message metadata. You can then influence how the outbound Camel +`Exchange` is created, for example by adding properties: + +[source,java] +---- +include::{examples-dir}/camel/outbound/CamelOutboundMetadataExample.java[tags=code,indent=0] +---- + +== Acknowledgement + +The incoming messages are acknowledged when the underlying Camel +exchange completes. If the exchange fails, the message is nacked. + +== Configuration Reference + +include::{generated-dir}/smallrye-camel-outgoing.adoc[indent=0] diff --git a/content/docs/smallrye-reactive-messaging/3/camel/using-existing-routes.adoc b/content/docs/smallrye-reactive-messaging/3/camel/using-existing-routes.adoc new file mode 100644 index 000000000..12e8ff3ac --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/camel/using-existing-routes.adoc @@ -0,0 +1,59 @@ += Using the Camel API +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The Camel connector is based on the https://camel.apache.org/components/latest/reactive-streams-component.html[Reactive Streams +support] +from Camel. If you have an application already using the Camel API +(routes, `from`...), you can integrate it with Reactive Messaging. + +== Getting the CamelReactiveStreamsService + +Once you add the Camel connector to your application, you can retrieve +the +`org.apache.camel.component.reactive.streams.api.CamelReactiveStreamsService` +object: + +[source,java] +---- +include::{examples-dir}/camel/api/CamelApi.java[tags=reactive,indent=0] +---- + +This `CamelReactiveStreamsService` lets you create `Publisher` and +`Subscriber` instances from existing routes. + +== Using Camel Route with @Outgoing + +If you have an existing Camel route, you can transform it as a +`Publisher` using the `CamelReactiveStreamsService`. Then, you can +return this `Publisher` from a method annotated with `@Outgoing`: + +[source,java] +---- +include::{examples-dir}/camel/api/CamelApi.java[tags=source,indent=0] +---- + +You can also use `RouteBuilder`: + +[source,java] +---- +include::{examples-dir}/camel/api/CamelApi.java[tags=source-route-builder,indent=0] +---- + +Using Camel Route with @Incoming + +If you have an existing Camel route, you can transform it as a +`Subscriber` using the `CamelReactiveStreamsService`. Then, you can +return this `Subscriber` from a method annotated with `@Incoming`: + +[source,java] +---- +include::{examples-dir}/camel/api/CamelApi.java[tags=sink,indent=0] +---- + +You can also use a producer: + +[source,java] +---- +include::{examples-dir}/camel/api/CamelApi.java[tags=producer,indent=0] +---- diff --git a/content/docs/smallrye-reactive-messaging/3/concepts/acknowledgement.adoc b/content/docs/smallrye-reactive-messaging/3/concepts/acknowledgement.adoc new file mode 100644 index 000000000..6dea20a6e --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/concepts/acknowledgement.adoc @@ -0,0 +1,173 @@ += Acknowledgement +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Acknowledgment is an essential concept in messaging. A message is +acknowledged when its processing or reception has been successful. It +allows the broker to move to the next message. + +How acknowledgment is used, and the exact behavior in terms of retry and +resilience depends on the broker. For example, for Kafka, it would +commit the offset. For AMQP, it would inform the broker that the message +has been _accepted_. + +Reactive Messaging supports acknowledgement. The default acknowledgement +depends on the method signature. Also, the acknowledgement policy can be +configured using the `@Acknowledgement` annotation. + +== Chain of acknowledgment + +If we reuse this example: + +[source,java] +---- +include::{examples-dir}/beans/Chain.java[tags=chain,indent=0] +---- + +The framework automatically acknowledges the message received from the +`sink` channel when the `consume` method returns. As a consequence, the +message received by the `process` method is acknowledged, and so on. In +other words, it creates a chain of acknowledgement - from the outbound +channel to the inbound channel. + +When using connectors to receive and consume messages, the outbound +connector acknowledges the messages when they are dispatched +successfully to the broker. The acknowledgment chain would, as a result, +acknowledges the inbound connector, which would be able to send an +acknowledgment to the broker. + +This chain of acknowledgment is automatically implemented when +processing payloads. + +== Acknowledgment when using Messages + +When using `Messages`, the user controls the acknowledgment, and so the +chain is not formed automatically. It gives you more flexibility about +when and how the incoming messages are acknowledged. + +If you create a `Message` using the `with` method, is copy the +acknowledgment function from the incoming message: + +[source,java] +---- +include::{examples-dir}/ack/MessageAckExamples.java[tags=with-payload,indent=0] +---- + +To have more control over the acknowledgment, you can create a brand new +`Message` and pass the acknowledgment function: + +[source,java] +---- +include::{examples-dir}/ack/MessageAckExamples.java[tags=message-creation,indent=0] +---- + +However, you may need to create the acknowledgment chain, to acknowledge +the incoming message: + +[source,java] +---- +include::{examples-dir}/ack/MessageAckExamples.java[tags=process,indent=0] +---- + +To trigger the acknowledgment of the incoming message, use the `ack()` +method. It returns a `CompletionStage`, receiving `null` as value when +the acknowledgment has completed. + +== Acknowledgment when using streams + +When transforming streams of `Message`, the acknowledgment is delegated +to the user. It means that it’s up to the user to acknowledge the +incoming messages: + +[source,java] +---- +include::{examples-dir}/ack/StreamAckExamples.java[tags=message,indent=0] +---- + +In the previous example, we only generate a single message per incoming +message so that we can use the `with` method. It becomes more +sophisticated when grouping incoming messages or when each incoming +message produces multiple messages. + +In the case of a stream of payloads, the default strategy acknowledges +the incoming messages before being processed by the method (regardless +of the outcome). + +[source,java] +---- +include::{examples-dir}/ack/StreamAckExamples.java[tags=payload,indent=0] +---- + +== Controlling acknowledgement + +The link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/org/eclipse/microprofile/reactive/messaging/Acknowledgment.html[`Acknowledgment`] +annotation lets you customize the default strategy presented in the +previous sections. The `@Acknowledgement` annotation takes a _strategy_ +as parameter. Reactive Messaging proposed 4 strategies: + +* `POST++_++PROCESSING` - the acknowledgement of the incoming message is +executed once the produced message is acknowledged. +* `PRE++_++PROCESSING` - the acknowledgement of the incoming message is +executed before the message is processed by the method. +* `MANUAL` - the acknowledgement is doe by the user. +* `NONE` - No acknowledgment is performed, neither manually or +automatically. + +It is recommended to use `POST++_++PROCESSING` as it guarantees that the +full processing has completed before acknowledging the incoming message. +However, sometimes it’s not possible, and this strategy is not available +if you manipulate streams of `Messages` or payloads. + +The `PRE++_++PROCESSING` strategy can be useful to acknowledge a message +early in the process: + +[source,java] +---- +include::{examples-dir}/ack/PreAckExamples.java[tags=pre,indent=0] +---- + +It cuts the acknowledgment chain, meaning that the rest of the +processing is not linked to the incoming message anymore. This strategy +is the default strategy when manipulating streams of payloads. + +Refer to the xref:concepts/signatures.adoc[signature list] to determine +which strategies are available for a specific method signature and +what’s the default strategy. + +== Negative acknowledgement + +Messages can also be _nacked_, which indicates that the message was not +processed correctly. The `Message.nack` method indicates failing +processing (and supply the reason), and, as for successful +acknowledgment, the _nack_ is propagated through the chain of messages. + +If the message has been produced by a connector, this connector +implements specific behavior when receiving a _nack_. It can fail +(default), or ignore the failing, or implement a dead-letter queue +mechanism. Refer to the connector documentation for further details +about the available strategies. + +If the message is sent by an emitter using the `send(P)` method, the +returned `CompletionStage` is completed _exceptionally_ with the _nack_ +reason. + +[source,java] +---- +include::{examples-dir}/ack/NackOnEmitter.java[tags=emitter,indent=0] +---- + +Negative acknowledgment can be manual or automatic. If your method +handles instances of `Message` and the acknowledgment strategy is +`MANUAL`, you can nack a message explicitly. You must indicate the +reason (an exception) when calling the `nack` method. As for successful +acknowledgment, the `nack` returns a `CompletionStage` completed when +the `nack` has been processed. + +If your method uses the `POST++_++PROCESSING` acknowledgment strategy, and +the method fails (either by throwing an exception or by producing a +failure), the message is automatically nacked with the caught exception: + +[source,java] +---- +include::{examples-dir}/ack/AutoNack.java[tags=auto-nack,indent=0] +---- diff --git a/content/docs/smallrye-reactive-messaging/3/concepts/advanced-config.adoc b/content/docs/smallrye-reactive-messaging/3/concepts/advanced-config.adoc new file mode 100644 index 000000000..89aa2cd08 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/concepts/advanced-config.adoc @@ -0,0 +1,43 @@ += Advanced configuration +:version-root: ../ +include::{version-root}_attributes.adoc[] + +== Strict Binding Mode + +By default, SmallRye Reactive Messaging does not enforce whether all +_mediators_ are connected. It just prints a warning message. The strict +mode fails the deployment if some "incomings" are not bound to +"outgoings". To enable this mode, you can pass the +`-Dsmallrye-messaging-strict-binding=true` via the command line, or you +can set the `smallrye-messaging-strict-binding` attribute to `true` in +the configuration: + +[source,text] +---- +smallrye-messaging-strict-binding=true +---- + +== Disabling channels + +You can disable a channel in the configuration by setting the `enabled` +attribute to `false`: + +[source,text] +---- +mp.messaging.outgoing.dummy-sink.connector=dummy +mp.messaging.outgoing.dummy-sink.enabled=false # Disable this channel +---- + +SmallRye Reactive Messaging does not register disabled channels, so make +sure the rest of the application does not rely on them. + +== Publisher metrics + +SmallRye Reactive Messaging integrates MicroProfile Metrics and +Micrometer for registering counter metrics (named +`mp.messaging.message.count`) of published messages per channel. + +Both MicroProfile and Micrometer publisher metrics are enabled by +default if found on the classpath. They can be disabled with +`smallrye.messaging.metrics.mp.enabled` and +`smallrye.messaging.metrics.micrometer.enabled` properties respectively. diff --git a/content/docs/smallrye-reactive-messaging/3/concepts/blocking.adoc b/content/docs/smallrye-reactive-messaging/3/concepts/blocking.adoc new file mode 100644 index 000000000..2641f8bdf --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/concepts/blocking.adoc @@ -0,0 +1,88 @@ += @Blocking +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The `io.smallrye.reactive.messaging.annotations.Blocking` annotation can +be used on a method annotated with `@Incoming`, or `@Outgoing` to +indicate that the method should be executed on a worker pool: + +[source,java] +---- +@Outgoing("Y") +@Incoming("X") +@Blocking +public String process(String s) { + return s.toUpperCase(); +} +---- + +If method execution does not need to be ordered, it can be indicated on +the `@Blocking` annotation: + +[source,java] +---- +@Outgoing("Y") +@Incoming("X") +@Blocking(ordered = false) +public String process(String s) { + return s.toUpperCase(); +} +---- + +When unordered, the invocation can happen concurrently. + +By default, use of `@Blocking` results in the method being executed in +the Vert.x worker pool. If it’s desired to execute methods on a custom +worker pool, with specific concurrency needs, it can be defined on +`@Blocking`: + +[source,java] +---- +@Outgoing("Y") +@Incoming("X") +@Blocking("my-custom-pool") +public String process(String s) { + return s.toUpperCase(); +} +---- + +Specifying the concurrency for the above worker pool requires the +following configuration property to be defined: + +.... +smallrye.messaging.worker.my-custom-pool.max-concurrency=3 +.... + +== Supported signatures + +`@Blocking` does not support every signature. The following table lists +the supported ones. + +[width="100%",cols="7%,35%,58%",options="header",] +|=== +|Shape |Signature |Comment +|Publisher |`@Outgoing("in") @Blocking O generator()` |Invokes the generator from a worker thread. If `ordered` is set to `false`, the generator can be called concurrently. +|Publisher |`@Outgoing("in") @Blocking Message++<++O++>++ generator()` |Invokes the generator from a worker thread. If `ordered` is set to `false`, the generator can be called concurrently. +|Processor |`@Incoming("in") @Outgoing("bar") @Blocking O process(I in)` |Invokes the method on a worker thread. If `ordered` is set to `false`, the method can be called concurrently. +|Processor |`@Incoming("in") @Outgoing("bar") @Blocking Message++<++O++>++ process(I in)` |Invokes the method on a worker thread. If `ordered` is set to `false`, the method can be called concurrently. +|Subscriber |`@Incoming("in") @Blocking void consume(I in)` |Invokes the method on a worker thread. If `ordered` is set to `false`, the method can be called concurrently. +|Subscriber |`@Incoming("in") @Blocking Uni++<++Void++>++ consume(I in)` |Invokes the method on a worker thread. If `ordered` is set to `false`, the method can be called concurrently. +|Subscriber |`@Incoming("in") @Blocking CompletionStage++<++Void++>++ consume(I in)` |Invokes the method on a worker thread. If `ordered` is set to `false`, the method can be called concurrently. +|=== + +When a method can be called concurrently, the max concurrency depends on +the number of threads from the worker thread pool. + +== Using io.smallrye.common.annotation.Blocking + +`io.smallrye.common.annotation.Blocking` is another annotation with the +same semantic. `io.smallrye.common.annotation.Blocking` is used by +multiple SmallRye projects and Quarkus. + +SmallRye Reactive Messaging also supports +`io.smallrye.common.annotation.Blocking`. However, +`io.smallrye.common.annotation.Blocking` does not allow configuring the +ordering (it defaults to `ordered=true`). + +When both annotations are used, +`io.smallrye.reactive.messaging.annotations.Blocking` is preferred. diff --git a/content/docs/smallrye-reactive-messaging/3/concepts/broadcast.adoc b/content/docs/smallrye-reactive-messaging/3/concepts/broadcast.adoc new file mode 100644 index 000000000..a10dc2c86 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/concepts/broadcast.adoc @@ -0,0 +1,49 @@ += Broadcast +:version-root: ../ +include::{version-root}_attributes.adoc[] + +[WARNING] +.Experimental +==== + +`@Broadcast` is an experimental feature. + +==== + +By default, messages transiting in a channel are only dispatched to a +single consumer. Having multiple consumers is considered as an error, +and is reported at deployment time. + +The link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/io/smallrye/reactive/messaging/annotations/Broadcast.html[`Broadcast`] +annotation changes this behavior and indicates that messages transiting +in the channel are dispatched to all the consumers. `@Broadcast` must be +used with the `@Outgoing` annotation: + +[source,java] +---- +include::{examples-dir}/broadcast/BroadcastExamples.java[tags=chain,indent=0] +---- + +In the previous example, both consumers get the messages. + +You can also control the number of consumers to wait before starting to +dispatch the messages. This allows waiting for the complete graph to be +woven: + +[source,java] +---- +include::{examples-dir}/broadcast/BroadcastWithCountExamples.java[tags=chain,indent=0] +---- + +[NOTE] +==== + +Inbound connectors also support a `broadcast` attribute that allows +broadcasting the messages to multiple downstream subscribers. + +==== + +Use with Emitter + +For details on how to use `@Broadcast` with `Emitter` see the +xref:concepts/emitter.adoc#emitter-and-broadcast[documentation]. diff --git a/content/docs/smallrye-reactive-messaging/3/concepts/concepts.adoc b/content/docs/smallrye-reactive-messaging/3/concepts/concepts.adoc new file mode 100644 index 000000000..e721ce3d0 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/concepts/concepts.adoc @@ -0,0 +1,97 @@ += Introduction +:version-root: ../ +include::{version-root}_attributes.adoc[] + +When dealing with event-driven or data streaming applications, there are +a few concepts and vocabulary to introduce. + +== Messages, Payload, Metadata + +A `Message` is an _envelope_ around a `payload`. Your application is +going to receive, process, and send `Messages`. + +Your application’s logic can generate these `Messages` or receive them +from a message broker. They can also be consumed by your application or +sent to a message broker. + +.An application can receive a message, process it and send a resulting message +image::images/messages.png[An application can receive a message, process it and send a resulting message] + +In Reactive Messaging, `Message` are represented by the link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/io/eclipse/microprofile/reactive/messaging/Message.html[`Message`] interface. +Each `Message++<++T++>++` contains a _payload_ of type `++<++T++>++`. This payload can be retrieved using `message.getPayload()`: + +[source,java] +---- +include::{examples-dir}/messages/MessageExamples.java[tags=message,indent=0] +---- + +As you can see in the previous snippet, `Messages` can also have +_metadata_. _Metadata_ is a way to extend messages with additional data. +It can be metadata related to the message broker +(like link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/io/smallrye/reactive/messaging/kafka/api/KafkaMessageMetadata.html[`KafkaMessageMetadata`]), or contain operational data (such as tracing metadata), or business-related data. + +[NOTE] +==== + +When retrieving metadata, you get an `Optional` as it may not be present. + +==== + +[TIP] +==== + +Metadata is also used to influence the outbound dispatching (how the message will be sent to the broker). + +==== + +== Channels and Streams + +Inside your application, `Messages` transit on _channel_. A _channel_ is +a virtual destination identified by a name. + +.The application is a set of channels +image::images/channels.png[The application is a set of channels] + +SmallRye Reactive Messaging connects the component to the channel they read and to the channel they populate. +The resulting structure is a stream: Messages flow between components through channels. + +[NOTE] +.What about Reactive Streams? +==== + +You may wonder why _Reactive Messaging_ has _Reactive_ in the name. The +Messaging part is kind of obvious. The Reactive part comes from the +streams that are created by binding components. These streams are +https://www.reactive-streams.org/[Reactive Streams]. They follow the +subscription and request protocol and implement back-pressure. It also +means that xref:#connectors[Connectors] are intended to use non-blocking IO +to interact with the various message brokers. + +==== + +== Connectors + +Your application is interacting with messaging brokers or event backbone using _connectors_. +A _connector_ is a piece of code that connects to a broker and: + +[arabic] +. subscribe/poll/receive messages from the broker and propagate them to the application +. send/write/dispatch messages provided by the application to the broker + +Connectors are configured to map incoming messages to a specific +_channel_ (consumed by the application) and collect outgoing messages +sent to a specific channel. These collected messages are sent to the +external broker. + +.Connectors manages the communication between the application and the brokers +image::images/connectors.png[Connectors manages the communication between the application and the brokers] + +Each connector is dedicated to a specific technology. +For example, a Kafka Connector only deals with Kafka. + +You don’t necessarily need a connector. +When your application does not use connectors, everything happens _in-memory_, and the streams are created by chaining methods altogether. +Each chain is still a reactive stream and enforces the back-pressure protocol. +When you don’t use connectors, you need to make sure the chain is complete, meaning it starts with a message source, and it ends with a sink. +In other words, you need to generate messages from within the application (using a method with only `@Outgoing`, or an `Emitter`) and consume the messages +from within the application (using a method with only `@Incoming` or using an unmanaged stream). diff --git a/content/docs/smallrye-reactive-messaging/3/concepts/connectors.adoc b/content/docs/smallrye-reactive-messaging/3/concepts/connectors.adoc new file mode 100644 index 000000000..cad48ea52 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/concepts/connectors.adoc @@ -0,0 +1,182 @@ += Connectors +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Reactive Messaging can handle messages generated from within the +application but also interact with remote _brokers_. Reactive Messaging +Connectors interacts with these remote _brokers_ to retrieve messages +and send messages using various protocols and technology. + +Each connector handles to a specific technology. For example, a Kafka +Connector is responsible for interacting with Kafka, while an MQTT +Connector is responsible for MQTT interactions. + +== Connector name + +Each connector has a name. This name is referenced by the application to +indicate that this connector manages a specific channel. + +For example, the SmallRye Kafka Connector is named: `smallrye-kafka`. + +== Inbound and Outbound connectors + +Connector can: + +[arabic] +. retrieve messages from a remote broker (inbound) +. send messages to a remote broker (outbound) + +A connector can, of course, implement both directions. + +Inbound connectors are responsible for: + +[arabic] +. Getting messages from the remote broker, +. Creating a Reactive Messaging `Message` associated with the +retrieved message. +. Potentially associating technical metadata with the message. It +includes unmarshalling the payload. +. Associating an acknowledgment callback to acknowledge the incoming +message when the Reactive Messaging message is +processed/acknowledged. + +[IMPORTANT] +.Reactive matters +==== + +The first step should follow the reactive streams principle: uses +non-blocking technology, respects downstream requests. + +==== + +Outbound connectors are responsible for: + +[arabic] +. Receiving Reactive Messaging `Message` and transform it into a +structure understood by the remote broker. It includes marshaling +the payload. +. If the `Message` contains outbound metadata (metadata set during the +processing to influence the outbound structure and routing), taking +them into account. +. Sending the message to the remote broker. +. Acknowledging the Reactive Messaging `Message` when the broker has +accepted/acknowledged the message. + +== Configuring connectors + +Applications need to configure the connector used by expressing which +channel is managed by which connector. Non-mapped channels are local / +in-memory. + +To configure connectors, you need to have an implementation of +MicroProfile Config. If you don’t have one, add an implementation of +MicroProfile Config in your _classpath_, such as: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.config + smallrye-config + {smallrye-config-version} + +---- + +Then edit the application configuration, generally +`src/main/resources/META-INF/microprofile-config.properties`. + +The application configures the connector with a set of properties +structured as follows: + +.... +mp.messaging.[incoming|outgoing].[channel-name].[attribute]=[value] +.... + +For example: + +.... +mp.messaging.incoming.dummy-incoming-channel.connector=dummy +mp.messaging.incoming.dummy-incoming-channel.attribute=value +mp.messaging.outgoing.dummy-outgoing-channel.connector=dummy +mp.messaging.outgoing.dummy-outgoing-channel.attribute=value +.... + +You configure each channel (both incoming and outgoing) individually. + +The `++[++incoming++|++outgoing++]++` segment indicates the direction. + +* an `incoming` channel consumes data from a message broker or +something producing data. It’s an inbound interaction. It relates to +methods annotated with an `@Incoming` using the same channel name. +* an `outgoing` consumes data from the application and forwards it to +a message broker or something consuming data. It’s an outbound +interaction. It relates to methods annotated with an `@Outgoing` +using the same channel name. + +The `++[++channel-name++]++` is the name of the channel. If the channel name +contains a `.` (dot), you would need to use `"` (double-quote) around +it. For example, to configure the `dummy.incoming.channel` channel, you +would need: + +.... +mp.messaging.incoming."dummy.incoming.channel".connector=dummy +mp.messaging.incoming."dummy.incoming.channel".attribute=value +.... + +The `++[++attribute++]++=++[++value++]++` sets a specific connector attribute to the +given value. Attributes depend on the used connector. So, refer to the +connector documentation to check the supported attributes. + +The `connector` attribute must be set for each mapped channel and +indicates the name of the connector responsible for the channel. + +Here is an example of a channel using an MQTT connector, consuming data +from a MQTT broker, and a channel using a Kafka connector (writing data +to Kafka): + +[source,properties] +---- +# Configure the incoming health channel +mp.messaging.incoming.health.topic=neo +mp.messaging.incoming.health.connector=smallrye-mqtt +mp.messaging.incoming.health.host=localhost +mp.messaging.incoming.health.broadcast=true + +# Configure outgoing data channel +mp.messaging.outgoing.data.connector=smallrye-kafka +mp.messaging.outgoing.data.bootstrap.servers=localhost:9092 +mp.messaging.outgoing.data.key.serializer=org.apache.kafka.common.serialization.StringSerializer +mp.messaging.outgoing.data.value.serializer=io.vertx.kafka.client.serialization.JsonObjectSerializer +mp.messaging.outgoing.data.acks=1 +---- + +[IMPORTANT] +==== + +To use a connector, you need to add it to your _CLASSPATH_. Generally, +adding the dependency to your project is enough. Then, you need to know +the connector’s name and set the `connector` attribute for each channel +managed by this connector. + +==== + +Connector attribute table + +In the connector documentation, you will find a table listing the +attribute supported by the connector. Be aware that attributes for +inbound and outbound interactions may be different. + +These tables contain the following entries: + +[arabic] +. The name of the attribute, and potentially an _alias_. The name of +the attribute is used with the +`mp.messaging.++[++incoming++|++outgoing++]++.++[++channel-name++]++.++[++attribute++]++=++[++value++]++` +syntax (the `attribute` segment). The _alias_ (if set) is the name +of a global MicroProfile Config property that avoids having to +configure the attribute for each managed channel. For example, to +set the location of your Kafka broker globally, you can use the +`kafka.bootstrap.servers` alias. +. The description of the attribute, including the type. +. Whether that attribute is mandatory. If so, it fails the +deployment if not set +. The default value, if any. diff --git a/content/docs/smallrye-reactive-messaging/3/concepts/converters.adoc b/content/docs/smallrye-reactive-messaging/3/concepts/converters.adoc new file mode 100644 index 000000000..ff3f6148e --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/concepts/converters.adoc @@ -0,0 +1,43 @@ += Message Converters +:version-root: ../ +include::{version-root}_attributes.adoc[] + +SmallRye Reactive Messaging supports _message converters_, allowing to +transform an incoming message into a version accepted by the method. If +the incoming messages or payload does not match the invoked method’s +expectation, SmallRye Reactive Messaging looks for a suitable converter. +If found, it converts the incoming message with this converter. + +Converters can have multiple purposes, but the main use case is about +transforming the message’s payload: + +[source,java] +---- +include::{examples-dir}/converters/MyConverter.java[tags=code,indent=0] +---- + +To provide a converter, implement a bean exposing the `MessageConverter` +interface. The `canConvert` method is called during the lookup and +verifies if it can handle the conversion. The `target` type is the +expected payload type. If the converter returns `true` to `canConvert`, +SmallRye Reactive Messaging calls the `convert` method to proceed to the +conversion. + +The previous converter can be used in application like the following, to +convert `Message++<++String++>++` to `Message++<++Person++>++`: + +[source,java] +---- +include::{examples-dir}/converters/ConverterExample.java[tags=code,indent=0] +---- + +Converters work for all supported method signatures. However, the +signature must be well-formed to allow the extraction of the expected +payload type. Wildcards and raw types do not support conversion. If the +expected payload type cannot be extracted, or no converter fits, the +message is passed as received. + +If multiple suitable converters are present, implementations should +override the `getPriority` method returning the priority. The default +priority is `100`. The converter lookup invokes converters with higher +priority (from the least value to the greatest) first. diff --git a/content/docs/smallrye-reactive-messaging/3/concepts/decorators.adoc b/content/docs/smallrye-reactive-messaging/3/concepts/decorators.adoc new file mode 100644 index 000000000..56722ae5a --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/concepts/decorators.adoc @@ -0,0 +1,75 @@ += Channel Decorators +:version-root: ../ +include::{version-root}_attributes.adoc[] + +SmallRye Reactive Messaging supports decorating reactive streams +of incoming and outgoing channels for implementing cross-cutting +concerns such as monitoring, tracing or message interception. + +Two symmetrical APIs are proposed for decorating publisher and subscriber channels, +link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/io/smallrye/reactive/messaging/PublisherDecorator.html[`PublisherDecorator`] +and link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/io/smallrye/reactive/messaging/SubscriberDecorator.html[`SubscriberDecorator`] respectively. + +[IMPORTANT] +==== + +`@Incoming` channels and channels bound to an outbound connector are both `Subscriber`s. +Conversely `@Outgoing` channels and channels bound to an inbound connector are `Publisher`s. + +==== + +For example, to provide a decorator which counts consumed messages from incoming connector, +implement a bean exposing the interface `PublisherDecorator`: + +[source,java] +---- +include::{examples-dir}/decorators/ConsumedMessageDecorator.java[tags=code,indent=0] +---- + +Decorators' `decorate` method is called only once per channel at application deployment when graph wiring is taking place. +Decorators are very powerful because they receive the stream of messages (Mutiny `Multi++<++Message++<++?++>>++`) +and potentially return a new stream of messages. + +Note that if a decorator is available it will be called on every channel. +The `decorate` method receives the channel name and whether the channel is a connector or not as parameters. +Decorators are called ordered from highest to lowest priority (from the least value to the greatest), +obtained using the `javax.enterprise.inject.spi.Prioritized#getPriority` method. + +[NOTE] +==== + +The `SubscriberDecorator` receive a list of channel names because `@Incoming` annotation is repeatable +and consuming methods can be linked to multiple channels. + +==== + +== Intercepting Outgoing Messages + +Decorators can be used to intercept and alter messages, both on incoming and outgoing channels. +Smallrye Reactive Messaging provides a `SubscriberDecorator` by default to allow intercepting outgoing messages for a specific channel. + +To provide an outgoing interceptor implement a bean exposing the interface link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/io/smallrye/reactive/messaging/OutgoingInterceptor.html[`OutgoingInterceptor`], qualified with a `@Identifier` with the channel name to intercept. +Only one interceptor is allowed to be bound for interception per outgoing channel. +If no interceptors are found with a `@Identifier` but a `@Default` one is available, it is used. +When multiple interceptors are available, the bean with the highest priority is used. + +[source,java] +---- +include::{examples-dir}/interceptors/MyInterceptor.java[indent=0] +---- + +An `OutgoingInterceptor` can implement these three methods: + +* `Message++<++?++>++ onMessage(Message++<++?++>++ message)` : Called before passing the message to the outgoing connector for transmission. +The message can be altered by returning a new message from this method. +* `void onMessageAck(Message++<++?++>++ message)` : Called after message acknowledgment. +This callback can access `OutgoingMessageMetadata` which will hold the result of the message transmission to the broker, if supported by the connector. This is only supported by MQTT and Kafka connectors. +* `void onMessageNack(Message++<++?++>++ message, Throwable failure)` : Called before message negative-acknowledgment. + +[NOTE] +==== + +If you are willing to adapt an incoming message payload to fit a consuming method receiving type, +you can use `MessageConverter`s. + +==== diff --git a/content/docs/smallrye-reactive-messaging/3/concepts/emitter.adoc b/content/docs/smallrye-reactive-messaging/3/concepts/emitter.adoc new file mode 100644 index 000000000..a59761323 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/concepts/emitter.adoc @@ -0,0 +1,270 @@ += Emitter and Channels +:version-root: ../ +include::{version-root}_attributes.adoc[] + +It is not rare to combine in a single application imperative parts +(Jax-RS, regular CDI _beans_) and reactive parts (_beans_ with +`@Incoming` and `@Outgoing` annotations). In these case, it’s often +required to send _messages_ from the imperative part to the reactive +part. In other words, send messages to channels handled by reactive +messaging and how can you retrieve messages. + +== Emitter and @Channel + +To send _things_ (payload or `Message`) from imperative code to a +specific channel you need to use: + +[arabic] +. the link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/org/eclipse/microprofile/reactive/messaging/Channel.html[`org.eclipse.microprofile.reactive.messaging.Channel`] annotations +. the link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/org/eclipse/microprofile/reactive/messaging/Emitter.html[`org.eclipse.microprofile.reactive.messaging.Emitter`] type + +The `@Channel` lets you indicate to which channel you are going to send +your payloads or messages. The `Emitter` is the object to use to send +these payloads or messages. + +[source,java] +---- +include::{examples-dir}/emitter/MyImperativeBean.java[tags=intro,indent=0] +---- + +The `Emitter` class takes a type parameter. It’s the type of payload. +Even if you want to send `Messages`, the type is the payload type. + +[IMPORTANT] +==== + +You must have a `@Incoming("prices")` somewhere in your application +(meaning a method consuming messages transiting on the channel +`prices`), or an outbound connector configured to manage the `prices` +channel (`mp.messaging.outgoing.prices...`) + +==== + +== Sending payloads + +Sending payloads is done as follows: + +[source,java] +---- +include::{examples-dir}/emitter/EmitterExamples.java[tags=payload,indent=0] +---- + +When sending a payload, the emitter returns a `CompletionStage`. This +`CompletionStage` gets completed once the message created from the +payload is acknowledged: + +[source,java] +---- +include::{examples-dir}/emitter/EmitterExamples.java[tags=cs,indent=0] +---- + +If the processing fails, the `CompletionStage` gets completed +exceptionally (with the reason of the nack). + +== Sending messages + +You can also send `Messages`: + +[source,java] +---- +include::{examples-dir}/emitter/EmitterExamples.java[tags=message,indent=0] +---- + +When sending a `Message`, the emitter does not return a +`CompletionStage`, but you can pass the ack/nack callback, and be called +when the message is acked/nacked. + +[source,java] +---- +include::{examples-dir}/emitter/EmitterExamples.java[tags=message-ack,indent=0] +---- + +Sending messages also let you pass metadata. + +[source,java] +---- +include::{examples-dir}/emitter/EmitterExamples.java[tags=message-meta,indent=0] +---- + +Metadata can be used to propagate some context objects with the message. + +== Overflow management + +When sending messages from imperative code to reactive code, you must be +aware of back-pressure. Indeed, messages sent using the emitter and +stored in a _queue_. If the consumer does not process the messages +quickly enough, this queue can become a memory hog and you may even run +out of memory. + +To control what need to happen when the queue becomes out of control, +use the link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/org/eclipse/microprofile/reactive/messaging/OnOverflow.html[`OnOverflow`] annotation. `@OnOverflow` lets you configure: + +* the maximum size of the queue (default is 256) +* what needs to happen when this size is reached (fail, drop...) + +[source,java] +---- +include::{examples-dir}/emitter/EmitterExamples.java[tags=overflow,indent=0] +---- + +The supported strategies are: + +* `OnOverflow.Strategy.BUFFER` - use a buffer to store the elements +until they are consumed. If the buffer is full, a failure is +propagated (and the thread using the emitted gets an exception) +* `OnOverflow.Strategy.UNBOUNDED++_++BUFFER` - use an unbounded buffer to +store the elements +* `OnOverflow.Strategy.DROP` - drops the most recent value if the +downstream can’t keep up. It means that new value emitted by the +emitter are ignored. +* `OnOverflow.Strategy.FAIL` - propagates a failure in case the +downstream can’t keep up. +* `OnOverflow.Strategy.LATEST` - keeps only the latest value, dropping +any previous value if the downstream can’t keep up. +* `OnOverflow.Strategy.NONE` - ignore the back-pressure signals +letting the downstream consumer to implement a strategy. + +=== Defensive emission + +Having an emitter injected into your code does not guarantee that +someone is ready to consume the message. For example, a subscriber may +be connecting to a remote broker. If there are no subscribers, using the +`send` method will throw an exception. + +The `emitter.hasRequests()` method indicates that a subscriber +subscribes to the channel and requested items. So, you can wrap your +emission with: + +[source,java] +---- +if (emitter.hasRequests()) { + emitter.send("hello"); +} +---- + +If you use the `OnOverflow.Strategy.DROP`, you can use the `send` method +even with no subscribers nor demands. The message will be nacked +immediately. + +== Retrieving channels + +You can use the `@Channel` annotation to inject in your bean the +underlying stream. Note that in this case, you will be responsible for +the subscription: + +[source,java] +---- +include::{examples-dir}/emitter/ChannelExamples.java[tags=channel,indent=0] +---- + +[IMPORTANT] +==== + +You must have a `@Outgoing("my-channel")` somewhere in your application +(meaning a method generating messages transiting on the channel +`my-channel`), or an inbound connector configured to manage the `prices` +channel (`mp.messaging.incoming.prices...`) + +==== + +Injected _channels_ merge all the matching _outgoing_ - so if you have +multiple `@Outgoing("out")`, `@Inject @Channel("out")` gets all the +messages. + +If your injected _channel_ receives _payloads_ (`Multi++<++T++>++`), it +acknowledges the message automatically, and support multiple +subscribers. If you injected _channel_ receives `Message` +(`Multi++<++Message++<++T++>>++`), you will be responsible for the acknowledgement +and broadcasting. + +== Emitter and @Broadcast + +When using an `Emitter`, you can now `@Broadcast` what is emitted to all +subscribers. + +Here is an example of emitting a price with two methods marked +`@Incoming` to receive the broadcast: + +[source,java] +---- +include::{examples-dir}/emitter/EmitterExamples.java[tags=broadcast,indent=0] +---- + +For more details see xref:concepts/broadcast.adoc[@Broadcast] +documentation. + +== Mutiny Emitter + +If you prefer to utilize `Uni` in all your code, there is now a +`MutinyEmitter` that will return `Uni++<++Void++>++` instead of `void`. + +[source,java] +---- +include::{examples-dir}/emitter/MutinyExamples.java[tags=uni,indent=0] +---- + +There’s also the ability to block on sending the event to the emitter. +It will only return from the method when the event is acknowledged, or +nacked, by the receiver: + +[source,java] +---- +include::{examples-dir}/emitter/MutinyExamples.java[tags=uni-await,indent=0] +---- + +And if you don’t need to worry about the success or failure of sending +an event, you can `sendAndForget`: + +[source,java] +---- +include::{examples-dir}/emitter/MutinyExamples.java[tags=uni-forget,indent=0] +---- + +== Custom Emitter Implementations + +[WARNING] +.Experimental +==== + +Custom emitter implementations is an experimental feature. + +==== + +`Emitter` and `MutinyEmitter` are two implementations of the emitter concept, +where imperative code in your application can send messages to Reactive Messaging channels. + +With `EmitterFactory` it is possible to provide custom implementations, and application facing emitter interfaces. + +In the following example, the injectable custom emitter interface is `CustomEmitter`, +and it is implemented by `CustomEmitterImpl`: + +[source,java] +---- +include::{examples-dir}/emitter/EmitterFactoryExamples.java[tags=custom-emitter-declaration,indent=0] +---- + +Note that `CustomEmitter` interface extends `EmitterType`, which is a marker interface for discovering custom emitter types. +Also, `CustomEmitterImpl` implements the `MessagePublisherProvider`, which is used by the framework to transform this emitter to a channel. + +Then we need to provide an implementation of the `EmitterFactory` interface: + +[source,java] +---- +include::{examples-dir}/emitter/EmitterFactoryExamples.java[tags=custom-emitter-factory,indent=0] +---- + +The `CustomEmitterFactory` is a CDI managed bean, which implements the `EmitterFactory`. +It is qualified with `EmitterFactoryFor` annotation which is configured with the emitter interface `CustomEmitter` that this factory provides. + +Smallrye Reactive Messaging discovers the emitter factory during the CDI deployment validation +and verifies that custom emitters used by the application have corresponding emitter factories. +It'll use the emitter factory to create the emitter implementation and will register the implementation into the `ChannelRegistry`. + +Note that the `CustomEmitterFactory` also uses the `ChannelRegistry` and provides the custom emitter with `@Produces`. + +Finally, the application can inject and use the `CustomEmitter` as a normal emitter channel: + +[source,java] +---- +include::{examples-dir}/emitter/EmitterFactoryExamples.java[tags=custom-emitter-usage,indent=0] +---- diff --git a/content/docs/smallrye-reactive-messaging/3/concepts/incomings.adoc b/content/docs/smallrye-reactive-messaging/3/concepts/incomings.adoc new file mode 100644 index 000000000..8fc179d3a --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/concepts/incomings.adoc @@ -0,0 +1,20 @@ += Multiple Incoming Channels +:version-root: ../ +include::{version-root}_attributes.adoc[] + +[WARNING] +.Experimental +==== + +Multiple `@Incomings` is an experimental feature. + +==== + +The `@Incoming` annotation is repeatable. It means that the method +receives the messages transiting on every listed channels, in no +specific order: + +[source,java] +---- +include::{examples-dir}/incomings/IncomingsExamples.java[tags=code,indent=0] +---- diff --git a/content/docs/smallrye-reactive-messaging/3/concepts/logging.adoc b/content/docs/smallrye-reactive-messaging/3/concepts/logging.adoc new file mode 100644 index 000000000..2a8fc8df3 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/concepts/logging.adoc @@ -0,0 +1,269 @@ += Logging +:version-root: ../ +include::{version-root}_attributes.adoc[] + +SmallRye Reactive Messaging uses https://github.com/jboss-logging[JBoss +Logging] as logging API. This section +explains how to configure the loggers for various logging backends. + +[TIP] +==== + +If you are developing SmallRye Reactive Messaging and wonder about how +the logs are managed, it uses https://jboss-logging.github.io/jboss-logging-tools/#introduction[JBoss Logging +Tools]. + +==== + +== Logging Backends + +SmallRye Reactive Messaging uses the JBoss Logging library to write +messages to a log file. This library is a logging _bridge_ that +integrates different log frameworks. You can decide which of the +following frameworks you want to use for your application: + +* JBoss LogManager (`jboss`) +* Log4j 2 (`log4j2`) +* Log4j 1 (`log4j`) +* Slf4j (`slf4j`) +* JDK logging (`jul`) + +You only need to add the chosen framework to the classpath, and the +JBoss Logging library will pick it up. If there are multiple frameworks +available on the classpath, it picks the first found (in the order from +the list). Alternatively, you can set the `org.jboss.logging.provider` +system property is one of the values given above. + +The concepts and log categories are the same for all frameworks. +However, the format of the configuration file and the names of the log +levels differ. Check the documentation of your logging library to find +out which _dependencies_ are required, the exact name of the log levels, +and where the configuration should be written. + +== Log Categories + +As all applications and frameworks, SmallRye Reactive Messaging writes +log messages in different categories and log levels. The categories +group messages from specific connectors, classes or components. The +following table shows the essential log categories used by SmallRye +Reactive Messaging: + +[width="100%",cols="36%,64%",options="header",] +|=== +|Category |Description +|`io.smallrye.reactive.messaging` |This category contains all the messages written by SmallRye Reactive Messaging. +|`io.smallrye.reactive.messaging.provider` |This category contains all the messages generated by the _core_ (provider). +|`io.smallrye.reactive.messaging.kafka` |This category contains all the messages generated by the Kafka Connector. +|`io.smallrye.reactive.messaging.amqp` |This category contains all the messages generated by the AMQP Connector. +|`io.smallrye.reactive.messaging.jms` |This category contains all the messages generated by the JMS Connector. +|`io.smallrye.reactive.messaging.camel` |This category contains all the messages generated by the Camel Connector. +|`io.smallrye.reactive.messaging.mqtt` |This category contains all the messages generated by the MQTT (Client) Connector. +|`io.smallrye.reactive.messaging.mqtt-server` |This category contains all the messages generated by the MQTT (Server) Connector. +|=== + +The names of the log levels are defined by your logging framework and +determine the amount and granularity of the log messages. You can assign +a log level to each category. If you do not specify a specific +category’s log level, it will inherit the level from its parent +category. Thus, setting the log level of +`io.smallrye.reactive.messaging` influences every loggers from SmallRye +Reactive Messaging. + +== Message Code + +Each message has an identifier code. They are all prefixed with `SRMSG`, +followed with the numeric code. + +In the following output, the code is `SRMSG00229`: + +.... +[2020-06-15 13:35:07] [INFO ] SRMSG00229: Channel manager initializing... +.... + +== Recommended logging configurations + +=== Development + +==== Log4J 1 + +_log4j.properties_ + +[source,properties] +---- +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.Target=System.out +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d{HH:mm:ss,SSS} %-5p [%c] - %m%n + +log4j.rootLogger=info, stdout +log4j.logger.io.smallrye.reactive.messaging=info +log4j.logger.org.jboss.weld=warn +---- + +==== Log4J 2 + +_log4j2.xml_ + +[source,xml] +---- + + + PropertiesConfiguration + + + + + + [%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %c{1} - %msg%n + > + + + + + + + + + + + + + + + +---- + +==== JDK (JUL) + +_logging.properties_ + +[source,properties] +---- +handlers=java.util.logging.ConsoleHandler + +java.util.logging.ConsoleHandler.level=FINEST +java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter +java.util.logging.SimpleFormatter.format=[%1$tF %1$tT] [%4$-7s] %5$s %n + +.level=INFO +io.smallrye.reactive.messaging.level=INFO +org.jboss.weld.level=WARNING +---- + +==== LogBack via SLF4J++*++ + +_logback.xml_ + +[source,xml] +---- + + + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + +---- + +=== Production + +==== Log4J 1 + +_log4j.properties_ + +[source,properties] +---- +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.Target=System.out +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d{HH:mm:ss,SSS} %-5p [%c] - %m%n + +log4j.rootLogger=info, stdout +log4j.logger.io.smallrye.reactive.messaging=warn +log4j.logger.org.jboss.weld=error +---- + +==== Log4J 2 + +_log4j2.xml_ + +[source,xml] +---- + + + PropertiesConfiguration + + + + + + [%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %c{1} - %msg%n + > + + + + + + + + + + + + + + + +---- + +==== JDK (JUL) + +_logging.properties_ + +[source,properties] +---- +handlers=java.util.logging.ConsoleHandler + +java.util.logging.ConsoleHandler.level=INFO +java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter +java.util.logging.SimpleFormatter.format=[%1$tF %1$tT] [%4$-7s] %5$s %n + +.level=INFO +io.smallrye.reactive.messaging.level=WARNING +org.jboss.weld.level=SEVERE +---- + +_logback.xml_ + +[source,xml] +---- + + + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + +---- diff --git a/content/docs/smallrye-reactive-messaging/3/concepts/merge.adoc b/content/docs/smallrye-reactive-messaging/3/concepts/merge.adoc new file mode 100644 index 000000000..d7bd6cbd7 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/concepts/merge.adoc @@ -0,0 +1,47 @@ += Merge channels +:version-root: ../ +include::{version-root}_attributes.adoc[] + +[WARNING] +.Experimental +==== + +`@Merge` is an experimental feature. + +==== + +By default, messages transiting in a channel can arise from a single +producer. Having multiple producers is considered erroneous and is +reported at deployment time. + +The link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/io/smallrye/reactive/messaging/annotations/Merge.html[`Merge`] +annotation changes this behavior and indicates that a channel can have +multiple producers. `@Merge` must be used with the `@Incoming` +annotation: + +[source,java] +---- +include::{examples-dir}/merge/MergeExamples.java[tags=chain,indent=0] +---- + +In the previous example, the consumer gets all the messages (from both +producers). + +The `@Merge` annotation allows configuring how the incoming messages +(from the different producers) are merged into the channel. The `mode` +attribute allows configuring this behavior: + +* `ONE` picks a single producer, discarding the other producer; +* `MERGE` (default) gets all the messages as they come, without any +defined order. Messages from different producers may be interleaved. +* `CONCAT` concatenates the producers. The messages from one producer +are received until the messages from other producers are received. + +[NOTE] +==== + +Outbound connectors also support a `merge` attribute that allows +consuming the messages to multiple upstreams. It will dispatch all the +received messages. + +==== diff --git a/content/docs/smallrye-reactive-messaging/3/concepts/message-context.adoc b/content/docs/smallrye-reactive-messaging/3/concepts/message-context.adoc new file mode 100644 index 000000000..f3162a13a --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/concepts/message-context.adoc @@ -0,0 +1,83 @@ += Message Contexts +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Message context provides a way to propagate data along the processing of a message. +It can be used to propagate message specific objects in an implicit manner and be able to retrieve them later, such as the user, session or transaction. + +[IMPORTANT] +==== + +Message contexts are only support by Kafka, AMQP, RabbitMQ and MQTT connectors. + +==== + +[NOTE] +==== + +Message context support is an experimental and SmallRye only feature. + +==== + +== What's a message context + +A message context is execution context on which a message is processed. +Each stage of the processing is going to use the same execution context. +Thus, it allows storing _data_ which can later be restored. +For example, you can imagine storing some authentication (`User` in the following example) data in one part of your processing and restore it in a later stage. + +[source,java] +---- +@Incoming("data") +@Outgoing("process") +public Message process(Message input) { + // Extract some data from the message and store it in the context + User user = ...; + // Store the extracted data into the message context. + ContextLocals.put("user", user); + record input; +} + +@Incoming("process") +@Outgoing("after-process") +public String handle(String payload) { + // You can retrieve the store data using + User user = ContextLocals.get("user", null); + + // ... + return payload; +} +---- + +The Message context is also available when using blocking or asynchronous stages (stage returning `Uni` or `CompletionStage`) + +== The difference with metadata + +Message metadata can be used to provide a similar feature. +However, it requires using `Messages` which can be inconvenient (need to handle the acknowledgement manually). +Message Contexts provide a simpler API, closer to a _Message CDI scope_: you can save data, and restore it later. +The implicit propagation avoid having to deal with `Messages`. + +== Supported signatures + +Message context works with: + +* methods consuming or producing `Messages`, `Uni++<++Message++<++T++>>++` and `CompletionStage++<++Message++<++T++>>++` +* methods consuming or producing payloads, `Uni++<++Payload++>++` and `CompletionStage++<++Payload++>++`. +* blocking and non-blocking methods + +However, message context are *NOT* enforced when using methods consuming or producing: + +* `Multi`, `Publisher` and `PublisherBuilder` +* `Subscriber` and `SubscriberBuilder` +* `Processor` and `ProcessorBuilder` + +== Under the hood + +Under the hood, the message context feature uses Vert.x _duplicated contexts_. +A duplicated context is a view of the "root" (event loop) context, which is restored at each stage of the message processing. + +Each time that a compatible connector receives a message from a broker, it creates a new _duplicated context_ and attaches it to the message. +So the context is stored in the metadata of the message. + +When the message is processed, SmallRye Reactive Messaging makes sure that this processing is executed on the stored duplicated context. diff --git a/content/docs/smallrye-reactive-messaging/3/concepts/model.adoc b/content/docs/smallrye-reactive-messaging/3/concepts/model.adoc new file mode 100644 index 000000000..6dab2a5ef --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/concepts/model.adoc @@ -0,0 +1,390 @@ += Development Model +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Reactive Messaging proposes a CDI-based programming model to implement event-driven applications. +Following the CDI principles, _beans_ are forming the main building block of your application. +Reactive Messaging provides a set of annotations and types to implement beans that generate, consume or process messages. + +== @Incoming and @Outgoing + +Reactive Messaging provides two main annotations: + +* link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/org/eclipse/microprofile/reactive/messaging/Incoming.html[`org.eclipse.microprofile.reactive.messaging.Incoming`] - indicates the consumed channel +* link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/org/eclipse/microprofile/reactive/messaging/Outgoing.html[`org.eclipse.microprofile.reactive.messaging.Outgoing`] - indicates the populated channel + +These annotations are used on _methods_: + +[source,java] +---- +include::{examples-dir}/beans/MessageProcessingBean.java[indent=0] +---- + +[NOTE] +==== + +Reactive Messaging beans can either be in the _application_ scope (`@ApplicationScoped`) or dependent scope (`@Dependent`). + +==== + +Manipulating messages can be cumbersome. +When you are only interested in the payload, you can use the following syntax: The following code is equivalent to the snippet from above: + +[source,java] +---- +include::{examples-dir}/beans/PayloadProcessingBean.java[indent=0] +---- + +[IMPORTANT] +==== + +You should not call methods annotated with `@Incoming` and/or +`@Outgoing` directly from your code. They are invoked by the framework. +Having user code invoking them would not have the expected outcome. + +==== + +SmallRye Reactive Messaging automatically binds matching `@Outgoing` to +`@Incoming` to form a chain: + +.A chain of components +image::images/chain.png[A chain of components] + +If we consider the following code: + +[source,java] +---- +include::{examples-dir}/beans/Chain.java[tags=chain,indent=0] +---- + +It would generate the following chain: + +[source,text] +---- +generate --> [ source ] --> process --> [ sink ] --> consume +---- + +Methods annotated with `@Incoming` or `@Outgoing` don’t have to be in the same _bean_ (_class_). +You can distribute them among a set of beans. +Remote interactions are also possible when using connectors. + +Methods annotated with: + +* only `@Outgoing` are used to generate messages or payloads +* only `@Incoming` are used to consume messages or payloads +* both `@Incoming` and `@Outgoing` are used to process messages or payloads; or transform the stream + +== Creating messages + +Messages are envelopes around payload. +They are the vehicle. +While manipulating payload is convenient, messages let you add metadata, handle acknowledgement... + +Creating `Messages` is done using the link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/io/eclipse/microprofile/reactive/messaging/Message.html[`Message`] interface directly: + +[source,java] +---- +include::{examples-dir}/messages/MessageExamples.java[tags=creation,indent=0] +---- + +Messages accept `null` as payload. +Channels connected to outbound connectors interpret messages with `null` payload differently depending on the technology. + +[source,java] +---- +include::{examples-dir}/messages/MessageExamples.java[tags=null-payload,indent=0] +---- + +You can also create new instance of `Message` from an existing one: + +[source,java] +---- +include::{examples-dir}/messages/MessageExamples.java[tags=copy,indent=0] +---- + +[NOTE] +.Acknowledgement? +==== + +Acknowledgement is an important part of messaging systems. This will be +covered in the xref:concepts/acknowledgement.adoc[acknowledgement] +section. + +==== + +[NOTE] +.Connector Metadata +==== + +Most connectors are providing metadata to let you extract technical +details about the message, but also customize the outbound dispatching. + +==== + +== Generating Messages + +To produce messages to a channel, you need to use the `@Outgoing` +annotation. This annotation takes a single parameter: the name of the +populated channel. + +=== Generating messages synchronously + +You can generate messages synchronously. In this case, the method is +called for every _request_ from the downstream: + +[source,java] +---- +include::{examples-dir}/generation/GenerationExamples.java[tags=message-sync,indent=0] +---- + +[NOTE] +.Requests? +==== + +Reactive Messaging connects components to build a reactive stream. +In a reactive stream, the emissions are controlled by the consumer +(downstream) indicating to the publisher (upstream) how many items it +can consume. With this protocol, the consumers are never flooded. + +==== + +=== Generating messages using CompletionStage + +You can also return a `CompletionStage` / `CompletableFuture`. +In this case, Reactive Messaging waits until the `CompletionStage` gets completed before calling it again. + +For instance, this signature is useful to poll messages from a source using an asynchronous client: + +[source,java] +---- +include::{examples-dir}/generation/GenerationExamples.java[tags=message-cs,indent=0] +---- + +=== Generating messages using Uni + +You can also return a https://smallrye.io/smallrye-mutiny/#_uni_and_multi[Uni] instance. +In this case, Reactive Messaging waits until the `Uni` emits its item before calling it again. + +This signature is useful when integrating asynchronous clients providing a Mutiny API. + +[source,java] +---- +include::{examples-dir}/generation/GenerationExamples.java[tags=message-uni,indent=0] +---- + +=== Generating Reactive Streams of messages + +Instead of producing the message one by one, you can return the stream +directly. If you have a data source producing Reactive Streams +`Publisher` (or sub-types, such as `Multi`), this is the signature you are looking for: + +[source,java] +---- +include::{examples-dir}/generation/GenerationExamples.java[tags=message-stream,indent=0] +---- + +In this case, the method is called once to retrieve the `Publisher`. + +== Generating Payloads + +Instead of `Message`, you can produce _payloads_. In this case, Reactive +Messaging produces a simple message from the _payload_ using +`Message.of`. + +=== Generating payload synchronously + +You can produce _payloads_ synchronously. The framework calls the method +upon request and create `Messages` around the produced payloads. + +[source,java] +---- +include::{examples-dir}/generation/GenerationExamples.java[tags=payload-sync,indent=0] +---- + +=== Generating payload using CompletionStage + +You can also return `CompletionStage` or `CompletableFuture`. For +example, if you have an asynchronous client returning `CompletionStage`, +you can use it as follows, to poll the data one by one: + +[source,java] +---- +include::{examples-dir}/generation/GenerationExamples.java[tags=payload-cs,indent=0] +---- + +=== Generating payload by producing Unis + +You can also return a `Uni` if you have a client using Mutiny types: + +[source,java] +---- +include::{examples-dir}/generation/GenerationExamples.java[tags=payload-uni,indent=0] +---- + +=== Generating Reactive Streams of payloads + +Finally, you can return a `Publisher` (or a sub-type such as a `Multi`): + +[source,java] +---- +include::{examples-dir}/generation/GenerationExamples.java[tags=payload-stream,indent=0] +---- + +In this case, Reactive Messaging calls the method only once to retrieve +the `Publisher`. + +== Consuming Messages + +To consume messages from a channel, you need to use the `@Incoming` +annotation. This annotation takes a single parameter: the name of the +consumed channel. + +Because `Messages` must be acknowledged, consuming messages requires +returning _asynchronous results_ that would complete when the incoming +message get acknowledged. + +For example, you can receive the `Message`, process it and return the +acknowledgement as result: + +[source,java] +---- +include::{examples-dir}/consumption/ConsumptionExamples.java[tags=message-cs,indent=0]' +---- + +You can also return a `Uni` if you need to implement more complicated +processing: + +[source,java] +---- +include::{examples-dir}/consumption/ConsumptionExamples.java[tags=message-uni,indent=0] +---- + +== Consuming Payloads + +Unlike consuming messages, consuming payloads support both synchronous +and asynchronous consumption. + +For example, you can consume a payload as follows: + +[source,java] +---- +include::{examples-dir}/consumption/ConsumptionExamples.java[tags=payload-sync,indent=0] +---- + +In this case, you don’t need to deal with the acknowledgement yourself. +The framework acknowledges the incoming message (that wrapped the +payload) once your method returns successfully. + +If you need to achieve asynchronous actions, you can return a +`CompletionStage` or a `Uni`: + +[source,java] +---- +include::{examples-dir}/consumption/ConsumptionExamples.java[tags=payload-cs,indent=0] +---- + +[source,java] +---- +include::{examples-dir}/consumption/ConsumptionExamples.java[tags=payload-uni,indent=0] +---- + +In these 2 cases, the framework acknowledges the incoming message when +the returned construct gets _completed_. + +== Processing Messages + +You can process `Message` both synchronously or asynchronously. +This later case is useful when you need to execute an asynchronous action during your processing such as invoking a remote service. + +Do process `Messages` synchronously uses: + +[source,java] +---- +include::{examples-dir}/processing/ProcessingExamples.java[tags=process-message,indent=0] +---- + +This method transforms the `int` payload to a `String`, and wraps it +into a `Message`. + +'''important "Using `Message.withX` methods" +You may be surprised by the usage of `Message.withX` methods. It allows +metadata propagation as the metadata would be copied from the incoming +message and so dispatched to the next method. + +You can also process `Messages` asynchronously: + +[source,java] +---- +include::{examples-dir}/processing/ProcessingExamples.java[tags=process-message-cs,indent=0] +---- + +Or using Mutiny: + +[source,java] +---- +include::{examples-dir}/processing/ProcessingExamples.java[tags=process-message-uni,indent=0] +---- + +In general, you want to create the new `Message` from the incoming one. +It enables metadata propagation and post-acknowledgement. For this, use +the `withX` method from the `Message` class returning a new `Message` +instance but copy the _content_ (metadata, ack/nack...). + +== Processing payloads + +If you don’t need to manipulate the envelope, you can process payload +directly either synchronously or asynchronously: + +[source,java] +---- +include::{examples-dir}/processing/ProcessingExamples.java[tags=process-payload,indent=0] +---- + +[source,java] +---- +include::{examples-dir}/processing/ProcessingExamples.java[tags=process-payload-cs,indent=0] +---- + +[source,java] +---- +include::{examples-dir}/processing/ProcessingExamples.java[tags=process-payload-uni,indent=0] +---- + +[NOTE] +.What about metadata? +==== + +With these methods, the metadata are automatically propagated. + +==== + +== Processing streams + +The previous processing method were taking single `Message` or payload. +Sometimes you need more advanced manipulation. For this, SmallRye +Reactive Messaging lets you process the stream of `Message` or the +stream of payloads directly: + +[source,java] +---- +include::{examples-dir}/processing/StreamExamples.java[tags=processing-stream-message,indent=0] +---- + +[source,java] +---- +include::{examples-dir}/processing/StreamExamples.java[tags=processing-stream-payload,indent=0] +---- + +You can receive either a (Reactive Streams) `Publisher`, a +`PublisherBuilder` or (Mutiny) `Multi`. You can return any subclass of +`Publisher` or a `Publisher` directly. + +[IMPORTANT] +==== + +These signatures do not support metadata propagation. In the case of a +stream of `Message`, you need to propagate the metadata manually. In the +case of a stream of payload, propagation is not supported, and incoming +metadata are lost. + +==== diff --git a/content/docs/smallrye-reactive-messaging/3/concepts/signatures.adoc b/content/docs/smallrye-reactive-messaging/3/concepts/signatures.adoc new file mode 100644 index 000000000..866a9f6bb --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/concepts/signatures.adoc @@ -0,0 +1,81 @@ += Supported signatures +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The following tables list the supported method signatures and indicate +the various supported features. For instance, they indicate the default +and available acknowledgement strategies (when applicable). + +[width="100%",cols="32%,68%",options="header",] +|=== +|Signature |Invocation time +|`@Outgoing Publisher++<++Message++<++O++>>++ method()` `{vbar} Called once at ++*++assembly++*++ time {vbar} {vbar}`@Outgoing Publisher method()`` |Called once at _assembly_ time +|`@Outgoing Multi++<++Message++<++O++>>++ method()` `{vbar} Called once at ++*++assembly++*++ time {vbar} {vbar}`@Outgoing Multi method()`` |Called once at _assembly_ time +|`@Outgoing PublisherBuilder++<++Message++<++O++>>++ method()` `{vbar} Called once at ++*++assembly++*++ time {vbar} {vbar}`@Outgoing PublisherBuilder method()`` |Called once at _assembly_ time +|`@Outgoing Message++<++O++>++ method()` `{vbar} Called for every downstream request, sequentially {vbar} {vbar}`@Outgoing O method()`` |Called for every downstream request, sequentially +|`@Outgoing CompletionStage++<++Message++<++O++>>++ method()` `{vbar} Called for every downstream request, sequentially (After the completion of the last returned CompletionStage) {vbar} {vbar}`@Outgoing CompletionStage method()`` |Called for every downstream request, , sequentially (After the completion of the last returned CompletionStage) +|`@Outgoing Uni++<++Message++<++O++>>++ method()` `{vbar} Called for every downstream request, sequentially (After the completion of the last returned Uni) {vbar} {vbar}`@Outgoing Uni method()`` |Called for every downstream request, , sequentially (After the completion of the last returned Uni) +|=== + +Method signatures to generate data + +[width="100%",cols="37%,32%,31%",options="header",] +|=== +|Signature |Invocation time |Supported Acknowledgement Strategies +|`@Incoming void method(I p)` |Called for every incoming payload (sequentially) |_POST++_++PROCESSING_, NONE, PRE++_++PROCESSING +|`@Incoming CompletionStage++<++?++>++ method(Message++<++I++>++ msg)` |Called for every incoming message (sequentially) |_MANUAL_, NONE, PRE++_++PROCESSING +|`@Incoming CompletionStage++<++?++>++ method(I p)` |Called for every incoming payload (sequentially) |_POST++_++PROCESSING_, PRE++_++PROCESSING, NONE +|`@Incoming Uni++<++?++>++ method(Message++<++I++>++ msg)` |Called for every incoming message (sequentially) |_MANUAL_, NONE, PRE++_++PROCESSING +|`@Incoming Uni++<++?++>++ method(I p)` |Called for every incoming payload (sequentially) |_POST++_++PROCESSING_, PRE++_++PROCESSING, NONE +|`@Incoming Subscriber++<++Message++<++I++>>++ method()` |Called once at _assembly_ time |_MANUAL_, POST++_++PROCESSING, NONE, PRE++_++PROCESSING +|`@Incoming Subscriber++<++I++>++ method()` |Called once at _assembly_ time |_POST++_++PROCESSING_, NONE, PRE++_++PROCESSING +|=== + +Method signatures to consume data + +[width="100%",cols="42%,26%,21%,11%",options="header",] +|=== +|Signature |Invocation time |Supported Acknowledgement Strategies |Metadata Propagation +|`@Outgoing @Incoming Message++<++O++>++ method(Message++<++I++>++ msg)` |Called for every incoming message (sequentially) |_MANUAL_, NONE, PRE++_++PROCESSING |manual +|`@Outgoing @Incoming O method(I payload)` |Called for every incoming payload (sequentially) |_POST++_++PROCESSING_, NONE, PRE++_++PROCESSING |automatic +|`@Outgoing @Incoming CompletionStage++<++Message++<++O++>>++ method(Message++<++I++>++ msg)` |Called for every incoming message (sequentially) |_MANUAL_, NONE, PRE++_++PROCESSING |manual +|`@Outgoing @Incoming CompletionStage++<++O++>++ method(I payload)` |Called for every incoming payload (sequentially) |_POST++_++PROCESSING_, NONE, PRE++_++PROCESSING |automatic +|`@Outgoing @Incoming Uni++<++Message++<++O++>>++ method(Message++<++I++>++ msg)` |Called for every incoming message (sequentially) |_MANUAL_, NONE, PRE++_++PROCESSING |manual +|`@Outgoing @Incoming Uni++<++O++>++ method(I payload)` |Called for every incoming payload (sequentially) |_POST++_++PROCESSING_, NONE, PRE++_++PROCESSING |automatic +|`@Outgoing @Incoming Processor++<++Message++<++I++>++, Message++<++O++>>++ method()` |Called once at _assembly_ time |_MANUAL_, PRE++_++PROCESSING, NONE |manual +|`@Outgoing @Incoming Processor++<++I, O++>++ method()` |Called once at _assembly_ time |_PRE++_++PROCESSING_, NONE |not supported +|`@Outgoing @Incoming ProcessorBuilder++<++Message++<++I++>++, Message++<++O++>>++ method()` |Called once at _assembly_ time |_MANUAL_, PRE++_++PROCESSING, NONE |manual +|`@Outgoing @Incoming ProcessorBuilder++<++I, O++>++ method()` |Called once at _assembly_ time |_PRE++_++PROCESSING_, NONE |not supported +|`@Outgoing @Incoming Publisher++<++Message++<++O++>>++ method(Message++<++I++>++ msg)` |Called once at _assembly_ time |_MANUAL_, PRE++_++PROCESSING, NONE |manual +|`@Outgoing @Incoming Publisher++<++O++>++ method(I payload)` |Called once at _assembly_ time |_PRE++_++PROCESSING_, NONE |automatic +|`@Outgoing @Incoming Multi++<++Message++<++O++>>++ method(Message++<++I++>++ msg)` |Called once at _assembly_ time |_MANUAL_, PRE++_++PROCESSING, NONE |manual +|`@Outgoing @Incoming Multi++<++O++>++ method(I payload)` |Called once at _assembly_ time |_PRE++_++PROCESSING_, NONE |automatic +|`@Outgoing @Incoming PublisherBuilder++<++Message++<++O++>>++ method(Message++<++I++>++ msg)` |Called once at _assembly_ time |_MANUAL_, PRE++_++PROCESSING, NONE |manual +|`@Outgoing @Incoming PublisherBuilder++<++O++>++ method(I payload)` |Called once at _assembly_ time |_PRE++_++PROCESSING_, NONE |automatic +|=== + +Method signatures to process data + +[width="100%",cols="53%,16%,20%,11%",options="header",] +|=== +|Signature |Invocation time |Supported Acknowledgement Strategies |Metadata Propagation +|`@Outgoing @Incoming Publisher++<++Message++<++O++>>++ method(Publisher++<++Message++<++I++>>++ pub)` |Called once at _assembly_ time |_MANUAL_, NONE, PRE++_++PROCESSING |manual +|`@Outgoing @Incoming Publisher++<++O++>++ method(Publisher++<++I++>++ pub)` |Called once at _assembly_ time |_PRE++_++PROCESSING_, NONE |not supported +|`@Outgoing @Incoming Multi++<++Message++<++O++>>++ method(Multi++<++Message++<++I++>>++ pub)` |Called once at _assembly_ time |_MANUAL_, NONE, PRE++_++PROCESSING |manual +|`@Outgoing @Incoming Multi++<++O++>++ method(Multi++<++I++>++ pub)` |Called once at _assembly_ time |_PRE++_++PROCESSING_, NONE |not supported +|`@Outgoing @Incoming PublisherBuilder++<++Message++<++O++>>++ method(PublisherBuilder++<++Message++<++I++>>++ pub)` |Called once at _assembly_ time |_MANUAL_, NONE, PRE++_++PROCESSING |manual +|`@Outgoing @Incoming PublisherBuilder++<++O++>++ method(PublisherBuilder++<++I++>++ pub)` |Called once at _assembly_ time |NONE, PRE++_++PROCESSING |not supported +|=== + +Method signatures to manipulate streams + +[IMPORTANT] +==== + +When processing `Message`, it is often required to _chain_ the incoming +`Message` to enable post-processing acknowledgement and metadata +propagation. Use the `with` (like `withPayload`) methods from the +incoming message, so it copies the metadata and ack/nack methods. It +returns a new `Message` with the right content. + +==== diff --git a/content/docs/smallrye-reactive-messaging/3/concepts/skipping.adoc b/content/docs/smallrye-reactive-messaging/3/concepts/skipping.adoc new file mode 100644 index 000000000..2717afe78 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/concepts/skipping.adoc @@ -0,0 +1,29 @@ += Skipping messages +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Sometimes you receive a message and don’t want to produce an output +message. To handle this, you have several choices: + +[arabic] +. for method processing single message or payload, producing `null` +would produce an ignored message (not forwarded) +. for method processing streams, you can generate an _empty_ stream. + +== Skipping a single item + +To skip a single message or payload, return `null`: + +[source,java] +---- +include::{examples-dir}/skip/SingleSkip.java[tags=skip,indent=0] +---- + +== Skipping in a stream + +To skip a message or payload when manipulating a stream, emit an _empty_ `Multi` (or `Publisher`): + +[source,java] +---- +include::{examples-dir}/skip/StreamSkip.java[tags=skip,indent=0] +---- diff --git a/content/docs/smallrye-reactive-messaging/3/concepts/testing.adoc b/content/docs/smallrye-reactive-messaging/3/concepts/testing.adoc new file mode 100644 index 000000000..6ed9eac9c --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/concepts/testing.adoc @@ -0,0 +1,66 @@ += Testing your application +:version-root: ../ +include::{version-root}_attributes.adoc[] + +It’s not rare to have to test your application but deploying the +infrastructure can be cumbersome. While Docker or Test Containers have +improved the testing experience, you may want to _mock_ this +infrastructure. + +SmallRye Reactive Messaging proposes an _in-memory_ connector for this +exact purpose. It allows switching the connector used for a channel with +an _in-memory_ connector. This in-memory connector provides a way to +send messages to incoming channels, or check the received messages for +outgoing channels. + +To use the _in-memory_ connector, you need to add the following +dependency to your project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.reactive + smallrye-reactive-messaging-in-memory + {project-version} + test + +---- + +Then, in a test, you can do something like: + +[source,java] +---- +include::{examples-dir}/testing/MyTest.java[indent=0] +---- + +When switching a channel to the in-memory connector, all the +configuration properties are ignored. + +[IMPORTANT] +==== + +This connector has been designed for testing purpose only. + +==== + +The _switch_ methods return `Map++<++String, String++>++` instances containing +the set properties. While these system properties are already set, you +can retrieve them and pass them around, for example if you need to start +an external process with these properties: + +[source,java] +---- +include::{examples-dir}/testing/MyTestSetup.java[tags=code,indent=0] +---- + +[NOTE] +==== + +The in-memory connector support the `broadcast` and `merge` attributes. +So, if your connector is configured with `broadcast: true`, the +connector broadcasts the messages to all the channel consumers. If your +connector is configured with `merge:true`, the connector receives all +the messages sent to the mapped channel even when coming from multiple +producers. + +==== diff --git a/content/docs/smallrye-reactive-messaging/3/getting-started.adoc b/content/docs/smallrye-reactive-messaging/3/getting-started.adoc new file mode 100644 index 000000000..9cc19d837 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/getting-started.adoc @@ -0,0 +1,63 @@ += Getting Started +include::_attributes.adoc[] + +The easiest way to start using SmallRye Reactive Messaging is from https://quarkus.io[Quarkus]. +SmallRye Reactive Messaging can also be used standalone, or with https://openliberty.io/guides/microprofile-reactive-messaging.html[Open Liberty]. + +First, go to https://code.quarkus.io/?g=io.smallrye&a=getting-started-with-reactive-messaging&e=smallrye-reactive-messaging[code.quarkus.io]. +Select the `smallrye-reactive-messaging` extension (already done if you use the link), and then click on the generate button to download the code. + +One downloaded, unzip the project and import it in your IDE. + +If you look at the `pom.xml` file, you will see the following dependency: + +[source,xml] +---- + + io.quarkus + quarkus-smallrye-reactive-messaging + +---- + +It provides the support for SmallRye Reactive Messaging. + +Ok, so far so good, but we need event-driven _beans_. + +Create the `quickstart` package, and copy the following class into it: + +For instance: + +[source,java] +---- +include::{examples-dir}/quickstart/ReactiveMessagingExample.java[indent=0] +---- + +This class contains a set of methods: + +* producing messages (`source`) +* processing messages (`toUpperCase`) +* transforming the stream by skipping messages (`filter`) +* consuming messages (`sink`) + +Each of these methods are connected through _channels_. + +Now, let's see this in action. +For the terminal, run: + +[source,shell] +---- +> ./mvnw quarkus:dev +---- + +Running the previous example should give the following output: + +[source,text] +---- +>> HELLO +>> SMALLRYE +>> REACTIVE +>> MESSAGE +---- + +Of course, this is a very simple example. +To go further, let's have a look to the core xref:concepts/concepts.adoc[concepts] behind SmallRye Reactive Messaging. diff --git a/content/docs/smallrye-reactive-messaging/3/images/chain.png b/content/docs/smallrye-reactive-messaging/3/images/chain.png new file mode 100644 index 000000000..eacfc241c Binary files /dev/null and b/content/docs/smallrye-reactive-messaging/3/images/chain.png differ diff --git a/content/docs/smallrye-reactive-messaging/3/images/channels.png b/content/docs/smallrye-reactive-messaging/3/images/channels.png new file mode 100644 index 000000000..8b2052ce4 Binary files /dev/null and b/content/docs/smallrye-reactive-messaging/3/images/channels.png differ diff --git a/content/docs/smallrye-reactive-messaging/3/images/connectors.png b/content/docs/smallrye-reactive-messaging/3/images/connectors.png new file mode 100644 index 000000000..ebd50c302 Binary files /dev/null and b/content/docs/smallrye-reactive-messaging/3/images/connectors.png differ diff --git a/content/docs/smallrye-reactive-messaging/3/images/favicon.png b/content/docs/smallrye-reactive-messaging/3/images/favicon.png new file mode 100644 index 000000000..93f705b9c Binary files /dev/null and b/content/docs/smallrye-reactive-messaging/3/images/favicon.png differ diff --git a/content/docs/smallrye-reactive-messaging/3/images/incoming.png b/content/docs/smallrye-reactive-messaging/3/images/incoming.png new file mode 100644 index 000000000..69e57a8b3 Binary files /dev/null and b/content/docs/smallrye-reactive-messaging/3/images/incoming.png differ diff --git a/content/docs/smallrye-reactive-messaging/3/images/logo.png b/content/docs/smallrye-reactive-messaging/3/images/logo.png new file mode 100644 index 000000000..2d957bd52 Binary files /dev/null and b/content/docs/smallrye-reactive-messaging/3/images/logo.png differ diff --git a/content/docs/smallrye-reactive-messaging/3/images/message.png b/content/docs/smallrye-reactive-messaging/3/images/message.png new file mode 100644 index 000000000..fb38e78b0 Binary files /dev/null and b/content/docs/smallrye-reactive-messaging/3/images/message.png differ diff --git a/content/docs/smallrye-reactive-messaging/3/images/messages.png b/content/docs/smallrye-reactive-messaging/3/images/messages.png new file mode 100644 index 000000000..c0b37e6a2 Binary files /dev/null and b/content/docs/smallrye-reactive-messaging/3/images/messages.png differ diff --git a/content/docs/smallrye-reactive-messaging/3/images/outgoing.png b/content/docs/smallrye-reactive-messaging/3/images/outgoing.png new file mode 100644 index 000000000..f5aaef29d Binary files /dev/null and b/content/docs/smallrye-reactive-messaging/3/images/outgoing.png differ diff --git a/content/docs/smallrye-reactive-messaging/3/images/overview.png b/content/docs/smallrye-reactive-messaging/3/images/overview.png new file mode 100644 index 000000000..42e00e441 Binary files /dev/null and b/content/docs/smallrye-reactive-messaging/3/images/overview.png differ diff --git a/content/docs/smallrye-reactive-messaging/3/images/processor.png b/content/docs/smallrye-reactive-messaging/3/images/processor.png new file mode 100644 index 000000000..faac01281 Binary files /dev/null and b/content/docs/smallrye-reactive-messaging/3/images/processor.png differ diff --git a/content/docs/smallrye-reactive-messaging/3/images/redhat_reversed.svg b/content/docs/smallrye-reactive-messaging/3/images/redhat_reversed.svg new file mode 100644 index 000000000..37fe83c48 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/images/redhat_reversed.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/content/docs/smallrye-reactive-messaging/3/index.adoc b/content/docs/smallrye-reactive-messaging/3/index.adoc new file mode 100644 index 000000000..153fa5e3d --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/index.adoc @@ -0,0 +1,16 @@ += SmallRye Reactive Messaging +include::_attributes.adoc[] + +*SmallRye Reactive Messaging is a framework for building event-driven, data streaming, and event-sourcing applications.* + +It lets your application interact using various messaging technologies such as Apache Kafka, AMQP or MQTT. +The framework provides a flexible programming model bridging https://www.cdi-spec.org/[CDI] and event-driven. + +SmallRye Reactive Messaging is an implementation of the Eclipse MicroProfile Reactive Messaging specification {spec-version}. + +.Event-Driven Architectures +image::images/overview.png[Event-Driven Architectures] + +SmallRye Reactive Messaging is used in http://quarkus.io[Quarkus], and https://openliberty.io/[Open Liberty]. + +Start with the xref:getting-started.adoc[Getting Started] guide to know how it can be used and introduce various features. diff --git a/content/docs/smallrye-reactive-messaging/3/jms/advanced-jms.adoc b/content/docs/smallrye-reactive-messaging/3/jms/advanced-jms.adoc new file mode 100644 index 000000000..ab0285e80 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/jms/advanced-jms.adoc @@ -0,0 +1,37 @@ += Advanced configuration +:version-root: ../ +include::{version-root}_attributes.adoc[] + +== Underlying thread pool + +Lots of JMS operations are blocking and so not cannot be done on the +_caller_ thread. For this reason, these blocking operations are executed +on a worker thread. + +You can configure the thread pool providing these worker threads using +the following MicroProfile Config properties: + +* `smallrye.jms.threads.max-pool-size` - the max number of threads +(Defaults to 10) +* `smallrye.jms.threads.ttl` - the ttl of the created threads +(Defaults to 60 seconds) + +== Selecting the ConnectionFactory + +The JMS Connector requires a `javax.jms.ConnectionFactory` to be exposed +as a CDI bean. The connector looks for a `javax.jms.ConnectionFactory` +and delegate the interaction with the JMS server to this factory. + +In case you have several connection factories, you can use the +`@Identifier` qualifier on your factory to specify the name. Then, in +the channel configuration, configure the name as follows: + +[source,properties] +---- +# Configure the connector globally +mp.messaging.connector.smallrye-jms.connection-factory-name=my-factory-name +# Configure a specific incoming channel +mp.messaging.incoming.my-channel.connection-factory-name=my-factory-name +# Configure a specific outgoing channel +mp.messaging.outgoing.my-channel.connection-factory-name=my-factory-name +---- diff --git a/content/docs/smallrye-reactive-messaging/3/jms/jms.adoc b/content/docs/smallrye-reactive-messaging/3/jms/jms.adoc new file mode 100644 index 000000000..15f7b917d --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/jms/jms.adoc @@ -0,0 +1,74 @@ += The JMS connector +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The JMS connector adds support for https://en.wikipedia.org/wiki/Jakarta_Messaging[Jakarta +Messaging] to Reactive +Messaging. It is designed to integrate with _JakartaEE_ applications +that are sending or receiving Jakarta Messaging Messages. + +Jakarta Messaging is a Java Message Oriented Middleware API for sending +messages between two or more clients. It is a programming model to +handle the producer-consumer messaging problem. It is a messaging +standard that allows application components based on Jakarta EE to +create, send, receive, and read messages. It allows the communication +between different components of a distributed application to be loosely +coupled, reliable, and asynchronous. + +Using the JMS connector + +To you the JMS Connector, add the following dependency to your project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.reactive + smallrye-reactive-messaging-jms + {project-version} + +---- + +The connector name is: `smallrye-jms`. + +So, to indicate that a channel is managed by this connector you need: + +[source,properties] +---- +# Inbound +mp.messaging.incoming.[channel-name].connector=smallrye-jms + +# Outbound +mp.messaging.outgoing.[channel-name].connector=smallrye-jms +---- + +The JMS Connector requires a `javax.jms.ConnectionFactory` to be exposed +(as CDI bean). The connector looks for a `javax.jms.ConnectionFactory` +and delegate the interaction with the JMS server to this factory. In +other words, it creates the JMS connection and context using this +factory. + +So, in order to use this connector you would need to expose a +`javax.jms.ConnectionFactory`: + +[source,java] +---- +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.Produces; +import javax.jms.ConnectionFactory; + +import org.apache.activemq.artemis.jms.client.ActiveMQJMSConnectionFactory; + +@ApplicationScoped +public class ConnectionFactoryBean { + + @Produces + ConnectionFactory factory() { + return new ActiveMQJMSConnectionFactory( + "tcp://localhost:61616", + null, null); + } + +} +---- + +The factory class may depend on your JMS connector/server. diff --git a/content/docs/smallrye-reactive-messaging/3/jms/receiving-jms-messages.adoc b/content/docs/smallrye-reactive-messaging/3/jms/receiving-jms-messages.adoc new file mode 100644 index 000000000..0685d33fe --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/jms/receiving-jms-messages.adoc @@ -0,0 +1,101 @@ += Receiving messages from JMS +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The JMS Connector retrieves JMS Message and maps each of them into +Reactive Messaging `Messages`. + +== Example + +Let’s imagine you have a `javax.jms.ConnectionFactory` bean exposed and +connected to your JMS server. Don’t forget that it’s required to use the +JMS connector. + +Configure your application to receive JMS messages on the `prices` +channel as follows: + +[source,properties] +---- +mp.messaging.incoming.prices.connector=smallrye-jms +---- + +[NOTE] +==== + +You don’t need to set the destination. By default, it uses the channel +name (`prices`). You can configure the `destination` attribute to +override it. + +==== + +[NOTE] +==== + +By default the connector uses a `queue`. You can configure it to use a +`topic` by setting `destination-type=topic`. + +==== + +Then, your application receives `Message++<++Double++>++`. You can consume the +payload directly: + +[source,java] +---- +include::{examples-dir}/jms/inbound/JmsPriceConsumer.java[indent=0] +---- + +Or, you can retrieve the `Message++<++Double++>++`: + +[source,java] +---- +include::{examples-dir}/jms/inbound/JmsPriceMessageConsumer.java[indent=0] +---- + +== Deserialization + +The content of the incoming JMS message is mapped to a Java object. + +By default it extracts the JMS Message _body_ as a `java.lang.Object`. +This can be changed by setting, in the incoming JMS Message: + +[arabic] +. The `++_++classname` property +. the `JMSType` + +The value must be a fully qualified class name. The connector then load +the associated class. + +[NOTE] +==== + +The connector loads the associated `Class` using the `TCCL` and if not +found, the classloader used to load the connector. + +==== + +If the target type is a primitive type ort `String`, the resulting +message contains the mapped payload. + +If the target type is a class, the object is built using included JSON +deserializer (JSON-B and Jackson provided OOB, for more details see +link:serde[Serde]), from the `JMSType`. If not, the default behavior is +used (Java deserialization). + +== Inbound Metadata + +Messages coming from JMS contains an instance of link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-jms/{javadoc-version}/io/smallrye/reactive/messaging/jms/IncomingJmsMessageMetadata.html[`io.smallrye.reactive.messaging.jms.IncomingJmsMessageMetadata`] in the metadata. + +[source,java] +---- +include::{examples-dir}/jms/inbound/JmsMetadataExample.java[tags=code,indent=0] +---- + +== Acknowledgement + +When the Reactive Messaging `Message` gets acknowledged, the associated +JMS Message is acknowledged. As JMS acknowledgement is blocking, this +acknowledgement is delegated to a worker thread. + +== Configuration Reference + +include::{generated-dir}/smallrye-jms-incoming.adoc[indent=0] diff --git a/content/docs/smallrye-reactive-messaging/3/jms/sending-jms-messages.adoc b/content/docs/smallrye-reactive-messaging/3/jms/sending-jms-messages.adoc new file mode 100644 index 000000000..e781729c9 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/jms/sending-jms-messages.adoc @@ -0,0 +1,106 @@ += Sending messages to JMS +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The JMS Connector can send Reactive Messaging `Messages` as JMS Message. + +== Example + +Let’s imagine you have a `javax.jms.ConnectionFactory` bean exposed and +connected to your JMS server. Don’t forget that it’s required to use the +JMS connector. + +Configure your application to write the messages from the `prices` +channel into a JMS Message as follows: + +[source,properties] +---- +mp.messaging.outgoing.prices.connector=smallrye-jms +---- + +[NOTE] +==== + +You don’t need to set the destination. By default, it uses the channel +name (`prices`). You can configure the `destination` attribute to +override it. + +==== + +[NOTE] +==== + +By default the connector uses a `queue`. You can configure it to use a +`topic` by setting `destination-type=topic`. + +==== + +Then, your application must send `Message++<++Double++>++` to the `prices` +channel. It can use `double` payloads as in the following snippet: + +[source,java] +---- +include::{examples-dir}/jms/outbound/JmsPriceProducer.java[indent=0] +---- + +Or, you can send `Message++<++Double++>++`: + +[source,java] +---- +include::{examples-dir}/jms/outbound/JmsPriceMessageProducer.java[indent=0] +---- + +== Serialization + +The connector serializes the incoming message payload into the body of +the outgoing JMS Message. + +If the payload is a `String` or a primitive type, the payload is encoded +as `String` and the `JMSType` is set to the target class. The +`++_++classname` property is also set. The JMS Message is a `TextMessage`. + +If the payload is a `byte++[]++`, it’s passed as `byte++[]++` in a JMS +`BytesMessage`. + +Otherwise, the payload is encoded using included JSON serializer (JSON-B +and Jackson provided OOB, for more details see link:serde[Serde]). The +`JMSType` is set to the target class. The `++_++classname` property is also +set. The JMS Message is a `TextMessage`. + +For example, the following code serialize the produced `Person` using +JSON-B. + +[source,java] +---- +@Incoming("...") +@Outgoing("my-channel") +public Person sendToJms(...) { + // ... + return new Person("bob", 42); +} +---- + +It requires that the `Person` class can be serialized to JSON. The +classname is passed in the `JMSType` property and `++_++classname` property. + +== Outbound Metadata + +When sending `Messages`, you can add an instance of link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-jms/{javadoc-version}/io/smallrye/reactive/messaging/jms/OutgoingJmsMessageMetadata.html[`OutgoingJmsMessageMetadata`] +to influence how the message is going to be written to JMS. + +[source,java] +---- +include::{examples-dir}/jms/outbound/JmsOutboundMetadataExample.java[tags=code,indent=0] +---- + +The metadata allow adding properties but also override the destination. + +== Acknowledgement + +Once the JMS message is sent to the JMS server, the message is +acknowledged. Sending a JMS message is a blocking operation. So, sending +is done on a worker thread. + +== Configuration Reference + +include::{generated-dir}/smallrye-jms-outgoing.adoc[indent=0] diff --git a/content/docs/smallrye-reactive-messaging/3/kafka/avro-configuration.adoc b/content/docs/smallrye-reactive-messaging/3/kafka/avro-configuration.adoc new file mode 100644 index 000000000..a714f9cb9 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/kafka/avro-configuration.adoc @@ -0,0 +1,152 @@ += Using Apache Avro serializer/deserializer +:version-root: ../ +include::{version-root}_attributes.adoc[] + +If you are using https://avro.apache.org/[Apache Avro] +serializer/deserializer, please note the following configuration +properties. + +== For https://docs.confluent.io/current/schema-registry/serdes-develop/serdes-avro.html[Confluent] Schema Registry + +Confluent Avro library is `io.confluent:kafka-avro-serializer`. Note +that this library is not available in Maven Central, you need to use the +https://docs.confluent.io/clients-kafka-java/current/overview.html[Confluent Maven +repository]. + +=== Consumer + +[width="100%",cols="25%,75%",options="header",] +|=== +|Property |Recommended value +|value.deserializer |io.confluent.kafka.serializers.KafkaAvroDeserializer +|schema.registry.url |http://%7Byour_host%7D:%7Byour_port%7D/ +|specific.avro.reader |true +|=== + +Example: + +.... +mp.messaging.incoming.[channel].value.deserializer=io.confluent.kafka.serializers.KafkaAvroDeserializer +mp.messaging.incoming.[channel].schema.registry.url=http://{your_host}:{your_port}/ +mp.messaging.incoming.[channel].specific.avro.reader=true +.... + +=== Producer + +[width="100%",cols="24%,76%",options="header",] +|=== +|Property |Recommended value +|value.serializer |io.confluent.kafka.serializers.KafkaAvroSerializer +|schema.registry.url |http://%7Byour_host%7D:%7Byour_port%7D/ +|=== + +Example: + +.... +mp.messaging.outgoing.[channel].value.serializer=io.confluent.kafka.serializers.KafkaAvroSerializer +mp.messaging.outgoing.[channel].schema.registry.url=http://{your_host}:{your_port}/ +.... + +== For https://www.apicur.io/registry/[Apicurio] Registry 1.x + +Apicurio Registry 1.x Avro library is +`io.apicurio:apicurio-registry-utils-serde`. + +The configuration properties listed here are meant to be used with the +Apicurio Registry 1.x client library and Apicurio Registry 1.x server. + +=== Consumer + +[width="100%",cols="38%,62%",options="header",] +|=== +|Property |Recommended value +|value.deserializer |io.apicurio.registry.utils.serde.AvroKafkaDeserializer +|apicurio.registry.url |http://%7Byour_host%7D:%7Byour_port%7D/api +|apicurio.registry.avro-datum-provider |io.apicurio.registry.utils.serde.avro.DefaultAvroDatumProvider +|apicurio.registry.use-specific-avro-reader |true +|=== + +Example: + +.... +mp.messaging.incoming.[channel].value.deserializer=io.apicurio.registry.utils.serde.AvroKafkaDeserializer +mp.messaging.incoming.[channel].apicurio.registry.url=http://{your_host}:{your_port}/api +mp.messaging.incoming.[channel].apicurio.registry.avro-datum-provider=io.apicurio.registry.utils.serde.avro.DefaultAvroDatumProvider +mp.messaging.incoming.[channel].apicurio.registry.use-specific-avro-reader=true +.... + +=== Producer + +[width="100%",cols="24%,76%",options="header",] +|=== +|Property |Recommended value +|value.serializer |io.apicurio.registry.utils.serde.AvroKafkaSerializer +|apicurio.registry.url |http://%7Byour_host%7D:%7Byour_port%7D/api +|=== + +To automatically register schemas with the registry, add: + +[width="100%",cols="31%,69%",options="header",] +|=== +|Property |Value +|apicurio.registry.global-id |io.apicurio.registry.utils.serde.strategy.GetOrCreateIdStrategy +|=== + +Example: + +.... +mp.messaging.outgoing.[channel].value.serializer=io.apicurio.registry.utils.serde.AvroKafkaSerializer +mp.messaging.outgoing.[channel].apicurio.registry.url=http://{your_host}:{your_port}/api +mp.messaging.outgoing.[channel].apicurio.registry.global-id=io.apicurio.registry.utils.serde.strategy.GetOrCreateIdStrategy +.... + +== For https://www.apicur.io/registry/[Apicurio] Registry 2.x + +Apicurio Registry 2.x Avro library is +`io.apicurio:apicurio-registry-serdes-avro-serde`. + +The configuration properties listed here are meant to be used with the +Apicurio Registry 2.x client library and Apicurio Registry 2.x server. + +=== Consumer + +[width="100%",cols="31%,69%",options="header",] +|=== +|Property |Recommended value +|value.deserializer |io.apicurio.registry.serde.avro.AvroKafkaDeserializer +|apicurio.registry.url |http://%7Byour_host%7D:%7Byour_port%7D/apis/registry/v2 +|apicurio.registry.use-specific-avro-reader |true +|=== + +Example: + +.... +mp.messaging.incoming.[channel].value.deserializer=io.apicurio.registry.serde.avro.AvroKafkaDeserializer +mp.messaging.incoming.[channel].apicurio.registry.url=http://{your_host}:{your_port}/apis/registry/v2 +mp.messaging.incoming.[channel].apicurio.registry.use-specific-avro-reader=true +.... + +=== Producer + +[width="100%",cols="19%,81%",options="header",] +|=== +|Property |Recommended value +|value.serializer |io.apicurio.registry.serde.avro.AvroKafkaSerializer +|apicurio.registry.url |http://%7Byour_host%7D:%7Byour_port%7D/apis/registry/v2 +|=== + +To automatically register schemas with the registry, add: + +[cols=",",options="header",] +|=== +|Property |Value +|apicurio.registry.auto-register |true +|=== + +Example: + +.... +mp.messaging.outgoing.[channel].value.serializer=io.apicurio.registry.serde.avro.AvroKafkaSerializer +mp.messaging.outgoing.[channel].apicurio.registry.url=http://{your_host}:{your_port}/apis/registry/v2 +mp.messaging.outgoing.[channel].apicurio.registry.auto-register=true +.... diff --git a/content/docs/smallrye-reactive-messaging/3/kafka/client-service.adoc b/content/docs/smallrye-reactive-messaging/3/kafka/client-service.adoc new file mode 100644 index 000000000..2487db3de --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/kafka/client-service.adoc @@ -0,0 +1,39 @@ += KafkaClientService +:version-root: ../ +include::{version-root}_attributes.adoc[] + +For advanced use cases, SmallRye Reactive Messaging provides a bean of +type `KafkaClientService` that you can inject: + +[source,java] +---- +@Inject +KafkaClientService kafka; +---- + +From there, you can obtain an +`io.smallrye.reactive.messaging.kafka.KafkaProducer` and an +`io.smallrye.reactive.messaging.kafka.KafkaConsumer`. + +`KafkaProducer` and `KafkaConsumer` expose a non-blocking API on top of +the Kafka client API. They also mediate access to the threads that +SmallRye Reactive Messaging uses to run all Kafka operations: the +_polling thread_, used for consuming records from Kafka topics, and the +_sending thread_, used for producing records to Kafka topics. (Just to +be clear: each _channel_ has its own polling thread and sending thread.) + +The reason why SmallRye Reactive Messaging uses a special thread to run +the poll loop should be obvious: the `Consumer` API is blocking. The +`Producer` API, on the other hand, is documented to be non-blocking. +However, in present versions, Kafka doesn’t guarantee that in all cases; +see https://issues.apache.org/jira/browse/KAFKA-3539[KAFKA-3539] for +more details. That is why SmallRye Reactive Messaging uses a dedicated +thread to run the send operations as well. + +Sometimes, SmallRye Reactive Messaging provides direct access to the +Kafka `Producer` or `Consumer`. For example, a +xref:kafka/consumer-rebalance-listener.adoc[`KafkaConsumerRebalanceListener`] +methods are always invoked on the polling thread, so they give you +direct access to `Consumer`. In such case, you should use the +`Producer`/`Consumer` API directly, instead of the +`KafkaProducer`/`KafkaConsumer` API. diff --git a/content/docs/smallrye-reactive-messaging/3/kafka/consumer-rebalance-listener.adoc b/content/docs/smallrye-reactive-messaging/3/kafka/consumer-rebalance-listener.adoc new file mode 100644 index 000000000..fbbdd605f --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/kafka/consumer-rebalance-listener.adoc @@ -0,0 +1,67 @@ += Consumer Rebalance Listener +:version-root: ../ +include::{version-root}_attributes.adoc[] + +To handle offset commit and assigned partitions yourself, you can +provide a consumer rebalance listener. To achieve this, implement the +`io.smallrye.reactive.messaging.kafka.KafkaConsumerRebalanceListener` +interface, make the implementing class a bean, and add the `@Identifier` +qualifier. A usual use case is to store offset in a separate data store +to implement exactly-once semantic, or starting the processing at a +specific offset. + +The listener is invoked every time the consumer topic/partition +assignment changes. For example, when the application starts, it invokes +the `partitionsAssigned` callback with the initial set of +topics/partitions associated with the consumer. If, later, this set +changes, it calls the `partitionsRevoked` and `partitionsAssigned` +callbacks again, so you can implement custom logic. + +Note that the rebalance listener methods are called from the Kafka +_polling_ thread and must block the caller thread until completion. +That’s because the rebalance protocol has synchronization barriers, and +using asynchronous code in a rebalance listener may be executed after +the synchronization barrier. + +When topics/partitions are assigned or revoked from a consumer, it +pauses the message delivery and restarts once the rebalance completes. + +If the rebalance listener handles offset commit on behalf of the user +(using the `ignore` commit strategy), the rebalance listener *must* +commit the offset synchronously in the `partitionsRevoked` callback. We +also recommend applying the same logic when the application stops. + +Unlike the `ConsumerRebalanceListener` from Apache Kafka, the +`io.smallrye.reactive.messaging.kafka.KafkaConsumerRebalanceListener` +methods pass the Kafka `Consumer` and the set of topics/partitions. + +== Example + +In this example we set-up a consumer that always starts on messages from +at most 10 minutes ago (or offset 0). First we need to provide a bean +that implements the +`io.smallrye.reactive.messaging.kafka.KafkaConsumerRebalanceListener` +interface and is annotated with `@Identifier`. We then must configure +our inbound connector to use this named bean. + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaRebalancedConsumerRebalanceListener.java[indent=0] +---- + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaRebalancedConsumer.java[indent=0] +---- + +To configure the inbound connector to use the provided listener we +either set the consumer rebalance listener’s name: + +* `mp.messaging.incoming.rebalanced-example.consumer-rebalance-listener.name=rebalanced-example.rebalancer` + +Or have the listener’s name be the same as the group id: + +* `mp.messaging.incoming.rebalanced-example.group.id=rebalanced-example.rebalancer` + +Setting the consumer rebalance listener’s name takes precedence over +using the group id. diff --git a/content/docs/smallrye-reactive-messaging/3/kafka/default-configuration.adoc b/content/docs/smallrye-reactive-messaging/3/kafka/default-configuration.adoc new file mode 100644 index 000000000..a8da3024e --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/kafka/default-configuration.adoc @@ -0,0 +1,80 @@ += Retrieving Kafka default configuration +:version-root: ../ +include::{version-root}_attributes.adoc[] + +If your application/runtime exposes as a CDI _bean_ of type +`Map++<++String, Object` with the identifier `default-kafka-broker`, this +configuration is used to establish the connection with the Kafka broker. + +For example, you can imagine exposing this map as follows: + +[source,java] +---- +@Produces +@ApplicationScoped +@Identifier("default-kafka-broker") +public Map createKafkaRuntimeConfig() { + Map properties = new HashMap<>(); + + StreamSupport + .stream(config.getPropertyNames().spliterator(), false) + .map(String::toLowerCase) + .filter(name -> name.startsWith("kafka")) + .distinct() + .sorted() + .forEach(name -> { + final String key = name.substring("kafka".length() + 1).toLowerCase().replaceAll("[^a-z0-9.]", "."); + final String value = config.getOptionalValue(name, String.class).orElse(""); + properties.put(key, value); + }); + + return properties; +} +---- + +This previous example would extract all the configuration keys from +MicroProfile Config starting with `kafka`. + +[NOTE] +.Quarkus +==== + +Starting with Quarkus 1.5, a map corresponding to the previous example +is automatically provided. + +==== + +In addition to this default configuration, you can configure the name of +the `Map` producer using the `kafka-configuration` attribute: + +[source,properties] +---- +mp.messaging.incoming.my-channel.connector=smallrye-kafka +mp.messaging.incoming.my-channel.kafka-configuration=my-configuration +---- + +In this case, the connector looks for the `Map` associated with the +`my-configuration` name. If `kafka-configuration` is not set, an +optional lookup for a `Map` exposed with the channel name (`my-channel` +in the previous example) is done. + +[IMPORTANT] +==== + +If `kafka-configuration` is set and no `Map` can be found, the +deployment fails. + +==== + +Attribute values are resolved as follows: + +[arabic] +. if the attribute is set directly on the channel configuration +(`mp.messaging.incoming.my-channel.attribute=value`), this value is +used +. if the attribute is not set on the channel, the connector looks for +a `Map` with the channel name or the configured +`kafka-configuration` (if set) and the value is retrieved from that +`Map` +. If the resolved `Map` does not contain the value the default `Map` +is used (exposed with the `default-kafka-broker` name) diff --git a/content/docs/smallrye-reactive-messaging/3/kafka/health.adoc b/content/docs/smallrye-reactive-messaging/3/kafka/health.adoc new file mode 100644 index 000000000..e4f16f771 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/kafka/health.adoc @@ -0,0 +1,73 @@ += Health reporting +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The Kafka connector reports the readiness and liveness of each channel +managed by the connector. + +[NOTE] +==== + +To disable health reporting, set the `health-enabled` attribute for the +channel to `false`. + +==== + +== Readiness + +On the inbound side, two strategies are available to check the readiness +of the application. The default strategy verifies that we have at least +one active connection with the broker. This strategy is lightweight. + +You can also enable another strategy by setting the +`health-readiness-topic-verification` attribute to `true`. In this case, +the check verifies that: + +* the broker is available +* the Kafka topic is created (available in the broker). +* no failures have been caught + +With this second strategy, if you consume multiple topics using the +`topics` attribute, the readiness check verifies that all the consumed +topics are available. If you use a pattern (using the `pattern` +attribute), the readiness check verifies that at least one existing +topic matches the pattern. + +On the outbound side (writing records to Kafka), two strategies are also +offered. The default strategy just verifies that the producer has at +least one active connection with the broker. + +You can also enable another strategy by setting the +`health-readiness-topic-verification` attribute to `true`. In this case, +teh check verifies that + +* the broker is available +* the Kafka topic is created (available in the broker). + +With this second strategy, the readiness check uses a Kafka Admin Client +to retrieve the existing topics. Retrieving the topics can be a lengthy +operation. You can configure a timeout using the +`health-readiness-timeout` attribute. The default timeout is set to 2 +seconds. + +Also, you can disable the readiness checks altogether by setting +`health-readiness-enabled` to `false`. + +== Liveness + +On the inbound side (receiving records from Kafka), the liveness check +verifies that: + +* no failures have been caught +* the client is connected to the broker + +On the outbound side (writing records to Kafka), the liveness check +verifies that: + +* no failures have been caught + +Note that a message processing failures _nacks_ the message which is +then handled by the failure-strategy. It the responsibility of the +failure-strategy to report the failure and influence the outcome of the +liveness checks. The `fail` failure strategy reports the failure and so +the liveness check will report the failure. diff --git a/content/docs/smallrye-reactive-messaging/3/kafka/kafka.adoc b/content/docs/smallrye-reactive-messaging/3/kafka/kafka.adoc new file mode 100644 index 000000000..dbe86b6d0 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/kafka/kafka.adoc @@ -0,0 +1,47 @@ += Apache Kafka Connector +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The Kafka connector adds support for Kafka to Reactive Messaging. With +it you can receive Kafka Records as well as write `message` into Kafka. + +https://kafka.apache.org/[Apache Kafka] is a popular distributed +streaming platform. It lets you: + +* Publish and subscribe to streams of records, similar to a message +queue or enterprise messaging system. +* Store streams of records in a fault-tolerant durable way. +* Process streams of records as they occur. + +The Kafka cluster stores streams of _records_ in categories called +_topics_. Each record consists of a _key_, a _value_, and a _timestamp_. + +For more details about Kafka, check the +https://kafka.apache.org/intro[documentation]. + +== Using the Kafka Connector + +To use the Kafka Connector, add the following dependency to your +project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.reactive + smallrye-reactive-messaging-kafka + {project-version} + +---- + +The connector name is: `smallrye-kafka`. + +So, to indicate that a channel is managed by this connector you need: + +[source,properties] +---- +# Inbound +mp.messaging.incoming.[channel-name].connector=smallrye-kafka + +# Outbound +mp.messaging.outgoing.[channel-name].connector=smallrye-kafka +---- diff --git a/content/docs/smallrye-reactive-messaging/3/kafka/kerberos.adoc b/content/docs/smallrye-reactive-messaging/3/kafka/kerberos.adoc new file mode 100644 index 000000000..873617497 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/kafka/kerberos.adoc @@ -0,0 +1,22 @@ += Kerberos authentication +:version-root: ../ +include::{version-root}_attributes.adoc[] + +When using https://en.wikipedia.org/wiki/Kerberos_(protocol)[Kerberos] +authentication, you need to configure the connector with: + +* the security protocol set to `SASL++_++PLAINTEXT` +* the SASL mechanism set to `GSSAPI` +* the Jaas config configured with `Krb5LoginModule` +* the Kerberos service name + +The following snippet provides an example: + +[source,properties] +---- +kafka.bootstrap.servers=ip-192-168-0-207.us-east-2.compute.internal:9094 +kafka.sasl.mechanism=GSSAPI +kafka.security.protocol=SASL_PLAINTEXT +kafka.sasl.jaas.config=com.sun.security.auth.module.Krb5LoginModule required doNotPrompt=true refreshKrb5Config=true useKeyTab=true storeKey=true keyTab="file:/opt/kafka/krb5/kafka-producer.keytab" principal="kafka-producer/ip-192-168-0-207.us-east-2.compute.internal@INTERNAL"; +kafka.sasl.kerberos.service.name=kafka +---- diff --git a/content/docs/smallrye-reactive-messaging/3/kafka/protobuf-configuration.adoc b/content/docs/smallrye-reactive-messaging/3/kafka/protobuf-configuration.adoc new file mode 100644 index 000000000..36e025d1e --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/kafka/protobuf-configuration.adoc @@ -0,0 +1,51 @@ += Using Google Protobuf serializer/deserializer +:version-root: ../ +include::{version-root}_attributes.adoc[] + +If you are using https://developers.google.com/protocol-buffers/[Protocol +Buffers] +serializer/deserializer, please note the following configuration +properties. + +== For https://docs.confluent.io/platform/current/schema-registry/serdes-develop/serdes-protobuf.html#serdes-and-formatter-protobuf[Confluent] Schema Registry + +Confluent protobuf library is `io.confluent:kafka-protobuf-serializer`. +Note that this library is not available in Maven Central, you need to +use the https://docs.confluent.io/clients-kafka-java/current/overview.html[Confluent Maven +repository]. + +=== Consumer + +[width="100%",cols="49%,51%",options="header",] +|=== +|Property |Recommended value +|value.deserializer |io.confluent.kafka.serializers.protobuf.KafkaProtobufDeserializer +|schema.registry.url |http://%7Byour_host%7D:%7Byour_port%7D/ +|mp.messaging.incoming.++[++channel++]++.specific.protobuf.value.type |your.package.DomainObjectKey$Key +|mp.messaging.incoming.++[++channel++]++.specific.protobuf.key.type |your.package.DomainObjectValue$Value +|=== + +Example: + +.... +mp.messaging.incoming.[channel].value.deserializer=io.confluent.kafka.serializers.protobuf.KafkaProtobufDeserializer +mp.messaging.incoming.[channel].schema.registry.url=http://{your_host}:{your_port}/ +mp.messaging.incoming.[channel].specific.protobuf.value.type=your.package.DomainObjectKey$Key +mp.messaging.incoming.[channel].specific.protobuf.key.type=your.package.DomainObjectValue$Value +.... + +=== Producer + +[width="100%",cols="24%,76%",options="header",] +|=== +|Property |Recommended value +|value.serializer |io.confluent.kafka.serializers.protobuf.KafkaProtobufSerializer +|schema.registry.url |http://%7Byour_host%7D:%7Byour_port%7D/ +|=== + +Example: + +.... +mp.messaging.outgoing.[channel].value.serializer=io.confluent.kafka.serializers.protobuf.KafkaProtobufSerializer +mp.messaging.outgoing.[channel].schema.registry.url=http://{your_host}:{your_port}/ +.... diff --git a/content/docs/smallrye-reactive-messaging/3/kafka/receiving-kafka-records.adoc b/content/docs/smallrye-reactive-messaging/3/kafka/receiving-kafka-records.adoc new file mode 100644 index 000000000..4aaaf4a4b --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/kafka/receiving-kafka-records.adoc @@ -0,0 +1,647 @@ += Receiving Kafka Records +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The Kafka Connector retrieves Kafka Records from Kafka Brokers and maps +each of them to Reactive Messaging `Messages`. + +== Example + +Let’s imagine you have a Kafka broker running, and accessible using the +`kafka:9092` address (by default it would use `localhost:9092`). +Configure your application to receive Kafka records from a Kafka _topic_ +on the `prices` channel as follows: + +[source,properties] +---- +kafka.bootstrap.servers=kafka:9092 # <1> + +mp.messaging.incoming.prices.connector=smallrye-kafka # <2> +mp.messaging.incoming.prices.value.deserializer=org.apache.kafka.common.serialization.DoubleDeserializer # <3> +mp.messaging.incoming.prices.broadcast=true # <4> +---- + +[arabic] +. Configure the broker location. You can configure it globally or per +channel +. Configure the connector to manage the `prices` channel +. Sets the (Kafka) deserializer to read the record’s value +. Make sure that we can receive from more than one consumer (see +`KafkaPriceConsumer` and `KafkaPriceMessageConsumer` below) + +[NOTE] +==== + +You don’t need to set the Kafka topic. By default, it uses the channel +name (`prices`). You can configure the `topic` attribute to override it. + +==== + +Then, your application receives `Message++<++Double++>++`. You can consume the +payload directly: + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaPriceConsumer.java[indent=0] +---- + +Or, you can retrieve the `Message++<++Double++>++`: + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaPriceMessageConsumer.java[indent=0] +---- + +== Deserialization + +The deserialization is handled by the underlying Kafka Client. You need +to configure the: + +* `mp.messaging.incoming.++[++channel-name++]++.value.deserializer` to +configure the value deserializer (mandatory) +* `mp.messaging.incoming.++[++channel-name++]++.key.deserializer` to configure +the key deserializer (optional, default to `String`) + +If you want to use a custom deserializer, add it to your `CLASSPATH` and +configure the associate attribute. + +In addition, the Kafka Connector also provides a set of _message +converters_. So you can receive _payloads_ representing records from +Kafka using: + +* link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-kafka/{javadoc-version}/io/smallrye/reactive/messaging/kafka/Record.html[`Record`] - a pair key/value +* https://kafka.apache.org/26/javadoc/index.html?org/apache/kafka/clients/consumer/ConsumerRecord.html[ConsumerRecord++<++K,V++>++] - +a structure representing the record with all its metadata + +[source,java] +---- +include::{examples-dir}/kafka/inbound/Converters.java[tags=code,indent=0] +---- + +== Inbound Metadata + +Messages coming from Kafka contains an instance of +link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-kafka/{javadoc-version}/io/smallrye/reactive/messaging/kafka/api/IncomingKafkaRecordMetadata.html[`IncomingKafkaRecordMetadata`] +in the metadata. It provides the key, topic, partitions, +headers and so on: + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaMetadataExample.java[tags=code,indent=0] +---- + +== Acknowledgement + +When a message produced from a Kafka record is acknowledged, the +connector invokes a _commit strategy_. These strategies decide when the +consumer offset for a specific topic/partition is committed. Committing +an offset indicates that all previous records have been processed. It is +also the position where the application would restart the processing +after a crash recovery or a restart. + +Committing every offset has performance penalties as Kafka offset +management can be slow. However, not committing the offset often enough +may lead to message duplication if the application crashes between two +commits. + +The Kafka connector supports three strategies: + +* `throttled` keeps track of received messages and commit to the next +offset after the latest _acked_ message in sequence. This strategy +guarantees _at-least-once delivery_ even if the channel performs +asynchronous processing. The connector tracks the received records +and periodically (period specified by `auto.commit.interval.ms` +(default: 5000)) commits the highest consecutive offset. The +connector will be marked as unhealthy if a message associated with a +record is not acknowledged in +`throttled.unprocessed-record-max-age.ms` (default: 60000). Indeed, +this strategy cannot commit the offset as soon as a single record +processing fails (see failure-strategy to configure what happens on +failing processing). If `throttled.unprocessed-record-max-age.ms` is +set to less than or equal to 0, it does not perform any health check +verification. Such a setting might lead to running out of memory if +there are poison pill messages. This strategy is the default if +`enable.auto.commit` is not explicitly set to `true`. +* `checkpoint` allows persisting consumer offsets on a "state store", +instead of committing them back to the Kafka broker. Using the +`CheckpointMetadata` API, consumer code can persist a processing +state with the offset to mark the progress of a consumer. +When the processing continues from a previously persisted offset, +it seeks the Kafka consumer to that offset and also restores the +persisted state, continuing the stateful processing from where it +left off. The `checkpoint` strategy holds locally the processing +state associated with the latest offset, and persists it +periodically to the state store (period specified by +`auto.commit.interval.ms` (default: 5000)). The connector will be +marked as unhealthy if no processing state is persisted to the state +store in `checkpoint.unsynced-state-max-age.ms` (default: 10000). +Using the `CheckpointMetadata` API the user code can force to persist +the state on message ack. If `checkpoint.unsynced-state-max-age.ms` +is set to less than or equal to 0, it does not perform any health +check verification. For more information, see +xref:#stateful-processing-with-checkpointing[Stateful processing with Checkpointing] +* `latest` commits the record offset received by the Kafka consumer as +soon as the associated message is acknowledged (if the offset is +higher than the previously committed offset). This strategy provides +_at-least-once_ delivery if the channel processes the message +without performing any asynchronous processing. This strategy should +not be used on high-load as offset commit is expensive. However, it +reduces the risk of duplicates. +* `ignore` performs no commit. This strategy is the default strategy +when the consumer is explicitly configured with `enable.auto.commit` +to `true`. It delegates the offset commit to the Kafka client. When +`enable.auto.commit` is `true` this strategy *DOES NOT* guarantee +at-least-once delivery. However, if the processing failed between +two commits, messages received after the commit and before the +failure will be re-processed. + +[IMPORTANT] +==== + +The Kafka connector disables the Kafka _auto commit_ if not explicitly +enabled. This behavior differs from the traditional Kafka consumer. + +==== + +If high-throughout is important for you, and not limited by the +downstream, we recommend to either: + +* Use the `throttled` policy +* or set `enable.auto.commit` to `true` and annotate the consuming +method with `@Acknowledgment(Acknowledgment.Strategy.NONE)` + +== Failure Management + +If a message produced from a Kafka record is _nacked_, a failure +strategy is applied. The Kafka connector supports 3 strategies: + +* `fail` - fail the application, no more records will be processed. +(default) The offset of the record that has not been processed +correctly is not committed. +* `ignore` - the failure is logged, but the processing continue. The +offset of the record that has not been processed correctly is +committed. +* `dead-letter-queue` - the offset of the record that has not been +processed correctly is committed, but the record is written to a +(Kafka) _dead letter queue_ topic. + +The strategy is selected using the `failure-strategy` attribute. + +In the case of `dead-letter-queue`, you can configure the following +attributes: + +* `dead-letter-queue.topic`: the topic to use to write the records not +processed correctly, default is `dead-letter-topic-$channel`, with +`$channel` being the name of the channel. +* `dead-letter-queue.producer-client-id`: the client id used by the kafka +producer when sending records to dead letter queue topic. If not specified +it will default to `kafka-dead-letter-topic-producer-$client-id`, with $client-id +being the value obtained from consumer client id. +* `dead-letter-queue.key.serializer`: the serializer used to write the +record key on the dead letter queue. By default, it deduces the +serializer from the key deserializer. +* `dead-letter-queue.value.serializer`: the serializer used to write +the record value on the dead letter queue. By default, it deduces +the serializer from the value deserializer. + +The record written on the dead letter topic contains the original +record’s headers, as well as a set of additional headers about the +original record: + +* `dead-letter-reason` - the reason of the failure (the `Throwable` +passed to `nack()`) +* `dead-letter-cause` - the cause of the failure (the `getCause()` of +the `Throwable` passed to `nack()`), if any +* `dead-letter-topic` - the original topic of the record +* `dead-letter-partition` - the original partition of the record +(integer mapped to String) +* `dead-letter-offset` - the original offset of the record (long +mapped to String) + +When using `dead-letter-queue`, it is also possible to change some +metadata of the record that is sent to the dead letter topic. To do +that, use the `Message.nack(Throwable, Metadata)` method: + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaDeadLetterExample.java[tags=code,indent=0] +---- + +The `Metadata` may contain an instance of `OutgoingKafkaRecordMetadata`. +If the instance is present, the following properties will be used: + +* key; if not present, the original record’s key will be used +* topic; if not present, the configured dead letter topic will be used +* partition; if not present, partition will be assigned automatically +* headers; combined with the original record’s headers, as well as the +`dead-letter-++*++` headers described above + +== Custom commit and failure strategies + +In addition to provided strategies, it is possible to implement custom +commit and failure strategies and configure Kafka channels with them. + +For example, for a custom commit strategy, implement the +link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-kafka/{javadoc-version}/io/smallrye/reactive/messaging/kafka/commit/KafkaCommitHandler.html[`KafkaCommitHandler`] interface, +and provide a managed bean implementing the `KafkaCommitHandler.Factory` interface, +identified using `@Identifier` qualifier. + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaCustomCommit.java[indent=0] +---- + +Finally, to use the custom commit strategy, +set the `commit-strategy` attribute to the identifier of the commit handler factory: +`mp.messaging.incoming.$channel.commit-strategy=custom`. +Similarly, custom failure strategies can be configured using `failure-strategy` attribute. + +[NOTE] +==== + +If the custom strategy implementation inherits +link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-kafka/{javadoc-version}/io/smallrye/reactive/messaging/kafka/commit/ContextHolder.html[`ContextHolder`] class it can access the +Vert.x event-loop context created for the Kafka consumer + +==== + +== Retrying processing + +You can combine Reactive Messaging with https://github.com/smallrye/smallrye-fault-tolerance[SmallRye Fault +Tolerance], and +retry processing when it fails: + +[source,java] +---- +@Incoming("kafka") +@Outgoing("processed") +@Retry(delay = 10, maxRetries = 5) +public String process(String v) { + // ... retry if this method throws an exception +} +---- + +You can configure the delay, the number of retries, the jitter... + +If your method returns a `Uni`, you need to add the `@NonBlocking` +annotation: + +[source,java] +---- +@Incoming("kafka") +@Outgoing("processed") +@Retry(delay = 10, maxRetries = 5) +@NonBlocking +public Uni process(String v) { + // ... retry if this method throws an exception or the returned Uni produce a failure +} +---- + +The incoming messages are acknowledged only once the processing +completes successfully. So, it commits the offset after the successful +processing. If after the retries the processing still failed, the +message is _nacked_ and the failure strategy is applied. + +You can also use `@Retry` on methods only consuming incoming messages: + +[source,java] +---- +@Incoming("kafka") +@Retry(delay = 10, maxRetries = 5) +public void consume(String v) { + // ... retry if this method throws an exception +} +---- + +== Handling deserialization failures + +Because deserialization happens before creating a `Message`, the failure +strategy presented above cannot be applied. However, when a +deserialization failure occurs, you can intercept it and provide a +fallback value. To achieve this, create a CDI bean implementing the +link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-kafka/{javadoc-version}/io/smallrye/reactive/messaging/kafka/api/DeserializationFailureHandler.html[`DeserializationFailureHandler`] +interface: + +[source,java] +---- +@ApplicationScoped +@Identifier("failure-retry") // Set the name of the failure handler +public class MyDeserializationFailureHandler + implements DeserializationFailureHandler { // Specify the expected type + + @Override + public JsonObject decorateDeserialization(Uni deserialization, + String topic, boolean isKey, String deserializer, byte[] data, + Headers headers) { + return deserialization + .onFailure().retry().atMost(3) + .await().atMost(Duration.ofMillis(200)); + } +} +---- + +The bean must be exposed with the `@Identifier` qualifier specifying the +name of the bean. Then, in the connector configuration, specify the +following attribute: + +* `mp.messaging.incoming.$channel.key-deserialization-failure-handler`: +name of the bean handling deserialization failures happening for the +record’s key +* `mp.messaging.incoming.$channel.value-deserialization-failure-handler`: +name of the bean handling deserialization failures happening for the +record’s value, + +The handler is called with the deserialization action as a `Uni++<++T++>++`, the +record’s topic, a boolean indicating whether the failure happened on a +key, the class name of the deserializer that throws the exception, the +corrupted data, the exception, and the records headers augmented with +headers describing the failure (which ease the write to a dead letter). +On the deserialization `Uni` failure strategies like retry, providing a +fallback value or applying timeout can be implemented. Note that the +method must await on the result and return the deserialized object. +Alternatively, the handler can only implement +`handleDeserializationFailure` method and provide a fallback value, +which may be `null`. + +If you don’t configure a deserialization failure handlers and a +deserialization failure happens, the application is marked unhealthy. +You can also ignore the failure, which will log the exception and +produce a `null` value. To enable this behavior, set the +`mp.messaging.incoming.$channel.fail-on-deserialization-failure` +attribute to `false`. + +== Receiving Cloud Events + +The Kafka connector supports https://cloudevents.io/[Cloud Events]. +When the connector detects a _structured_ or _binary_ Cloud Events, it +adds a link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-kafka/{javadoc-version}/io/smallrye/reactive/messaging/kafka/IncomingKafkaCloudEventMetadata.html[`IncomingKafkaCloudEventMetadata`] in the metadata of the +Message. `IncomingKafkaCloudEventMetadata` contains the various (mandatory and optional) Cloud Event attributes. + +If the connector cannot extract the Cloud Event metadata, it sends the +Message without the metadata. + +=== Binary Cloud Events + +For `binary` Cloud Events, *all* mandatory Cloud Event attributes must +be set in the record header, prefixed by `ce++_++` (as mandated by the +https://github.com/cloudevents/spec/blob/v1.0/kafka-protocol-binding.md[protocol +binding]). +The connector considers headers starting with the `ce++_++` prefix but not +listed in the specification as extensions. You can access them using the +`getExtension` method from `IncomingKafkaCloudEventMetadata`. You can +retrieve them as `String`. + +The `datacontenttype` attribute is mapped to the `content-type` header +of the record. The `partitionkey` attribute is mapped to the record’s +key, if any. + +Note that all headers are read as UTF-8. + +With binary Cloud Events, the record’s key and value can use any +deserializer. + +=== Structured Cloud Events + +For `structured` Cloud Events, the event is encoded in the record’s +value. Only JSON is supported, so your event must be encoded as JSON in +the record’s value. + +Structured Cloud Event must set the `content-type` header of the record +to `application/cloudevents` or prefix the value with +`application/cloudevents` such as: +`application/cloudevents{plus}json; charset=UTF-8`. + +To receive structured Cloud Events, your value deserializer must be: + +* `org.apache.kafka.common.serialization.StringDeserializer` +* `org.apache.kafka.common.serialization.ByteArrayDeserializer` +* `io.vertx.kafka.client.serialization.JsonObjectDeserializer` + +As mentioned previously, the value must be a valid JSON object +containing at least all the mandatory Cloud Events attributes. + +If the record is a structured Cloud Event, the created Message’s payload +is the Cloud Event `data`. + +The `partitionkey` attribute is mapped to the record’s key if any. + +== Consumer Rebalance Listener + +To handle offset commit and assigned partitions yourself, you can +provide a consumer rebalance listener. To achieve this, implement the +`io.smallrye.reactive.messaging.kafka.KafkaConsumerRebalanceListener` +interface, make the implementing class a bean, and add the `@Identifier` +qualifier. A usual use case is to store offset in a separate data store +to implement exactly-once semantic, or starting the processing at a +specific offset. + +The listener is invoked every time the consumer topic/partition +assignment changes. For example, when the application starts, it invokes +the `partitionsAssigned` callback with the initial set of +topics/partitions associated with the consumer. If, later, this set +changes, it calls the `partitionsRevoked` and `partitionsAssigned` +callbacks again, so you can implement custom logic. + +Note that the rebalance listener methods are called from the Kafka +_polling_ thread and must block the caller thread until completion. +That’s because the rebalance protocol has synchronization barriers, and +using asynchronous code in a rebalance listener may be executed after +the synchronization barrier. + +When topics/partitions are assigned or revoked from a consumer, it +pauses the message delivery and restarts once the rebalance completes. + +If the rebalance listener handles offset commit on behalf of the user +(using the `ignore` commit strategy), the rebalance listener *must* +commit the offset synchronously in the `partitionsRevoked` callback. We +also recommend applying the same logic when the application stops. + +Unlike the `ConsumerRebalanceListener` from Apache Kafka, the +`io.smallrye.reactive.messaging.kafka.KafkaConsumerRebalanceListener` +methods pass the Kafka `Consumer` and the set of topics/partitions. + +=== Example + +In this example we set-up a consumer that always starts on messages from +at most 10 minutes ago (or offset 0). First we need to provide a bean +that implements the +`io.smallrye.reactive.messaging.kafka.KafkaConsumerRebalanceListener` +interface and is annotated with `@Identifier`. We then must configure +our inbound connector to use this named bean. + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaRebalancedConsumerRebalanceListener.java[indent=0] +---- + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaRebalancedConsumer.java[indent=0] +---- + +To configure the inbound connector to use the provided listener we +either set the consumer rebalance listener’s name: + +* `mp.messaging.incoming.rebalanced-example.consumer-rebalance-listener.name=rebalanced-example.rebalancer` + +Or have the listener’s name be the same as the group id: + +* `mp.messaging.incoming.rebalanced-example.group.id=rebalanced-example.rebalancer` + +Setting the consumer rebalance listener’s name takes precedence over +using the group id. + +== Receiving Kafka Records in Batches + +By default, incoming methods receive each Kafka record individually. +Under the hood, Kafka consumer clients poll the broker constantly and +receive records in batches, presented inside the `ConsumerRecords` +container. + +In *batch* mode, your application can receive all the records returned +by the consumer *poll* in one go. + +To achieve this you need to set +`mp.messaging.incoming.$channel.batch=true` *and* specify a compatible +container type to receive all the data: + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaRecordBatchPayloadExample.java[tags=code,indent=0] +---- + +The incoming method can also receive `Message++<++List++<++Payload++>++`, +`KafkaBatchRecords++<++Payload++>++` `ConsumerRecords++<++Key, Payload++>++` types, They +give access to record details such as offset or timestamp : + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaRecordBatchExample.java[tags=code,indent=0] +---- + +Note that the successful processing of the incoming record batch will +commit the latest offsets for each partition received inside the batch. +The configured commit strategy will be applied for these records only. + +Conversely, if the processing throws an exception, all messages are +_nacked_, applying the failure strategy for all the records inside the +batch. + +== Stateful processing with Checkpointing + +[WARNING] +.Experimental +==== + +Checkpointing is experimental, and APIs and features are subject +to change in the future. + +==== + +The `checkpoint` commit strategy allows for a Kafka incoming channel to +manage topic-partition offsets, not by committing on the Kafka broker, +but by persisting consumers' advancement on a +xref:#implementing-state-stores[_state store_]. + +In addition to that, if the consumer builds an internal state as +a result of consumed records, the topic-partition offset persisted +to the state store can be associated with a _processing state_, +saving the local state to the persistent store. When a consumer +restarts or consumer group instances scale, i.e. when new partitions +get assigned to the consumer, the checkpointing works by resuming the +processing from the latest offset and its saved state. + +The `@Incoming` channel consumer code can manipulate the processing +state through the `CheckpointMetadata` API: + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaCheckpointExample.java[tags=code,indent=0] +---- + +The `transform` method allows applying a transformation function to +the current state, producing a changed state and registering it +locally for checkpointing. By default, the local state is synced +(persisted) to the state store periodically, period specified by +`auto.commit.interval.ms`, (default: 5000). If `persistOnAck` flag +is given, the latest state is persisted to the state store eagerly +on message acknowledgment. The `setNext` method works similarly +directly setting the latest state. + +The `checkpoint` commit strategy tracks when a processing state +is last persisted for each topic-partition. If an outstanding state +change can not be persisted for `checkpoint.unsynced-state-max-age.ms` +(default: 10000), the channel is marked unhealthy. + +Where and how processing states are persisted is decided by the +state store implementation. This can be configured on the incoming +channel using `checkpoint.state-store` configuration property, +using the state store factory identifier name. +The serialization of state objects depends on the state store +implementation. In order to instruct state stores for serialization +can require configuring the type name of state objects +using `checkpoint.state-type` property. + +In order to keep Smallrye Reactive Messaging free of persistence-related +dependencies, this library includes only a default state store named `file`. +It is based on Vert.x Filesystem API and stores the processing state +in Json formatted files, in a local directory configured by the +`checkpoint.file.state-dir` property. State files follow the naming +scheme `++[++consumer-group-id++]++:++[++topic++]++:++[++partition++]++`. + +=== Implementing State Stores + +State store implementations are required to implement `CheckpointStateStore` +interface, and provide a managed bean implementing +`CheckpointStateStore.Factory`, identified with `@Identifier` bean +qualifier indicating the name of the state-store. +The factory bean identifier indicates the name to configure on +`checkpoint.state-store` config property. +The factory is discovered as a CDI managed bean and state store is +created once per channel: + +[source,java] +---- +include::{examples-dir}/kafka/inbound/MyCheckpointStateStore.java[indent=0] +---- + +The checkpoint commit strategy calls the state store in following events: + +* `fetchProcessingState` : on partitions assigned, to seek the consumer to the latest offset. +* `persistProcessingState` on partitions revoked, to persist the state of last processed record. +* `persistProcessingState` on message acknowledgement, if a new state is set during the processing and `persistOnAck` flag is set. +* `persistProcessingState` on `auto.commit.interval.ms` intervals, if a new state is set during processing. +* `persistProcessingState` on channel shutdown. +* `close` on channel shutdown. + +== Configuration Reference + +include::{generated-dir}/smallrye-kafka-incoming.adoc[indent=0] + +You can also pass any property supported by the underlying https://kafka.apache.org/documentation/#consumerconfigs[Kafka +consumer]. + +For example, to configure the `max.poll.records` property, use: + +[source,properties] +---- +mp.messaging.incoming.[channel].max.poll.records=1000 +---- + +Some consumer client properties are configured to sensible default +values: + +If not set, `reconnect.backoff.max.ms` is set to `10000` to avoid high +load on disconnection. + +If not set, `key.deserializer` is set to +`org.apache.kafka.common.serialization.StringDeserializer`. + +The consumer `client.id` is configured according to the number of +clients to create using `mp.messaging.incoming.++[++channel++]++.partitions` +property. + +* If a `client.id` is provided, it is used as-is or suffixed with +client index if `partitions` property is set. +* If a `client.id` is not provided, it is generated as +`kafka-consumer-++[++channel++][++-index++]++`. diff --git a/content/docs/smallrye-reactive-messaging/3/kafka/test-companion.adoc b/content/docs/smallrye-reactive-messaging/3/kafka/test-companion.adoc new file mode 100644 index 000000000..d25aa111f --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/kafka/test-companion.adoc @@ -0,0 +1,158 @@ += Kafka Companion +:version-root: ../ +include::{version-root}_attributes.adoc[] + +[WARNING] +.Experimental +==== + +Kafka Companion is experimental and APIs are subject to change in the future. + +==== + +The *Kafka Companion* is a separate Java library for helping to test Kafka applications. +It is not intended to mock Kafka, but to the contrary, connect to a Kafka broker and provide high-level features. + +It is not limited to the SmallRye Reactive Messaging testing, but intends to improve the testability of applications using Kafka. Some of its features: + +* Running In-container Kafka broker for tests via https://github.com/strimzi/test-container[strimzi-test-container]. +* Running the Kafka broker behind a https://github.com/Shopify/toxiproxy[toxiproxy] for simulating network issues. +* Running embedded Kafka Kraft broker for tests. +* Base classes for tests to bootstrap tests. +* Companion classes for easily creating tasks to produce and consume Kafka records. +* Writing assertions for produce and consume tasks, state of consumers, topics, offsets etc. + +== Getting started writing tests + +Easiest way of starting to write Kafka tests is to extend `KafkaCompanionTestBase`. +It starts a single-node Kafka broker for the test suite using testcontainers and creates the `KafkaCompanion` to connect to this broker: + +[source,java] +---- +include::{examples-dir}/kafka/companion/KafkaWithBaseTest.java[tags=code,indent=0] +---- + +`KafkaCompanion` can be used on its own to connect to a broker: + +[source,java] +---- +include::{examples-dir}/kafka/companion/KafkaTest.java[tags=companion,indent=0] +---- + +There are a couple of things to note on how Kafka companion handles *producers*, *consumers* and *tasks*: + +`ProducerBuilder` and `ConsumerBuilder` lazy descriptions of with which configuration to create a producer or a consumer. + +`ProducerTask` and `ConsumerTask` run asynchronously on dedicated threads and are started as soon as they are created. +A task terminates when it is explicitly stopped, when it's predefined duration or number of records has been produced/consumed, or when it encounters an error. +An exterior thread can await on records processed, or simply on termination of the task. +At a given time produced or consumed records are accessible through the task. + +The actual creation of the producer or consumer happens when a task is started. When the task terminates the producer or the consumer is automatically closed. + +For example, in the previous example: + +[arabic] +. We described a producer with a client id `my-producer` and `max.block.ms` of 5 seconds. +. Then we created a task to produce 100 records using the generator function, without waiting for its completion. +. We then described a consumer with group id `my-group` and which commits offset synchronously on every received record. +. Finally, we created a task to consume records for 10 seconds and await its completion. + +== Producing records + +=== Produce from records + +Produce given records: + +[source,java] +---- +include::{examples-dir}/kafka/companion/ProducerTest.java[tags=records,indent=0] +---- + +=== Produce from generator function + +Produce 10 records using the generator function: + +[source,java] +---- +include::{examples-dir}/kafka/companion/ProducerTest.java[tags=generator,indent=0] +---- + +=== Produce from CSV file + +Given a comma-separated file `records.csv` with the following content + +[source,csv] +---- +messages,0,a,asdf +messages,1,b,asdf +messages,3,c,asdf +---- + +Produce records from the file: + +[source,java] +---- +include::{examples-dir}/kafka/companion/ProducerTest.java[tags=csv,indent=0] +---- + +== Consuming records + +=== Consume from topics + +[source,java] +---- +include::{examples-dir}/kafka/companion/ConsumerTest.java[tags=topics,indent=0] +---- + +=== Consume from offsets + +[source,java] +---- +include::{examples-dir}/kafka/companion/ConsumerTest.java[tags=offsets,indent=0] +---- + +=== Consumer assignment and offsets + +During execution of the consumer task, information about the underlying consumer's topic partition assignment, position or committed offsets can be accessed. + +[source,java] +---- +include::{examples-dir}/kafka/companion/ConsumerTest.java[tags=committed,indent=0] +---- + +== Registering Custom Serdes + +KafkaCompanion handles Serializers and Deserializers for default types such as primitives, String, ByteBuffer, UUID. + +Serdes for custom types can be registered to the companion object, and will be used for producer and consumer tasks: + +[source,java] +---- +include::{examples-dir}/kafka/companion/KafkaTest.java[tags=serdes,indent=0] +---- + +== Topics + +Create, list, describe and delete topics: + +[source,java] +---- +include::{examples-dir}/kafka/companion/TopicsTest.java[tags=code,indent=0] +---- + +== Consumer Groups and Offsets + +List topic partition offsets: + +[source,java] +---- +include::{examples-dir}/kafka/companion/OffsetsTest.java[tags=offsets,indent=0] +---- + +List, describe, alter consumer groups and their offsets: + +[source,java] +---- +include::{examples-dir}/kafka/companion/OffsetsTest.java[tags=consumerGroups,indent=0] +---- diff --git a/content/docs/smallrye-reactive-messaging/3/kafka/transactions.adoc b/content/docs/smallrye-reactive-messaging/3/kafka/transactions.adoc new file mode 100644 index 000000000..c2e9a4cf3 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/kafka/transactions.adoc @@ -0,0 +1,119 @@ += Kafka Transactions and Exactly-Once Processing +:version-root: ../ +include::{version-root}_attributes.adoc[] + +[WARNING] +.Experimental +==== + +Kafka Transactions is an experimental feature. + +==== + +https://cwiki.apache.org/confluence/display/KAFKA/KIP-98+-+Exactly+Once+Delivery+and+Transactional+Messaging[Kafka transactions] enable atomic writes to multiple Kafka topics and partitions. +Inside a transaction, a producer writes records to the Kafka topic partitions as it would normally do. +If the transaction completes successfully, all the records previously written to the broker inside that transaction will be _committed_, and will be readable for consumers. +If an error during the transaction causes it to be _aborted_, none will be readable for consumers. + +There are a couple of fundamental things to consider before using transactions: + +* Each transactional producer is configured with a unique identifier called the `transactional.id`. +This is used to identify the same producer instance across application restarts. +By default, it is not configured, and transactions cannot be used. +When it is configured, the producer is limited to idempotent delivery, therefore `enable.idempotence=true` is implied. +* For *only* reading transactional messages, a consumer needs to explicitly configure its `isolation.level` property to `read++_++committed`. +This will make sure that the consumer will deliver only records committed inside a transaction, and filter out messages from aborted transactions. +* It should also be noted that this does not mean all records atomically written inside a transaction will be read atomically by the consumer. +Transactional producers can write to multiple topics and partitions inside a transaction, but consumers do not know where the transaction starts or ends. +Not only multiple consumers inside a consumer group can share those partitions, +all records which were part of a single transaction can be consumed in different batch of records by a consumer. + +Kafka connector provides `KafkaTransactions` custom emitter for writing records inside a transaction. +Before using a transaction we need to make sure that `transactional.id` is configured on the channel properties. + +.... +mp.messaging.outgoing.tx-out-example.transactional.id=example-tx-producer +.... + +It can be used as a regular emitter `@Channel`: + +[source,java] +---- +include::{examples-dir}/kafka/outbound/KafkaTransactionalProducer.java[indent=0] +---- + +[NOTE] +==== + +When `transactional.id` is provided `KafkaProducer#initTransactions` is called when the underlying Kafka producer is created. + +==== + +The function given to the `withTransaction` method receives a `TransactionalEmitter` for producing records, and returns a `Uni` that provides the result of the transaction. +If the processing completes successfully, the producer is flushed and the transaction is committed. +If the processing throws an exception, returns a failing `Uni`, or marks the `TransactionalEmitter` for abort, the transaction is aborted. + +If this method is called on a Vert.x context, the processing function is also called on that context. +Otherwise, it is called on the sending thread of the producer. + +[IMPORTANT] +==== + +A transaction is considered _in progress_ from the call to the `withTransaction` until the returned `Uni` results in success or failure. +While a transaction is in progress, subsequent calls to the `withTransaction`, including nested ones inside the given function, will throw `IllegalStateException`. +Note that in Reactive Messaging, the execution of processing methods is already serialized, unless `@Blocking(ordered = false)` is used. +If `withTransaction` can be called concurrently, for example from a REST endpoint, it is recommended to limit the concurrency of the execution. +This can be done using the `@Bulkhead` annotation from Microprofile Fault Tolerance. + +==== + +== Exactly-Once Processing + +Kafka Transactions API also allows managing consumer offsets inside a transaction, together with produced messages. +This in turn enables coupling a consumer with a transactional producer in a consume-transform-produce pattern, +also known as exactly-once processing. +It means that an application consumes messages from a topic-partition, processes them, publishes the results to a topic-partition, +and commits offsets of the consumed messages in a transaction. + +The `KafkaTransactions` emitter also provides a way to apply exactly-once processing to an incoming Kafka message inside a transaction. + +The following example includes a batch of Kafka records inside a transaction. + +.... +mp.messaging.outgoing.tx-out-example.transactional.id=example-tx-producer +mp.messaging.outgoing.in-channel.batch=true +mp.messaging.outgoing.in-channel.commit-strategy=ignore +mp.messaging.outgoing.in-channel.failure-strategy=ignore +.... + +[source,java] +---- +include::{examples-dir}/kafka/outbound/KafkaExactlyOnceProcessor.java[indent=0] +---- + +If the processing completes successfully, before committing the transaction, the topic partition offsets of the given batch message will be committed to the transaction. + +If the processing needs to abort, after aborting the transaction, the consumer's position is reset to the last committed offset, effectively resuming the consumption from that offset. +If no consumer offset has been committed, the consumer's position is reset to the beginning of the topic, even if the offset reset policy is `latest`. + +[IMPORTANT] +==== + +When using exactly-once processing, consumed message offset commits are handled by the transaction and therefore `commit-strategy` needs to be `ignore`. + +==== + +Any strategy can be employed for the `failure-strategy`. +Note that the `Uni` returned from the `#withTransaction` will yield a failure if the transaction fails and is aborted. + +The application can choose to handle the error case, but for the message consumption to continue, `Uni` returned from the `@Incoming` method must not result in failure. +`KafkaTransactions#withTransactionAndAck` method will ack and nack the message but will not stop the reactive stream. +Ignoring the failure simply resets the consumer to the last committed offsets and resumes the processing from there. + +[NOTE] +==== + +It is recommended to use exactly-once processing along with the batch consumption mode. +While it is possible to use it with a single Kafka message, it'll have a significant performance impact. + +==== diff --git a/content/docs/smallrye-reactive-messaging/3/kafka/writing-kafka-records.adoc b/content/docs/smallrye-reactive-messaging/3/kafka/writing-kafka-records.adoc new file mode 100644 index 000000000..6d27eee5b --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/kafka/writing-kafka-records.adoc @@ -0,0 +1,354 @@ += Writing Kafka Records +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The Kafka Connector can write Reactive Messaging `Messages` as Kafka +Records. + +== Example + +Let’s imagine you have a Kafka broker running, and accessible using the +`kafka:9092` address (by default it would use `localhost:9092`). +Configure your application to write the messages from the `prices` +channel into a Kafka _topic_ as follows: + +[source,properties] +---- +kafka.bootstrap.servers=kafka:9092 # <1> + +mp.messaging.outgoing.prices-out.connector=smallrye-kafka # <2> +mp.messaging.outgoing.prices-out.value.serializer=org.apache.kafka.common.serialization.DoubleSerializer # <3> +mp.messaging.outgoing.prices-out.topic=prices # <4> +---- + +[arabic] +. Configure the broker location. You can configure it globally or per +channel +. Configure the connector to manage the `prices` channel +. Sets the (Kafka) serializer to encode the message payload into the +record’s value +. Make sure the topic name is `prices` (and not the default +`prices-out`) + +Then, your application must send `Message++<++Double++>++` to the `prices` +channel. It can use `double` payloads as in the following snippet: + +[source,java] +---- +include::{examples-dir}/kafka/outbound/KafkaPriceProducer.java[indent=0] +---- + +Or, you can send `Message++<++Double++>++`: + +[source,java] +---- +include::{examples-dir}/kafka/outbound/KafkaPriceMessageProducer.java[indent=0] +---- + +== Serialization + +The serialization is handled by the underlying Kafka Client. You need to +configure the: + +* `mp.messaging.outgoing.++[++channel-name++]++.value.serializer` to configure +the value serializer (mandatory) +* `mp.messaging.outgoing.++[++channel-name++]++.key.serializer` to configure +the key serializer (optional, default to `String`) + +If you want to use a custom serializer, add it to your `CLASSPATH` and +configure the associate attribute. + +By default, the written record contains: + +* the `Message` payload as _value_ +* no key, or the key configured using the `key` attribute or the key +passed in the metadata attached to the `Message` +* the timestamp computed for the system clock (`now`) or the timestamp +passed in the metadata attached to the `Message` + +== Sending key/value pairs + +In the Kafka world, it’s often necessary to send _records_, i.e. a +key/value pair. The connector provides the link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-kafka/{javadoc-version}/io/smallrye/reactive/messaging/kafka/Record.html[`io.smallrye.reactive.messaging.kafka.Record`] +class that you can use to send a pair: + +[source,java] +---- +include::{examples-dir}/kafka/outbound/KafkaRecordExample.java[tags=code,indent=0] +---- + +When the connector receives a message with a `Record` payload, it +extracts the key and value from it. The configured serializers for the +key and the value must be compatible with the record’s key and value. +Note that the `key` and the `value` can be `null`. It is also possible +to create a record with a `null` key AND a `null` value. + +If you need more control on the written records, use +`OutgoingKafkaRecordMetadata`. + +== Outbound Metadata + +When sending `Messages`, you can add an instance of +link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-kafka/{javadoc-version}/io/smallrye/reactive/messaging/kafka/api/OutgoingKafkaRecordMetadata.html[`OutgoingKafkaRecordMetadata`] +to influence how the message is going to be written to Kafka. For example, +you can add Kafka headers, configure the record key… + +[source,java] +---- +include::{examples-dir}/kafka/outbound/KafkaOutboundMetadataExample.java[tags=code,indent=0] +---- + +== Propagating Record Key + +When processing messages, you can propagate incoming record key to the +outgoing record. + +Consider the following example method, which consumes messages from the +channel `in`, transforms the payload, and writes the result to the +channel `out`. + +[source,java] +---- +include::{examples-dir}/kafka/outbound/KafkaPriceProcessor.java[tags=code,indent=0] +---- + +Enabled with +`mp.messaging.outgoing.++[++channel-name++]++.propagate-record-key=true` +configuration, record key propagation produces the outgoing record with +the same _key_ as the incoming record. + +If the outgoing record already contains a _key_, it *won’t be +overridden* by the incoming record key. If the incoming record does +have a _null_ key, the `mp.messaging.outgoing.++[++channel-name++]++.key` +property is used. + +== Propagating Record headers + +You can also propagate incoming record headers to the outgoing record, by specifying the list of headers to be considered. + +`mp.messaging.outgoing.++[++channel-name++]++.propagate-headers=Authorization,Proxy-Authorization` + +If the ougoing record already defines a header with the same key, it won't be overriden by the incoming header. + +== Dynamic topic names + +Sometimes it is desirable to select the destination of a message +dynamically. In this case, you should not configure the topic inside +your application configuration file, but instead, use the outbound +metadata to set the name of the topic. + +For example, you can route to a dynamic topic based on the incoming +message: + +[source,java] +---- +include::{examples-dir}/kafka/outbound/KafkaOutboundDynamicTopicExample.java[tags=code,indent=0] +---- + +== Acknowledgement + +Kafka acknowledgement can take times depending on the configuration. +Also, it stores in-memory the records that cannot be written. + +By default, the connector does wait for Kafka to acknowledge the record +to continue the processing (acknowledging the received `Message`). You +can disable this by setting the `waitForWriteCompletion` attribute to +`false`. + +Note that the `acks` attribute has a huge impact on the record +acknowledgement. + +If a record cannot be written, the message is `nacked`. + +== Back-pressure and inflight records + +The Kafka outbound connector handles back-pressure monitoring the number +of in-flight messages waiting to be written to the Kafka broker. The +number of in-flight messages is configured using the +`max-inflight-messages` attribute and defaults to 1024. + +The connector only sends that amount of messages concurrently. No other +messages will be sent until at least one in-flight message gets +acknowledged by the broker. Then, the connector writes a new message to +Kafka when one of the broker’s in-flight messages get acknowledged. Be +sure to configure Kafka’s `batch.size` and `linger.ms` accordingly. + +You can also remove the limit of inflight messages by setting +`max-inflight-messages` to `0`. However, note that the Kafka Producer +may block if the number of requests reaches +`max.in.flight.requests.per.connection`. + +== Handling serialization failures + +For Kafka producer client serialization failures are not recoverable, +thus the message dispatch is not retried. However, using schema +registries, serialization may intermittently fail to contact the +registry. In these cases you may need to apply a failure strategy for +the serializer. To achieve this, create a CDI bean implementing the link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-kafka/{javadoc-version}/io/smallrye/reactive/messaging/kafka/SerializationFailureHandler.html[`SerializationFailureHandler`] +interface: + +[source,java] +---- +@ApplicationScoped +@Identifier("failure-fallback") // Set the name of the failure handler +public class MySerializationFailureHandler + implements SerializationFailureHandler { // Specify the expected type + + @Override + public byte[] decorateSerialization(Uni serialization, String topic, + boolean isKey, String serializer, + Object data, Headers headers) { + return serialization + .onFailure().retry().atMost(3) + .await().atMost(Duration.ofMillis(200)); + } +} +---- + +The bean must be exposed with the `@Identifier` qualifier specifying the +name of the bean. Then, in the connector configuration, specify the +following attribute: + +* `mp.messaging.incoming.$channel.key-serialization-failure-handler`: +name of the bean handling serialization failures happening for the +record’s key +* `mp.messaging.incoming.$channel.value-serialization-failure-handler`: +name of the bean handling serialization failures happening for the +record’s value, + +The handler is called with the serialization action as a `Uni`, the +record’s topic, a boolean indicating whether the failure happened on a +key, the class name of the deserializer that throws the exception, the +corrupted data, the exception, and the records headers. Failure +strategies like retry, providing a fallback value or applying timeout +can be implemented. Note that the method must await on the result and +return the serialized byte array. Alternatively, the handler can +implement `decorateSerialization` method which can return a fallback +value. + +== Sending Cloud Events + +The Kafka connector supports https://cloudevents.io/[Cloud Events]. +The connector sends the outbound record as Cloud Events if: + +* the message metadata contains an +`io.smallrye.reactive.messaging.ce.OutgoingCloudEventMetadata` +instance, +* the channel configuration defines the `cloud-events-type` and +`cloud-events-source` attributes. + +You can create +`io.smallrye.reactive.messaging.ce.OutgoingCloudEventMetadata` instances +using: + +[source,java] +---- +include::{examples-dir}/kafka/outbound/KafkaCloudEventProcessor.java[indent=0] +---- + +If the metadata does not contain an id, the connector generates one +(random UUID). The `type` and `source` can be configured per message or +at the channel level using the `cloud-events-type` and +`cloud-events-source` attributes. Other attributes are also +configurable. + +The metadata can be contributed by multiple methods, however, you must +always retrieve the already existing metadata to avoid overriding the +values: + +[source,java] +---- +include::{examples-dir}/kafka/outbound/KafkaCloudEventMultipleProcessors.java[indent=0] +---- + +By default, the connector sends the Cloud Events using the _binary_ +format. You can write _structured_ Cloud Events by setting the +`cloud-events-mode` to `structured`. Only JSON is supported, so the +created records had its `content-type` header set to +`application/cloudevents{plus}json; charset=UTF-8` When using the +_structured_ mode, the value serializer must be set to +`org.apache.kafka.common.serialization.StringSerializer`, otherwise the +connector reports the error. In addition, in _structured_, the connector +maps the message’s payload to JSON, except for `String` passed directly. + +The record’s key can be set in the channel configuration (`key` +attribute), in the `OutgoingKafkaRecordMetadata` or using the +`partitionkey` Cloud Event attribute. + +[NOTE] +==== + +you can disable the Cloud Event support by setting the `cloud-events` +attribute to `false` + +==== + +== Using `ProducerRecord` + +Kafka built-in type +https://kafka.apache.org/26/javadoc/index.html?org/apache/kafka/clients/producer/ProducerRecord.html[ProducerRecord++<++K,V++>++] +can also be used for producing messages: + +[source,java] +---- +include::{examples-dir}/kafka/outbound/KafkaProducerRecordExample.java[tags=code,indent=0] +---- + +[WARNING] +==== + +This is an advanced feature. The `ProducerRecord` is sent to Kafka as +is. Any possible metadata attached through `Message++<++ProducerRecord++<++K, V++>>++` are ignored and lost. + +==== + +== Producer Interceptors + +Producer interceptors can be configured for Kafka producer clients using the standard `interceptor.classes` property. +Configured classes will be instantiated by the Kafka producer on client creation. + +Alternatively, producer clients can be configured with a CDI managed-bean implementing link:https://javadoc.io/doc/org.apache.kafka/kafka-clients/{javadoc-version}/org/apache/kafka/clients/producer/ProducerInterceptor.html[`org.apache.kafka.clients.producer.ProducerInterceptor`] interface: + +To achieve this, the bean must be exposed with the `@Identifier` qualifier specifying the name of the bean: + +[source,java] +---- +include::{examples-dir}/kafka/outbound/ProducerInterceptorBeanExample.java[indent=0] +---- + +Then, in the channel configuration, specify the following attribute: +`mp.messaging.incoming.$channel.interceptor-bean=my-producer-interceptor`. + +[WARNING] +==== + +The `onSend` method will be called on the producer _sending thread_ and `onAcknowledgement` will be called on the _Kafka producer I/O thread_. +In both cases if implementations are not fast, sending of messages could be delayed. + +==== + +== Configuration Reference + +include::{generated-dir}/smallrye-kafka-outgoing.adoc[indent=0] + +You can also pass any property supported by the underlying https://kafka.apache.org/documentation/#producerconfigs[Kafka +producer]. + +For example, to configure the `batch.size` property, use: + +[source,properties] +---- +mp.messaging.outgoing.[channel].batch.size=32768 +---- + +Some producer client properties are configured to sensible default +values: + +If not set, `reconnect.backoff.max.ms` is set to `10000` to avoid high +load on disconnection. + +If not set, `key.serializer` is set to +`org.apache.kafka.common.serialization.StringSerializer`. + +If not set, producer `client.id` is generated as +`kafka-producer-++[++channel++]++`. diff --git a/content/docs/smallrye-reactive-messaging/3/mqtt/client-customization.adoc b/content/docs/smallrye-reactive-messaging/3/mqtt/client-customization.adoc new file mode 100644 index 000000000..768513593 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/mqtt/client-customization.adoc @@ -0,0 +1,21 @@ += Customizing the underlying MQTT client +:version-root: ../ +include::{version-root}_attributes.adoc[] + +You can customize the underlying MQTT Client configuration by +_producing_ an instance of +`io.smallrye.reactive.messaging.mqtt.session.MqttClientSessionOptions`: + +[source,java] +---- +include::{examples-dir}/mqtt/customization/ClientProducers.java[tags=named,indent=0] +---- + +This instance is retrieved and used to configure the client used by the +connector. You need to indicate the name of the client using the +`client-options-name` attribute: + +[source,properties] +---- +mp.messaging.incoming.prices.client-options-name=my-options +---- diff --git a/content/docs/smallrye-reactive-messaging/3/mqtt/mqtt.adoc b/content/docs/smallrye-reactive-messaging/3/mqtt/mqtt.adoc new file mode 100644 index 000000000..afa02d06f --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/mqtt/mqtt.adoc @@ -0,0 +1,44 @@ += MQTT Connector +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The MQTT connector adds support for MQTT to Reactive Messaging. + +It lets you receive messages from an MQTT server or broker as well as +send MQTT messages. The MQTT connector is based on the https://vertx.io/docs/vertx-mqtt/java/#_vert_x_mqtt_client[Vert.x MQTT +Client]. + +== Introduction + +http://mqtt.org/[MQTT] is a machine-to-machine (M2M)/"Internet of +Things" connectivity protocol. It was designed as an extremely +lightweight publish/subscribe messaging transport. + +The MQTT Connector allows consuming messages from MQTT as well as +sending MQTT messages. + +== Using the MQTT connector + +To you the MQTT Connector, add the following dependency to your project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.reactive + smallrye-reactive-messaging-mqtt + {project-version} + +---- + +The connector name is: `smallrye-mqtt`. + +So, to indicate that a channel is managed by this connector you need: + +[source,properties] +---- +# Inbound +mp.messaging.incoming.[channel-name].connector=smallrye-mqtt + +# Outbound +mp.messaging.outgoing.[channel-name].connector=smallrye-mqtt +---- diff --git a/content/docs/smallrye-reactive-messaging/3/mqtt/receiving-mqtt-messages.adoc b/content/docs/smallrye-reactive-messaging/3/mqtt/receiving-mqtt-messages.adoc new file mode 100644 index 000000000..5a1e176bb --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/mqtt/receiving-mqtt-messages.adoc @@ -0,0 +1,114 @@ += Receiving messages from MQTT +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The MQTT Connector connects to a MQTT broker or router, and forward the +messages to the Reactive Messaging application. It maps each of them +into Reactive Messaging `Messages`. + +== Example + +Let’s imagine you have a MQTT server/broker running, and accessible +using the `mqtt:1883` address (by default it would use +`localhost:1883`). Configure your application to receive MQTT messages +on the `prices` channel as follows: + +[source,properties] +---- +mp.messaging.incoming.prices.connector=smallrye-mqtt # <1> +mp.messaging.incoming.prices.host=mqtt # <2> +mp.messaging.incoming.prices.port=1883 # <3> +---- + +[arabic] +. Sets the connector for the `prices` channel +. Configure the broker/server host name. +. Configure the broker/server port. 1883 is the default. + +[NOTE] +==== + +You don’t need to set the MQTT topic. By default, it uses the channel +name (`prices`). You can configure the `topic` attribute to override it. + +==== + +[NOTE] +==== + +It is generally recommended to set the `client-id`. By default, the connector is generating a unique `client-id`. + +==== + +[IMPORTANT] +==== + +Message coming from MQTT have a `byte++[]++` payload. + +==== + +Then, your application receives `Message++<++byte++[]>++`. You can consume the +payload directly: + +[source,java] +---- +include::{examples-dir}/mqtt/inbound/MqttPriceConsumer.java[indent=0] +---- + +Or, you can retrieve the `Message++<++byte++[]>++`: + +[source,java] +---- +include::{examples-dir}/mqtt/inbound/MqttPriceMessageConsumer.java[indent=0] +---- + +The inbound topic can use the https://mosquitto.org/man/mqtt-7.html[MQTT +wildcards] (`{plus}` and `#`). + +== Deserialization + +The MQTT Connector does not handle the deserialization and creates a +`Message++<++byte++[]>++`. + +== Inbound Metadata + +The MQTT connector does not provide inbound metadata. + +== Failure Management + +If a message produced from a MQTT message is _nacked_, a failure +strategy is applied. The MQTT connector supports 3 strategies: + +* `fail` - fail the application, no more MQTT messages will be +processed. (default) The offset of the record that has not been +processed correctly is not committed. +* `ignore` - the failure is logged, but the processing continue. + +== Configuration Reference + +include::{generated-dir}/smallrye-mqtt-incoming.adoc[indent=0] + +The MQTT connector is based on the https://vertx.io/docs/vertx-mqtt/java/#_vert_x_mqtt_client[Vert.x MQTT +client]. So +you can pass any attribute supported by this client. + +[IMPORTANT] +==== + +A single instance of `MqttClient` and a single connection is used for +each `host` / `port` / `server-name` / `client-id`. This client is +reused for both the inbound and outbound connectors. + +==== + +[IMPORTANT] +==== + +Using `auto-clean-session=false` the MQTT Connector send Subscribe requests +to the broken only if a Persistent Session is not present (like on the first +connection). This means that if a Session is already present (maybe for a +previous run) and you add a new incoming channel, this will not be subscribed. +Beware to check always the subscription present on Broker when use +`auto-clean-session=false`. + +==== diff --git a/content/docs/smallrye-reactive-messaging/3/mqtt/sending-messages-to-mqtt.adoc b/content/docs/smallrye-reactive-messaging/3/mqtt/sending-messages-to-mqtt.adoc new file mode 100644 index 000000000..bc8017414 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/mqtt/sending-messages-to-mqtt.adoc @@ -0,0 +1,94 @@ += Sending messages to MQTT +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The MQTT Connector can write Reactive Messaging `Messages` as MQTT +Message. + +== Example + +Let’s imagine you have a MQTT server/broker running, and accessible +using the `mqtt:1883` address (by default it would use +`localhost:1883`). Configure your application to write the messages from +the `prices` channel into a MQTT Messages as follows: + +[source,properties] +---- +mp.messaging.outgoing.prices.type=smallrye-mqtt +mp.messaging.outgoing.prices.host=mqtt +mp.messaging.outgoing.prices.port=1883 +---- + +[arabic] +. Sets the connector for the `prices` channel +. Configure the broker/server host name. +. Configure the broker/server port. 1883 is the default. + +[NOTE] +==== + +You don’t need to set the MQTT topic. By default, it uses the channel +name (`prices`). You can configure the `topic` attribute to override it. +NOTE: It is generally recommended to set the `client-id`. By default, +the connector is generating a unique `client-id`. + +==== + +Then, your application must send `Message++<++Double++>++` to the `prices` +channel. It can use `double` payloads as in the following snippet: + +[source,java] +---- +include::{examples-dir}/mqtt/outbound/MqttPriceProducer.java[indent=0] +---- + +Or, you can send `Message++<++Double++>++`: + +[source,java] +---- +include::{examples-dir}/mqtt/outbound/MqttPriceMessageProducer.java[indent=0] +---- + +== Serialization + +The `Message` sent to MQTT can have various payload types: + +* https://vertx.io/docs/apidocs/io/vertx/core/json/JsonObject.html[`JsonObject`]: +JSON string encoded as `byte++[]++` +* https://vertx.io/docs/apidocs/io/vertx/core/json/JsonArray.html[`JsonArray`]: +JSON string encoded as `byte++[]++` +* `java.lang.String` and Java primitive types: `toString` encoded as +`byte++[]++` +* `byte++[]++` +* complex objects: The objects are encoded to JSON and passed as +`byte++[]++` + +== Outbound Metadata + +The MQTT connector does not provide outbound metadata. + +== Acknowledgement + +MQTT acknowledgement depends on the QoS level. The message is +acknowledged when the broker indicated the successful reception of the +message (or immediately if the level of QoS does not support +acknowledgment). + +If a MQTT message cannot be sent to the broker, the message is `nacked`. + +== Configuration Reference + +include::{generated-dir}/smallrye-mqtt-outgoing.adoc[indent=0] + +The MQTT connector is based on the https://vertx.io/docs/vertx-mqtt/java/#_vert_x_mqtt_client[Vert.x MQTT +client]. So +you can pass any attribute supported by this client. + +[IMPORTANT] +==== + +A single instance of `MqttClient` and a single connection is used for +each `host` / `port` / `server-name` / `client-id`. This client is +reused for both the inbound and outbound connectors. + +==== diff --git a/content/docs/smallrye-reactive-messaging/3/rabbitmq/rabbitmq-client-customization.adoc b/content/docs/smallrye-reactive-messaging/3/rabbitmq/rabbitmq-client-customization.adoc new file mode 100644 index 000000000..260871fca --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/rabbitmq/rabbitmq-client-customization.adoc @@ -0,0 +1,20 @@ += Customizing the underlying RabbitMQ client +:version-root: ../ +include::{version-root}_attributes.adoc[] + +You can customize the underlying RabbitMQ Client configuration by +_producing_ an instance of +https://vertx.io/docs/apidocs/io/vertx/rabbitmq/RabbitMQOptions.html[`RabbitMQOptions`]: + +[source,java] +---- +include::{examples-dir}/rabbitmq/customization/RabbitMQProducers.java[tags=named,indent=0] +---- + +This instance is retrieved and used to configure the client used by the +connector. You need to indicate the name of the client using the +`client-options-name` attribute: + +.... +mp.messaging.incoming.prices.client-options-name=my-named-options +.... diff --git a/content/docs/smallrye-reactive-messaging/3/rabbitmq/rabbitmq-cloud.adoc b/content/docs/smallrye-reactive-messaging/3/rabbitmq/rabbitmq-cloud.adoc new file mode 100644 index 000000000..b1df1aed5 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/rabbitmq/rabbitmq-cloud.adoc @@ -0,0 +1,52 @@ += Connecting to managed instances +:version-root: ../ +include::{version-root}_attributes.adoc[] + +This section describes the connector configuration to use managed +RabbitMQ instances (hosted on the Cloud). + +== Cloud AMQP + +To connect to an instance of RabbitMQ hosted on https://www.cloudamqp.com/[Cloud +AMQP], use the following configuration: + +[source,properties] +---- +rabbitmq-host=host-name +rabbitmq-port=5671 +rabbitmq-username=user-name +rabbitmq-password=password +rabbitmq-virtual-host=user-name +rabbitmq-ssl=true +---- + +You can extract the values from the `AMQPS` url displayed on the +administration portal: + +.... +amqps://user-name:password@host/user-name +.... + +== Amazon MQ + +https://aws.amazon.com/amazon-mq/[Amazon MQ] can host RabbitMQ brokers +(as well as AMQP 1.0 brokers). To connect to a RabbitMQ instance hosted +on Amazon MQ, use the following configuration: + +[source,properties] +---- +rabbitmq-host=host-name +rabbitmq-port=5671 +rabbitmq-username=user-name +rabbitmq-password=password +rabbitmq-ssl=true +---- + +You can extract the host value from the `AMQPS` url displayed on the +administration console: + +.... +amqps://foobarbaz.mq.us-east-2.amazonaws.com:5671 +.... + +The username and password are configured during the broker creation. diff --git a/content/docs/smallrye-reactive-messaging/3/rabbitmq/rabbitmq-health.adoc b/content/docs/smallrye-reactive-messaging/3/rabbitmq/rabbitmq-health.adoc new file mode 100644 index 000000000..854806af4 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/rabbitmq/rabbitmq-health.adoc @@ -0,0 +1,14 @@ += Health reporting +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The RabbitMQ connector reports the readiness and liveness of each +channel managed by the connector. + +On the inbound side (receiving messages from RabbitMQ), the check +verifies that the receiver is connected to the broker. + +On the outbound side (sending records to RabbitMQ), the check verifies +that the sender is not disconnected from the broker; the sender _may_ +still be in an initialized state (connection not yet attempted), but +this is regarded as live/ready. diff --git a/content/docs/smallrye-reactive-messaging/3/rabbitmq/rabbitmq.adoc b/content/docs/smallrye-reactive-messaging/3/rabbitmq/rabbitmq.adoc new file mode 100644 index 000000000..35bb40e7c --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/rabbitmq/rabbitmq.adoc @@ -0,0 +1,57 @@ += RabbitMQ Connector +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The RabbitMQ Connector adds support for RabbitMQ to Reactive Messaging, +based on the AMQP 0-9-1 Protocol Specification. + +Advanced Message Queuing Protocol 0-9-1 (https://www.rabbitmq.com/resources/specs/amqp0-9-1.pdf[AMQP +0-9-1]) is an +open standard for passing business messages between applications or +organizations. + +With this connector, your application can: + +* receive messages from a RabbitMQ queue +* send messages to a RabbitMQ exchange + +The RabbitMQ connector is based on the https://vertx.io/docs/vertx-rabbitmq-client/java/[Vert.x RabbitMQ +Client]. + +[IMPORTANT] +==== + +The *AMQP connector* supports the AMQP 1.0 protocol, which is very +different from AMQP 0-9-1. You _can_ use the AMQP connector with +RabbitMQ provided that the latter has the https://github.com/rabbitmq/rabbitmq-amqp1.0/blob/v3.7.x/README.md[AMQP 1.0 +Plugin] +installed, albeit with reduced functionality. + +==== + +== Using the RabbitMQ connector + +To use the RabbitMQ Connector, add the following dependency to your +project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.reactive + smallrye-reactive-messaging-rabbitmq + {project-version} + +---- + +The connector name is: `smallrye-rabbitmq`. + +So, to indicate that a channel is managed by this connector you need: + +[source,properties] +---- +# Inbound +mp.messaging.incoming.[channel-name].connector=smallrye-rabbitmq + +# Outbound +mp.messaging.outgoing.[channel-name].connector=smallrye-rabbitmq +---- diff --git a/content/docs/smallrye-reactive-messaging/3/rabbitmq/receiving-messages-from-rabbitmq.adoc b/content/docs/smallrye-reactive-messaging/3/rabbitmq/receiving-messages-from-rabbitmq.adoc new file mode 100644 index 000000000..8fe8d09be --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/rabbitmq/receiving-messages-from-rabbitmq.adoc @@ -0,0 +1,265 @@ += Receiving messages from RabbitMQ +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The RabbitMQ connector lets you retrieve messages from a https://www.rabbitmq.com/[RabbitMQ +broker]. The RabbitMQ connector retrieves +_RabbitMQ Messages_ and maps each of them into Reactive Messaging +`Messages`. + +[NOTE] +==== + +In this context, the reactive messaging concept of a _Channel_ is +realised as a https://www.rabbitmq.com/queues.html[RabbitMQ Queue]. + +==== + +== Example + +Let’s imagine you have a RabbitMQ broker running, and accessible using +the `rabbitmq:5672` address (by default it would use `localhost:5672`). +Configure your application to receive RabbitMQ Messages on the `prices` +channel as follows: + +[source,properties] +---- +rabbitmq-host=rabbitmq # <1> +rabbitmq-port=5672 # <2> +rabbitmq-username=my-username # <3> +rabbitmq-password=my-password # <4> + +mp.messaging.incoming.prices.connector=smallrye-rabbitmq # <5> +mp.messaging.incoming.prices.queue.name=my-queue # <6> +mp.messaging.incoming.prices.routing-keys=urgent # <7> +---- + +[arabic] +. Configures the broker/router host name. You can do it per channel +(using the `host` attribute) or globally using `rabbitmq-host`. +. Configures the broker/router port. You can do it per channel (using +the `port` attribute) or globally using `rabbitmq-port`. The default +is 5672. +. Configures the broker/router username if required. You can do it per +channel (using the `username` attribute) or globally using +`rabbitmq-username`. +. Configures the broker/router password if required. You can do it per +channel (using the `password` attribute) or globally using +`rabbitmq-password`. +. Instructs the `prices` channel to be managed by the RabbitMQ +connector. +. Configures the RabbitMQ queue to read messages from. +. Configures the binding between the RabbitMQ exchange and the +RabbitMQ queue using a routing key. The default is `#` (all messages +will be forwarded from the exchange to the queue) but in general +this can be a comma-separated list of one or more keys. + +Then, your application receives `Message++<++String++>++`. You can consume the +payload directly: + +[source,java] +---- +include::{examples-dir}/rabbitmq/inbound/RabbitMQPriceConsumer.java[indent=0] +---- + +Or, you can retrieve the `Message++<++String++>++`: + +[source,java] +---- +include::{examples-dir}/rabbitmq/inbound/RabbitMQPriceMessageConsumer.java[indent=0] +---- + +[NOTE] +==== + +Whether you need to explicitly acknowledge the message depends on the +`auto-acknowledgement` channel setting; if that is set to `true` then +your message will be automatically acknowledged on receipt. + +==== + +== Deserialization + +The connector converts incoming RabbitMQ Messages into Reactive +Messaging `Message++<++T++>++` instances. The payload type `T` depends on the +value of the RabbitMQ received message Envelope `content++_++type` and +`content++_++encoding` properties. + +[width="100%",cols="7%,6%,87%",options="header",] +|=== +|content++_++encoding |content++_++type |Type +|_Value present_ |_n/a_ |`byte++[]++` +|_No value_ |`text/plain` |`String` +|_No value_ |`application/json` |a JSON element which can be a https://vertx.io/docs/apidocs/io/vertx/core/json/JsonArray.html[`JsonArray`], https://vertx.io/docs/apidocs/io/vertx/core/json/JsonObject.html[`JsonObject`], `String`, ... if the buffer contains an array, object, string,... +|_No value_ |_Anything else_ |`byte++[]++` +|=== + +If you send objects with this RabbitMQ connector (outbound connector), +they are encoded as JSON and sent with `content++_++type` set to +`application/json`. You can receive this payload using (Vert.x) JSON +Objects, and then map it to the object class you want: + +[source,java] +---- +@ApplicationScoped +public static class Generator { + + @Outgoing("to-rabbitmq") + public Multi prices() { // <1> + AtomicInteger count = new AtomicInteger(); + return Multi.createFrom().ticks().every(Duration.ofMillis(1000)) + .map(l -> new Price().setPrice(count.incrementAndGet())) + .onOverflow().drop(); + } + +} + +@ApplicationScoped +public static class Consumer { + + List prices = new CopyOnWriteArrayList<>(); + + @Incoming("from-rabbitmq") + public void consume(JsonObject p) { // <2> + Price price = p.mapTo(Price.class); // <3> + prices.add(price); + } + + public List list() { + return prices; + } +} +---- + +[arabic] +. The `Price` instances are automatically encoded to JSON by the +connector +. You can receive it using a `JsonObject` +. Then, you can reconstruct the instance using the `mapTo` method + +== Inbound Metadata + +Messages coming from RabbitMQ contain an instance of link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-rabbitmq/{javadoc-version}/io/smallrye/reactive/messaging/rabbitmq/IncomingRabbitMQMetadata.html[`IncomingRabbitMQMetadata`] +in the metadata. + +RabbitMQ message headers can be accessed from the metadata either by +calling `getHeader(String header, Class++<++T++>++ type)` to retrieve a single +header value. or `getHeaders()` to get a map of all header values. + +[source,java] +---- +include::{examples-dir}/rabbitmq/inbound/RabbitMQMetadataExample.java[tags=code,indent=0] +---- + +The type `++<++T++>++` of the header value depends on the RabbitMQ type used for +the header: + +[cols=",",options="header",] +|=== +|RabbitMQ Header Type |T +|String |`String` +|Boolean |`Boolean` +|Number |`Number` +|List |`java.util.List` +|=== + +== Acknowledgement + +When a Reactive Messaging Message associated with a RabbitMQ Message is +acknowledged, it informs the broker that the message has been +_accepted_. + +Whether you need to explicitly acknowledge the message depends on the +`auto-acknowledgement` setting for the channel; if that is set to `true` +then your message will be automatically acknowledged on receipt. + +== Failure Management + +If a message produced from a RabbitMQ message is _nacked_, a failure +strategy is applied. The RabbitMQ connector supports three strategies, +controlled by the `failure-strategy` channel setting: + +* `fail` - fail the application; no more RabbitMQ messages will be +processed. The RabbitMQ message is marked as rejected. +* `accept` - this strategy marks the RabbitMQ message as accepted. The +processing continues ignoring the failure. +* `reject` - this strategy marks the RabbitMQ message as rejected +(default). The processing continues with the next message. + +== Configuration Reference + +include::{generated-dir}/smallrye-rabbitmq-incoming.adoc[indent=0] + +To use an existing _queue_, you need to configure the `queue.name` +attribute. + +For example, if you have a RabbitMQ broker already configured with a +queue called `peopleQueue` that you wish to read messages from, you need +the following configuration: + +[source,properties] +---- +mp.messaging.incoming.people.connector=smallrye-rabbitmq +mp.messaging.incoming.people.queue.name=peopleQueue +---- + +If you want RabbitMQ to create the queue for you but bind it to an +existing topic exchange `people`, you need the following configuration: + +[source,properties] +---- +mp.messaging.incoming.people.connector=smallrye-rabbitmq +mp.messaging.incoming.people.queue.name=peopleQueue +mp.messaging.incoming.people.queue.declare=true +---- + +[NOTE] +==== + +In the above the channel name `people` is implicitly assumed to be the +name of the exchange; if this is not the case you would need to name the +exchange explicitly using the `exchange.name` property. + +==== + +[NOTE] +==== + +The connector supports RabbitMQ's "Server-named Queues" feature to create +an exclusive, auto-deleting, non-durable and randomly named queue. To +enable this feature you set the queue name to exactly `(server.auto)`. +Using this name not only enables the server named queue feature but also +automatically makes ths queue exclusive, auto-deleting, and non-durable; +therefore ignoring any values provided for the `exclusive`, `auto-delete` +and `durable` options. + +==== + +If you want RabbitMQ to create the `people` exchange, queue and binding, +you need the following configuration: + +[source,properties] +---- +mp.messaging.incoming.people.connector=smallrye-rabbitmq +mp.messaging.incoming.people.exchange.declare=true +mp.messaging.incoming.people.queue.name=peopleQueue +mp.messaging.incoming.people.queue.declare=true +mp.messaging.incoming.people.queue.routing-keys=tall,short +---- + +In the above we have used an explicit list of routing keys rather than +the default (`#`). Each component of the list creates a separate binding +between the queue and the exchange, so in the case above we would have +two bindings; one based on a routing key of `tall`, the other based on +one of `short`. + +[NOTE] +==== + +The default value of `routing-keys` is `#` (indicating a match against +all possible routing keys) which is only appropriate for _topic_ +Exchanges. If you are using other types of exchange and/or need to +declare queue bindings, you’ll need to supply a valid value for the +exchange in question. + +==== diff --git a/content/docs/smallrye-reactive-messaging/3/rabbitmq/sending-messages-to-rabbitmq.adoc b/content/docs/smallrye-reactive-messaging/3/rabbitmq/sending-messages-to-rabbitmq.adoc new file mode 100644 index 000000000..347cf6b92 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/3/rabbitmq/sending-messages-to-rabbitmq.adoc @@ -0,0 +1,170 @@ += Sending messages to RabbitMQ +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The RabbitMQ connector can write Reactive Messaging `Messages` as +RabbitMQ Messages. + +[NOTE] +==== + +In this context, the reactive messaging concept of a _Channel_ is +realised as a https://www.rabbitmq.com/tutorials/amqp-concepts.html#exchanges[RabbitMQ +Exchange]. + +==== + +== Example + +Let’s imagine you have a RabbitMQ broker running, and accessible using +the `rabbitmq:5672` address (by default it would use `localhost:5672`). +Configure your application to send the messages from the `prices` +channel as a RabbitMQ Message as follows: + +.... +rabbitmq-host=rabbitmq # <1> +rabbitmq-port=5672 # <2> +rabbitmq-username=my-username # <3> +rabbitmq-password=my-password # <4> + +mp.messaging.outgoing.prices.connector=smallrye-rabbitmq # <5> +mp.messaging.outgoing.prices.default-routing-key=normal # <6> +.... + +[arabic] +. Configures the broker/router host name. You can do it per channel +(using the `host` attribute) or globally using `rabbitmq-host` +. Configures the broker/router port. You can do it per channel (using +the `port` attribute) or globally using `rabbitmq-port`. The default +is `5672`. +. Configures the broker/router username if required. You can do it per +channel (using the `username` attribute) or globally using +`rabbitmq-username`. +. Configures the broker/router password if required. You can do it per +channel (using the `password` attribute) or globally using +`rabbitmq-password`. +. Instructs the `prices` channel to be managed by the RabbitMQ +connector +. Supplies the default routing key to be included in outbound +messages; this will be if the "raw payload" form of message sending +is used (see below). + +[NOTE] +==== + +You don’t need to set the RabbitMQ exchange name. By default, it uses +the channel name (`prices`) as the name of the exchange to send messages +to. You can configure the `exchange.name` attribute to override it. + +==== + +Then, your application can send `Message++<++Double++>++` to the prices channel. +It can use `double` payloads as in the following snippet: + +[source,java] +---- +include::{examples-dir}/rabbitmq/outbound/RabbitMQPriceProducer.java[indent=0] +---- + +Or, you can send `Message++<++Double++>++`, which affords the opportunity to +explicitly specify metadata on the outgoing message: + +[source,java] +---- +include::{examples-dir}/rabbitmq/outbound/RabbitMQPriceMessageProducer.java[indent=0] +---- + +== Serialization + +When sending a `Message++<++T++>++`, the connector converts the message into a +RabbitMQ Message. The payload is converted to the RabbitMQ Message body. + +[width="100%",cols="59%,41%",options="header",] +|=== +|T |RabbitMQ Message Body +|primitive types or `UUID`/`String` |String value with `content++_++type` set to `text/plain` +|https://vertx.io/docs/apidocs/io/vertx/core/json/JsonObject.html[`JsonObject`] or https://vertx.io/docs/apidocs/io/vertx/core/json/JsonArray.html[`JsonArray`] |Serialized String payload with `content++_++type` set to `application/json` +|`io.vertx.mutiny.core.buffer.Buffer` |Binary content, with `content++_++type` set to `application/octet-stream` +|`byte++[]++` |Binary content, with content++_++type set to `application/octet-stream` +|Any other class |The payload is converted to JSON (using a Json Mapper) then serialized with `content++_++type` set to `application/json` +|=== + +If the message payload cannot be serialized to JSON, the message is +_nacked_. + +== Outbound Metadata + +When sending `Messages`, you can add an instance of link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-rabbitmq/{javadoc-version}/io/smallrye/reactive/messaging/rabbitmq/OutgoingRabbitMQMetadata.html[`OutgoingRabbitMQMetadata`] +to influence how the message is handled by RabbitMQ. For example, you +can configure the routing key, timestamp and headers: + +[source,java] +---- +include::{examples-dir}/rabbitmq/outbound/RabbitMQOutboundMetadataExample.java[tags=code,indent=0] +---- + +== Acknowledgement + +By default, the Reactive Messaging `Message` is acknowledged when the +broker acknowledges the message. + +== Configuration Reference + +include::{generated-dir}/smallrye-rabbitmq-outgoing.adoc[indent=0] + +== Using existing destinations + +To use an existing _exchange_, you may need to configure the +`exchange.name` attribute. + +For example, if you have a RabbitMQ broker already configured with an +exchange called `people` that you wish to send messages to, you need the +following configuration: + +[source,properties] +---- +mp.messaging.outgoing.people.connector=smallrye-rabbitmq +---- + +You would need to configure the `exchange.name` attribute, if the +exchange name were not the channel name: + +[source,properties] +---- +mp.messaging.outgoing.people-out.connector=smallrye-rabbitmq +mp.messaging.outgoing.people-out.exchange.name=people +---- + +If you want RabbitMQ to create the `people` exchange, you need the +following configuration: + +[source,properties] +---- +mp.messaging.outgoing.people-out.connector=smallrye-amqp +mp.messaging.outgoing.people-out.exchange.name=people +mp.messaging.outgoing.people-out.exchange.declare=true +---- + +[NOTE] +==== + +The above example will create a `topic` exchange and use an empty +default `routing-key` (unless overridden programatically using outgoing +metadata for the message). If you want to create a different type of +exchange or have a different default routing key, then the +`exchange.type` and `default-routing-key` properties need to be +explicitly specified. + +==== + +== Sending to specific queues via the default exchange + +To send a message to a specific queue (usually a reply queue), +you have to configure the default exchange as an outgoing channel and set the name of the queue as routing key in the message metadata. +The name of the exchange needs to be set to `""`. + +[source,properties] +---- +mp.messaging.outgoing.channel-name-for-default-exchange.connector=smallrye-rabbitmq +mp.messaging.outgoing.channel-name-for-default-exchange.exchange.name="" +---- diff --git a/content/docs/smallrye-reactive-messaging/4/_attributes.adoc b/content/docs/smallrye-reactive-messaging/4/_attributes.adoc new file mode 100644 index 000000000..4807c6e5d --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_attributes.adoc @@ -0,0 +1,26 @@ +:project-version: 4.37.1 +// Attributes shared by every page. smallrye.io overrides the ones it needs when it publishes. +// Pages in subdirectories set :version-root: (../ per level) before including this file. +ifndef::version-root[:version-root:] +:idprefix: +:idseparator: - +:sectanchors: +:examples-dir: {version-root}_examples +:partialsdir: {version-root}_partials +:project-name: SmallRye Reactive Messaging +:javadoc-version: latest +:version: 4.37.1 +:weld-version: 6.0.4.Final +:smallrye-config-version: 3.18.1 +:mutiny-version: 3.1.0 +:camel-version: 4.21.0 +:spec-version: 3.0.1 +:generated-dir: {version-root}_generated + +// Added by smallrye.io (tools/docs/Docs.java sync) +:javadoc-version: 4.37.1 +:relfileprefix: /docs/smallrye-reactive-messaging/4/ +:imagesdir: /docs/smallrye-reactive-messaging/4 +:docs-project: smallrye-reactive-messaging +:docs-version: 4 +:docs-ref: 4.37.1 diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/ack/AutoNack.java b/content/docs/smallrye-reactive-messaging/4/_examples/ack/AutoNack.java new file mode 100644 index 000000000..bdb5a371a --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/ack/AutoNack.java @@ -0,0 +1,55 @@ +package ack; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Uni; + +public class AutoNack { + + // tag::auto-nack[] + @Incoming("data") + @Outgoing("out") + public String process(String s) { + if (s.equalsIgnoreCase("b")) { + // Throwing an exception triggers a nack + throw new IllegalArgumentException("b"); + } + + if (s.equalsIgnoreCase("e")) { + // Returning null would skip the message (it will be acked) + return null; + } + + return s.toUpperCase(); + } + + @Incoming("data") + @Outgoing("out") + public Uni processAsync(String s) { + if (s.equalsIgnoreCase("a")) { + // Returning a failing Uni triggers a nack + return Uni.createFrom().failure(new Exception("a")); + } + + if (s.equalsIgnoreCase("b")) { + // Throwing an exception triggers a nack + throw new IllegalArgumentException("b"); + } + + if (s.equalsIgnoreCase("e")) { + // Returning null would skip the message (it will be acked not nacked) + return Uni.createFrom().nullItem(); + } + + if (s.equalsIgnoreCase("f")) { + // returning `null` is invalid for method returning Unis, the message is nacked + return null; + } + + return Uni.createFrom().item(s.toUpperCase()); + } + + // end::auto-nack[] + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/ack/MessageAckExamples.java b/content/docs/smallrye-reactive-messaging/4/_examples/ack/MessageAckExamples.java new file mode 100644 index 000000000..5b4ddf459 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/ack/MessageAckExamples.java @@ -0,0 +1,52 @@ +package ack; + +import java.util.concurrent.CompletableFuture; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +public class MessageAckExamples { + + // tag::with-payload[] + @Incoming("in") + @Outgoing("out") + public Message process(Message in) { + // The acknowledgement is forwarded, when the consumer + // acknowledges the message, `in` will be acknowledged + return in.withPayload(in.getPayload() + 1); + } + // end::with-payload[] + + public void creation() { + // tag::message-creation[] + Message message = Message.of("hello", () -> { + // called when the consumer acknowledges the message + + // return a CompletionStage completed when the + // acknowledgment of the created message is + // completed. + // For immediate ack use: + return CompletableFuture.completedFuture(null); + + }); + // end::message-creation[] + } + + // tag::process[] + @Incoming("in") + @Outgoing("out") + public Message processAndProduceNewMessage(Message in) { + // The acknowledgement is forwarded, when the consumer + // acknowledges the message, `in` will be acknowledged + return Message.of(in.getPayload() + 1, + () -> { + // Called when the consumer acknowledges the message + // ... + // Don't forget to acknowledge the incoming message: + return in.ack(); + }); + } + // end::process[] + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/ack/NackOnEmitter.java b/content/docs/smallrye-reactive-messaging/4/_examples/ack/NackOnEmitter.java new file mode 100644 index 000000000..ffdbc70bc --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/ack/NackOnEmitter.java @@ -0,0 +1,27 @@ +package ack; + +import java.util.concurrent.CompletionStage; + +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Emitter; + +public class NackOnEmitter { + + // tag::emitter[] + @Inject + @Channel("data") + Emitter emitter; + + public void emitPayload() { + CompletionStage completionStage = emitter.send("hello"); + completionStage.whenComplete((acked, nacked) -> { + if (nacked != null) { + // the processing has failed + } + }); + } + // end::emitter[] + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/ack/PreAckExamples.java b/content/docs/smallrye-reactive-messaging/4/_examples/ack/PreAckExamples.java new file mode 100644 index 000000000..04b336bde --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/ack/PreAckExamples.java @@ -0,0 +1,20 @@ +package ack; + +import org.eclipse.microprofile.reactive.messaging.Acknowledgment; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +public class PreAckExamples { + + // tag::pre[] + @Incoming("in") + @Outgoing("out") + @Acknowledgment(Acknowledgment.Strategy.PRE_PROCESSING) + public String process(String input) { + // The message wrapping the payload is already acknowledged + // The default would have waited the produced message to be + // acknowledged + return input.toUpperCase(); + } + // end::pre[] +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/ack/StreamAckExamples.java b/content/docs/smallrye-reactive-messaging/4/_examples/ack/StreamAckExamples.java new file mode 100644 index 000000000..ea47f34b4 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/ack/StreamAckExamples.java @@ -0,0 +1,42 @@ +package ack; + +import java.util.concurrent.Flow.Publisher; + +import org.eclipse.microprofile.reactive.messaging.Acknowledgment; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +public class StreamAckExamples { + + // tag::message[] + @Incoming("in") + @Outgoing("out") + public Publisher> transform(Multi> stream) { + return stream + .map(message -> message.withPayload(message.getPayload().toUpperCase())); + } + // end::message[] + + // tag::payload[] + @Incoming("in") + @Outgoing("out") + public Publisher transformPayload(Multi stream) { + return stream + // The incoming messages are already acknowledged + .map(String::toUpperCase); + } + // end::payload[] + + // tag::payload-to-multi[] + @Incoming("in") + @Outgoing("out") + // Defaults to pre-processing, but post-processing is also supported + @Acknowledgment(Acknowledgment.Strategy.POST_PROCESSING) + public Multi transformPayload(String one) { + return Multi.createFrom().items(one, one); + } + // end::payload-to-multi[] +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/amqp/customization/ClientProducers.java b/content/docs/smallrye-reactive-messaging/4/_examples/amqp/customization/ClientProducers.java new file mode 100644 index 000000000..0e107cc3c --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/amqp/customization/ClientProducers.java @@ -0,0 +1,34 @@ +package amqp.customization; + +import jakarta.enterprise.inject.Produces; + +import io.smallrye.common.annotation.Identifier; +import io.vertx.amqp.AmqpClientOptions; +import io.vertx.core.net.PemKeyCertOptions; +import io.vertx.core.net.PemTrustOptions; + +public class ClientProducers { + + // tag::named[] + @Produces + @Identifier("my-options") + public AmqpClientOptions getNamedOptions() { + // You can use the produced options to configure the TLS connection + PemKeyCertOptions keycert = new PemKeyCertOptions() + .addCertPath("./tls/tls.crt") + .addKeyPath("./tls/tls.key"); + PemTrustOptions trust = new PemTrustOptions().addCertPath("./tlc/ca.crt"); + + return new AmqpClientOptions() + .setSsl(true) + .setPemKeyCertOptions(keycert) + .setPemTrustOptions(trust) + .addEnabledSaslMechanism("EXTERNAL") + .setHostnameVerificationAlgorithm("") // Disable hostname verification + .setConnectTimeout(30000) + .setReconnectInterval(5000) + .setContainerId("my-container"); + } + // end::named[] + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/amqp/customization/MyCbsTokenProvider.java b/content/docs/smallrye-reactive-messaging/4/_examples/amqp/customization/MyCbsTokenProvider.java new file mode 100644 index 000000000..45346773f --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/amqp/customization/MyCbsTokenProvider.java @@ -0,0 +1,32 @@ +package amqp.customization; + +import java.time.OffsetDateTime; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.amqp.AmqpConnectorCommonConfiguration; +import io.smallrye.reactive.messaging.amqp.cbs.CbsToken; +import io.smallrye.reactive.messaging.amqp.cbs.CbsTokenProvider; +import io.smallrye.reactive.messaging.amqp.cbs.DefaultCbsToken; + +// tag::provider[] +@ApplicationScoped +public class MyCbsTokenProvider implements CbsTokenProvider { + + @Override + public Uni getToken(AmqpConnectorCommonConfiguration config) { + String token = fetchTokenFromAuthService(); + return Uni.createFrom().item(new DefaultCbsToken( + token, + "jwt", + OffsetDateTime.now().plusHours(1), + OffsetDateTime.now().plusMinutes(45))); + } + + private String fetchTokenFromAuthService() { + // Retrieve a token from your identity provider + return "my-jwt-token"; + } +} +// end::provider[] diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/amqp/customization/MySasTokenProvider.java b/content/docs/smallrye-reactive-messaging/4/_examples/amqp/customization/MySasTokenProvider.java new file mode 100644 index 000000000..0440f43b5 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/amqp/customization/MySasTokenProvider.java @@ -0,0 +1,29 @@ +package amqp.customization; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.amqp.AmqpConnectorCommonConfiguration; +import io.smallrye.reactive.messaging.amqp.cbs.CbsToken; +import io.smallrye.reactive.messaging.amqp.cbs.CbsTokenProvider; +import io.smallrye.reactive.messaging.amqp.cbs.DefaultCbsToken; + +// tag::sas-provider[] +@ApplicationScoped +public class MySasTokenProvider implements CbsTokenProvider { + + @Override + public Uni getToken(AmqpConnectorCommonConfiguration config) { + String sasToken = generateSasToken(config.getHost()); + return Uni.createFrom().item(new DefaultCbsToken( + sasToken, + "servicebus.windows.net:sastoken", + 3600)); // valid for 1 hour + } + + private String generateSasToken(String host) { + // Build a SAS token for Azure Service Bus + return "SharedAccessSignature sr=..."; + } +} +// end::sas-provider[] diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/amqp/inbound/AmqpMetadataExample.java b/content/docs/smallrye-reactive-messaging/4/_examples/amqp/inbound/AmqpMetadataExample.java new file mode 100644 index 000000000..8683ccbb7 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/amqp/inbound/AmqpMetadataExample.java @@ -0,0 +1,27 @@ +package amqp.inbound; + +import java.util.Optional; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.amqp.IncomingAmqpMetadata; +import io.vertx.core.json.JsonObject; + +public class AmqpMetadataExample { + + public void metadata() { + Message incoming = Message.of(12.0); + // tag::code[] + Optional metadata = incoming.getMetadata(IncomingAmqpMetadata.class); + metadata.ifPresent(meta -> { + String address = meta.getAddress(); + String subject = meta.getSubject(); + boolean durable = meta.isDurable(); + // Use io.vertx.core.json.JsonObject + JsonObject properties = meta.getProperties(); + // ... + }); + // end::code[] + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/amqp/inbound/AmqpPriceConsumer.java b/content/docs/smallrye-reactive-messaging/4/_examples/amqp/inbound/AmqpPriceConsumer.java new file mode 100644 index 000000000..ac877fc2b --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/amqp/inbound/AmqpPriceConsumer.java @@ -0,0 +1,15 @@ +package amqp.inbound; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; + +@ApplicationScoped +public class AmqpPriceConsumer { + + @Incoming("prices") + public void consume(double price) { + // process your price. + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/amqp/inbound/AmqpPriceMessageConsumer.java b/content/docs/smallrye-reactive-messaging/4/_examples/amqp/inbound/AmqpPriceMessageConsumer.java new file mode 100644 index 000000000..1611ebf7d --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/amqp/inbound/AmqpPriceMessageConsumer.java @@ -0,0 +1,21 @@ +package amqp.inbound; + +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; + +@ApplicationScoped +public class AmqpPriceMessageConsumer { + + @Incoming("prices") + public CompletionStage consume(Message price) { + // process your price. + + // Acknowledge the incoming message, marking the AMQP message as `accepted`. + return price.ack(); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/amqp/outbound/AmqpCloudEventMultipleProcessors.java b/content/docs/smallrye-reactive-messaging/4/_examples/amqp/outbound/AmqpCloudEventMultipleProcessors.java new file mode 100644 index 000000000..f87f08a15 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/amqp/outbound/AmqpCloudEventMultipleProcessors.java @@ -0,0 +1,39 @@ +package amqp.outbound; + +import java.net.URI; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.reactive.messaging.ce.OutgoingCloudEventMetadata; + +@ApplicationScoped +public class AmqpCloudEventMultipleProcessors { + + @Incoming("source") + @Outgoing("processed") + public Message process(Message in) { + return in.addMetadata(OutgoingCloudEventMetadata.builder() + .withId("id-" + in.getPayload()) + .withType("greeting") + .build()); + } + + @SuppressWarnings("unchecked") + @Incoming("processed") + @Outgoing("cloud-events") + public Message process2(Message in) { + OutgoingCloudEventMetadata metadata = in + .getMetadata(OutgoingCloudEventMetadata.class) + .orElseGet(() -> OutgoingCloudEventMetadata.builder().build()); + + return in.addMetadata(OutgoingCloudEventMetadata.from(metadata) + .withSource(URI.create("source://me")) + .withSubject("test") + .build()); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/amqp/outbound/AmqpCloudEventProcessor.java b/content/docs/smallrye-reactive-messaging/4/_examples/amqp/outbound/AmqpCloudEventProcessor.java new file mode 100644 index 000000000..3b7a981f8 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/amqp/outbound/AmqpCloudEventProcessor.java @@ -0,0 +1,25 @@ +package amqp.outbound; + +import java.net.URI; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.reactive.messaging.ce.OutgoingCloudEventMetadata; + +@ApplicationScoped +public class AmqpCloudEventProcessor { + + @Outgoing("cloud-events") + public Message toCloudEvents(Message in) { + return in.addMetadata(OutgoingCloudEventMetadata.builder() + .withId("id-" + in.getPayload()) + .withType("greetings") + .withSource(URI.create("http://example.com")) + .withSubject("greeting-message") + .build()); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/amqp/outbound/AmqpOutboundDynamicAddressExample.java b/content/docs/smallrye-reactive-messaging/4/_examples/amqp/outbound/AmqpOutboundDynamicAddressExample.java new file mode 100644 index 000000000..35f848796 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/amqp/outbound/AmqpOutboundDynamicAddressExample.java @@ -0,0 +1,26 @@ +package amqp.outbound; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.amqp.OutgoingAmqpMetadata; + +public class AmqpOutboundDynamicAddressExample { + public Message metadata(Message incoming) { + + // tag::code[] + String addressName = selectAddressFromIncommingMessage(incoming); + OutgoingAmqpMetadata metadata = OutgoingAmqpMetadata.builder() + .withAddress(addressName) + .withDurable(true) + .build(); + + // Create a new message from the `incoming` message + // Add `metadata` to the metadata from the `incoming` message. + return incoming.addMetadata(metadata); + // end::code[] + } + + private String selectAddressFromIncommingMessage(Message incoming) { + return "fake"; + } +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/amqp/outbound/AmqpOutboundMetadataExample.java b/content/docs/smallrye-reactive-messaging/4/_examples/amqp/outbound/AmqpOutboundMetadataExample.java new file mode 100644 index 000000000..3eef8b590 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/amqp/outbound/AmqpOutboundMetadataExample.java @@ -0,0 +1,23 @@ +package amqp.outbound; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.amqp.OutgoingAmqpMetadata; + +public class AmqpOutboundMetadataExample { + + public Message metadata(Message incoming) { + + // tag::code[] + OutgoingAmqpMetadata metadata = OutgoingAmqpMetadata.builder() + .withDurable(true) + .withSubject("my-subject") + .build(); + + // Create a new message from the `incoming` message + // Add `metadata` to the metadata from the `incoming` message. + return incoming.addMetadata(metadata); + // end::code[] + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/amqp/outbound/AmqpPriceMessageProducer.java b/content/docs/smallrye-reactive-messaging/4/_examples/amqp/outbound/AmqpPriceMessageProducer.java new file mode 100644 index 000000000..418a901b3 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/amqp/outbound/AmqpPriceMessageProducer.java @@ -0,0 +1,26 @@ +package amqp.outbound; + +import java.time.Duration; +import java.util.Random; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +@ApplicationScoped +public class AmqpPriceMessageProducer { + + private Random random = new Random(); + + @Outgoing("prices") + public Multi> generate() { + // Build an infinite stream of random prices + // It emits a price every second + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .map(x -> Message.of(random.nextDouble())); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/amqp/outbound/AmqpPriceProducer.java b/content/docs/smallrye-reactive-messaging/4/_examples/amqp/outbound/AmqpPriceProducer.java new file mode 100644 index 000000000..0e760b71f --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/amqp/outbound/AmqpPriceProducer.java @@ -0,0 +1,25 @@ +package amqp.outbound; + +import java.time.Duration; +import java.util.Random; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +@ApplicationScoped +public class AmqpPriceProducer { + + private Random random = new Random(); + + @Outgoing("prices") + public Multi generate() { + // Build an infinite stream of random prices + // It emits a price every second + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .map(x -> random.nextDouble()); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/amqp/outbound/AmqpReplier.java b/content/docs/smallrye-reactive-messaging/4/_examples/amqp/outbound/AmqpReplier.java new file mode 100644 index 000000000..7f5236aba --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/amqp/outbound/AmqpReplier.java @@ -0,0 +1,20 @@ +package amqp.outbound; + +import java.util.Random; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +@ApplicationScoped +public class AmqpReplier { + + Random rand = new Random(); + + @Incoming("request") + @Outgoing("reply") + String handleRequest(String request) { + return request + "-" + rand.nextInt(100); + } +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/amqp/outbound/AmqpReplierWithMetadata.java b/content/docs/smallrye-reactive-messaging/4/_examples/amqp/outbound/AmqpReplierWithMetadata.java new file mode 100644 index 000000000..c798cd136 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/amqp/outbound/AmqpReplierWithMetadata.java @@ -0,0 +1,31 @@ +package amqp.outbound; + +import java.util.Random; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.reactive.messaging.amqp.IncomingAmqpMetadata; +import io.smallrye.reactive.messaging.amqp.OutgoingAmqpMetadata; + +@ApplicationScoped +public class AmqpReplierWithMetadata { + + Random rand = new Random(); + + @Incoming("request") + @Outgoing("reply") + Message handleRequest(Message request) { + IncomingAmqpMetadata incoming = request.getMetadata(IncomingAmqpMetadata.class) + .orElseThrow(); + OutgoingAmqpMetadata outMeta = OutgoingAmqpMetadata.builder() + .withAddress(incoming.getReplyTo()) + .withCorrelationId(incoming.getId()) + .build(); + String reply = request.getPayload() + "-" + rand.nextInt(100); + return request.withPayload(reply).addMetadata(outMeta); + } +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/amqp/outbound/AmqpRequestReplyEmitter.java b/content/docs/smallrye-reactive-messaging/4/_examples/amqp/outbound/AmqpRequestReplyEmitter.java new file mode 100644 index 000000000..f1d5285c8 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/amqp/outbound/AmqpRequestReplyEmitter.java @@ -0,0 +1,21 @@ +package amqp.outbound; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; + +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.amqp.reply.AmqpRequestReply; + +@ApplicationScoped +public class AmqpRequestReplyEmitter { + + @Inject + @Channel("my-request") + AmqpRequestReply quoteRequest; + + public Uni requestQuote(String request) { + return quoteRequest.request(request); + } +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/amqp/outbound/AmqpRequestReplyMessageEmitter.java b/content/docs/smallrye-reactive-messaging/4/_examples/amqp/outbound/AmqpRequestReplyMessageEmitter.java new file mode 100644 index 000000000..de5f1bbaf --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/amqp/outbound/AmqpRequestReplyMessageEmitter.java @@ -0,0 +1,22 @@ +package amqp.outbound; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.amqp.reply.AmqpRequestReply; + +@ApplicationScoped +public class AmqpRequestReplyMessageEmitter { + + @Inject + @Channel("my-request") + AmqpRequestReply quoteRequest; + + public Uni> requestMessage(String request) { + return quoteRequest.request(Message.of(request)); + } +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/amqp/outbound/AmqpRequestReplyMultiEmitter.java b/content/docs/smallrye-reactive-messaging/4/_examples/amqp/outbound/AmqpRequestReplyMultiEmitter.java new file mode 100644 index 000000000..25d776606 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/amqp/outbound/AmqpRequestReplyMultiEmitter.java @@ -0,0 +1,21 @@ +package amqp.outbound; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; + +import io.smallrye.mutiny.Multi; +import io.smallrye.reactive.messaging.amqp.reply.AmqpRequestReply; + +@ApplicationScoped +public class AmqpRequestReplyMultiEmitter { + + @Inject + @Channel("my-request") + AmqpRequestReply quoteRequest; + + public Multi requestQuote(String request) { + return quoteRequest.requestMulti(request).select().first(5); + } +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/amqp/outbound/MyAmqpReplyFailureHandler.java b/content/docs/smallrye-reactive-messaging/4/_examples/amqp/outbound/MyAmqpReplyFailureHandler.java new file mode 100644 index 000000000..77b1d2d14 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/amqp/outbound/MyAmqpReplyFailureHandler.java @@ -0,0 +1,24 @@ +package amqp.outbound; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.smallrye.common.annotation.Identifier; +import io.smallrye.reactive.messaging.amqp.AmqpMessage; +import io.smallrye.reactive.messaging.amqp.reply.ReplyFailureHandler; + +@ApplicationScoped +@Identifier("my-reply-error") +public class MyAmqpReplyFailureHandler implements ReplyFailureHandler { + + @Override + public Throwable handleReply(AmqpMessage replyMessage) { + var properties = replyMessage.getApplicationProperties(); + if (properties != null) { + String error = properties.getString("REPLY_ERROR"); + if (error != null) { + return new IllegalArgumentException(error); + } + } + return null; + } +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/beans/Chain.java b/content/docs/smallrye-reactive-messaging/4/_examples/beans/Chain.java new file mode 100644 index 000000000..8a44b6d55 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/beans/Chain.java @@ -0,0 +1,27 @@ +package beans; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +public class Chain { + + // tag::chain[] + @Outgoing("source") + public Multi generate() { + return Multi.createFrom().items("Hello", "from", "reactive", "messaging"); + } + + @Incoming("source") + @Outgoing("sink") + public String process(String in) { + return in.toUpperCase(); + } + + @Incoming("sink") + public void consume(String processed) { + System.out.println(processed); + } + // end::chain[] +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/beans/MessageProcessingBean.java b/content/docs/smallrye-reactive-messaging/4/_examples/beans/MessageProcessingBean.java new file mode 100644 index 000000000..edfbf0df4 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/beans/MessageProcessingBean.java @@ -0,0 +1,20 @@ +package beans; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +@ApplicationScoped +public class MessageProcessingBean { + + @Incoming("consumed-channel") + @Outgoing("populated-channel") + public Message process(Message in) { + // Process the payload + String payload = in.getPayload().toUpperCase(); + // Create a new message from `in` and just update the payload + return in.withPayload(payload); + } +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/beans/PayloadProcessingBean.java b/content/docs/smallrye-reactive-messaging/4/_examples/beans/PayloadProcessingBean.java new file mode 100644 index 000000000..b7fa6b0ae --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/beans/PayloadProcessingBean.java @@ -0,0 +1,16 @@ +package beans; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +@ApplicationScoped +public class PayloadProcessingBean { + + @Incoming("consumed-channel") + @Outgoing("populated-channel") + public String process(String in) { + return in.toUpperCase(); + } +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/broadcast/BroadcastExamples.java b/content/docs/smallrye-reactive-messaging/4/_examples/broadcast/BroadcastExamples.java new file mode 100644 index 000000000..53a8bb160 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/broadcast/BroadcastExamples.java @@ -0,0 +1,29 @@ +package broadcast; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.reactive.messaging.annotations.Broadcast; + +public class BroadcastExamples { + + // tag::chain[] + @Incoming("in") + @Outgoing("out") + @Broadcast + public int increment(int i) { + return i + 1; + } + + @Incoming("out") + public void consume1(int i) { + //... + } + + @Incoming("out") + public void consume2(int i) { + //... + } + // end::chain[] + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/broadcast/BroadcastWithCountExamples.java b/content/docs/smallrye-reactive-messaging/4/_examples/broadcast/BroadcastWithCountExamples.java new file mode 100644 index 000000000..e227d7747 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/broadcast/BroadcastWithCountExamples.java @@ -0,0 +1,29 @@ +package broadcast; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.reactive.messaging.annotations.Broadcast; + +public class BroadcastWithCountExamples { + + // tag::chain[] + @Incoming("in") + @Outgoing("out") + @Broadcast(2) + public int increment(int i) { + return i + 1; + } + + @Incoming("out") + public void consume1(int i) { + //... + } + + @Incoming("out") + public void consume2(int i) { + //... + } + // end::chain[] + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/camel/api/CamelApi.java b/content/docs/smallrye-reactive-messaging/4/_examples/camel/api/CamelApi.java new file mode 100644 index 000000000..f96087599 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/camel/api/CamelApi.java @@ -0,0 +1,94 @@ +package camel.api; + +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +import org.apache.camel.BindToRegistry; +import org.apache.camel.CamelContext; +import org.apache.camel.Exchange; +import org.apache.camel.builder.LambdaRouteBuilder; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.component.reactive.streams.api.CamelReactiveStreamsService; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; + +public class CamelApi { + + // tag::reactive[] + @Inject + CamelReactiveStreamsService reactiveStreamsService; + // end::reactive[] + + // tag::source[] + @Outgoing("camel") + public Publisher retrieveDataFromCamelRoute() { + return reactiveStreamsService.from("seda:camel"); + } + // end::source[] + + // tag::source-route-builder[] + @Singleton + static class MyRouteBuilder extends RouteBuilder { + @Inject + CamelReactiveStreamsService reactiveStreamsService; + + @Outgoing("sink") + public Publisher getDataFromCamelRoute() { + return reactiveStreamsService.fromStream("my-stream", String.class); + } + + @Override + public void configure() { + from("seda:camel").process( + exchange -> exchange.getMessage().setBody(exchange.getIn().getBody(String.class).toUpperCase())) + .to("reactive-streams:my-stream"); + } + } + // end::source-route-builder[] + + // tag::source-lambda-route-builder[] + @ApplicationScoped + static class MyLambdaRouteBuilder { + @Inject + CamelReactiveStreamsService reactiveStreamsService; + + @Outgoing("sink") + public Publisher getDataFromCamelRoute() { + return reactiveStreamsService.fromStream("my-stream", String.class); + } + + @Produces + @BindToRegistry + public LambdaRouteBuilder route() { + return rb -> rb.from("seda:camel").process( + exchange -> exchange.getMessage().setBody(exchange.getIn().getBody(String.class).toUpperCase())) + .to("reactive-streams:my-stream"); + } + } + // end::source-lambda-route-builder[] + + // tag::sink[] + @Incoming("to-camel") + public Subscriber sendDataToCamelRoute() { + return reactiveStreamsService.subscriber("file:./target?fileName=values.txt&fileExist=append", + String.class); + } + // end::sink[] + + // tag::producer[] + @Inject + CamelContext camel; + + @Incoming("to-camel") + public CompletionStage sink(String value) { + return camel.createProducerTemplate() + .asyncSendBody("file:./target?fileName=values.txt&fileExist=append", value).thenApply(x -> null); + } + // end::producer[] +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/camel/inbound/CamelFileConsumer.java b/content/docs/smallrye-reactive-messaging/4/_examples/camel/inbound/CamelFileConsumer.java new file mode 100644 index 000000000..21c206129 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/camel/inbound/CamelFileConsumer.java @@ -0,0 +1,20 @@ +package camel.inbound; + +import java.io.File; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.apache.camel.component.file.GenericFile; +import org.eclipse.microprofile.reactive.messaging.Incoming; + +@ApplicationScoped +public class CamelFileConsumer { + + @Incoming("files") + public void consume(GenericFile gf) { + File file = gf.getFile(); + // process the file + + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/camel/inbound/CamelFileMessageConsumer.java b/content/docs/smallrye-reactive-messaging/4/_examples/camel/inbound/CamelFileMessageConsumer.java new file mode 100644 index 000000000..56cbc8559 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/camel/inbound/CamelFileMessageConsumer.java @@ -0,0 +1,23 @@ +package camel.inbound; + +import java.io.File; +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.apache.camel.component.file.GenericFile; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; + +@ApplicationScoped +public class CamelFileMessageConsumer { + + @Incoming("files") + public CompletionStage consume(Message> msg) { + File file = msg.getPayload().getFile(); + // process the file + + return msg.ack(); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/camel/inbound/IncomingCamelMetadataExample.java b/content/docs/smallrye-reactive-messaging/4/_examples/camel/inbound/IncomingCamelMetadataExample.java new file mode 100644 index 000000000..63f0a4f3d --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/camel/inbound/IncomingCamelMetadataExample.java @@ -0,0 +1,29 @@ +package camel.inbound; + +import java.io.File; +import java.util.Optional; +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.apache.camel.Exchange; +import org.apache.camel.component.file.GenericFile; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.camel.IncomingExchangeMetadata; + +@ApplicationScoped +public class IncomingCamelMetadataExample { + + @Incoming("files") + public CompletionStage consume(Message> msg) { + Optional metadata = msg.getMetadata(IncomingExchangeMetadata.class); + if (metadata.isPresent()) { + // Retrieve the camel exchange: + Exchange exchange = metadata.get().getExchange(); + } + return msg.ack(); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/camel/outbound/CamelOutboundMetadataExample.java b/content/docs/smallrye-reactive-messaging/4/_examples/camel/outbound/CamelOutboundMetadataExample.java new file mode 100644 index 000000000..3261e7371 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/camel/outbound/CamelOutboundMetadataExample.java @@ -0,0 +1,31 @@ +package camel.outbound; + +import java.time.Duration; +import java.util.Random; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; +import io.smallrye.reactive.messaging.camel.OutgoingExchangeMetadata; + +@ApplicationScoped +public class CamelOutboundMetadataExample { + + private Random random = new Random(); + + @Outgoing("prices") + public Multi> generate() { + // tag::code[] + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .map(x -> random.nextDouble()) + .map(p -> Double.toString(p)) + .map(s -> Message.of(s) + .addMetadata(new OutgoingExchangeMetadata() + .putProperty("my-property", "my-value"))); + // end::code[] + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/camel/outbound/CamelPriceMessageProducer.java b/content/docs/smallrye-reactive-messaging/4/_examples/camel/outbound/CamelPriceMessageProducer.java new file mode 100644 index 000000000..5a0202608 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/camel/outbound/CamelPriceMessageProducer.java @@ -0,0 +1,27 @@ +package camel.outbound; + +import java.time.Duration; +import java.util.Random; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +@ApplicationScoped +public class CamelPriceMessageProducer { + + private Random random = new Random(); + + @Outgoing("prices") + public Multi> generate() { + // Build an infinite stream of random prices + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .map(x -> random.nextDouble()) + .map(p -> Double.toString(p)) + .map(Message::of); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/camel/outbound/CamelPriceProducer.java b/content/docs/smallrye-reactive-messaging/4/_examples/camel/outbound/CamelPriceProducer.java new file mode 100644 index 000000000..30392e08e --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/camel/outbound/CamelPriceProducer.java @@ -0,0 +1,26 @@ +package camel.outbound; + +import java.time.Duration; +import java.util.Random; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +@ApplicationScoped +public class CamelPriceProducer { + + private Random random = new Random(); + + @Outgoing("prices") + public Multi generate() { + // Build an infinite stream of random prices + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .onOverflow().drop() + .map(x -> random.nextDouble()) + .map(p -> Double.toString(p)); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/camel/processor/CamelProcessor.java b/content/docs/smallrye-reactive-messaging/4/_examples/camel/processor/CamelProcessor.java new file mode 100644 index 000000000..434b12911 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/camel/processor/CamelProcessor.java @@ -0,0 +1,18 @@ +package camel.processor; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +@ApplicationScoped +public class CamelProcessor { + + @Incoming("mynatssubject") + @Outgoing("mykafkatopic") + public byte[] process(byte[] message) { + // do some logic + return message; + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/connectors/MyAckHandler.java b/content/docs/smallrye-reactive-messaging/4/_examples/connectors/MyAckHandler.java new file mode 100644 index 000000000..dc1d1d146 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/connectors/MyAckHandler.java @@ -0,0 +1,25 @@ +package connectors; + +import java.util.concurrent.CompletionStage; + +import connectors.api.BrokerClient; +import io.smallrye.mutiny.Uni; + +public class MyAckHandler { + + private final BrokerClient client; + + static MyAckHandler create(BrokerClient client) { + return new MyAckHandler(client); + } + + public MyAckHandler(BrokerClient client) { + this.client = client; + } + + public CompletionStage handle(MyMessage msg) { + return Uni.createFrom().completionStage(client.ack(msg.getConsumedMessage())) + .emitOn(msg::runOnMessageContext) + .subscribeAsCompletionStage(); + } +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/connectors/MyConnector.java b/content/docs/smallrye-reactive-messaging/4/_examples/connectors/MyConnector.java new file mode 100644 index 000000000..9dd75beb8 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/connectors/MyConnector.java @@ -0,0 +1,75 @@ +package connectors; + +import static io.smallrye.reactive.messaging.annotations.ConnectorAttribute.Direction.INCOMING; +import static io.smallrye.reactive.messaging.annotations.ConnectorAttribute.Direction.INCOMING_AND_OUTGOING; +import static io.smallrye.reactive.messaging.annotations.ConnectorAttribute.Direction.OUTGOING; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Flow; + +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.spi.Connector; + +import connectors.api.BrokerClient; +import io.smallrye.reactive.messaging.annotations.ConnectorAttribute; +import io.smallrye.reactive.messaging.connector.InboundConnector; +import io.smallrye.reactive.messaging.connector.OutboundConnector; +import io.smallrye.reactive.messaging.providers.connectors.ExecutionHolder; +import io.vertx.mutiny.core.Vertx; + +@ApplicationScoped +@Connector(MyConnector.CONNECTOR_NAME) +@ConnectorAttribute(name = "client-id", type = "string", direction = INCOMING_AND_OUTGOING, description = "The client id ", mandatory = true) +@ConnectorAttribute(name = "buffer-size", type = "int", direction = INCOMING, description = "The size buffer of incoming messages waiting to be processed", defaultValue = "128") +@ConnectorAttribute(name = "topic", type = "string", direction = OUTGOING, description = "The default topic to send the messages, defaults to channel name if not set") +@ConnectorAttribute(name = "maxPendingMessages", type = "int", direction = OUTGOING, description = "The maximum size of a queue holding pending messages", defaultValue = "1000") +@ConnectorAttribute(name = "waitForWriteCompletion", type = "boolean", direction = OUTGOING, description = "Whether the outgoing channel waits for the write completion", defaultValue = "true") +public class MyConnector implements InboundConnector, OutboundConnector { + + public static final String CONNECTOR_NAME = "smallrye-my-connector"; + + @Inject + ExecutionHolder executionHolder; + + Vertx vertx; + + List incomingChannels = new CopyOnWriteArrayList<>(); + List outgoingChannels = new CopyOnWriteArrayList<>(); + + @PostConstruct + void init() { + this.vertx = executionHolder.vertx(); + } + + @Override + public Flow.Publisher> getPublisher(Config config) { + MyConnectorIncomingConfiguration ic = new MyConnectorIncomingConfiguration(config); + String channelName = ic.getChannel(); + String clientId = ic.getClientId(); + int bufferSize = ic.getBufferSize(); + // ... + BrokerClient client = BrokerClient.create(clientId); + MyIncomingChannel channel = new MyIncomingChannel(vertx, ic, client); + incomingChannels.add(channel); + return channel.getStream(); + } + + @Override + public Flow.Subscriber> getSubscriber(Config config) { + MyConnectorOutgoingConfiguration oc = new MyConnectorOutgoingConfiguration(config); + String channelName = oc.getChannel(); + String clientId = oc.getClientId(); + int pendingMessages = oc.getMaxPendingMessages(); + // ... + BrokerClient client = BrokerClient.create(clientId); + MyOutgoingChannel channel = new MyOutgoingChannel(vertx, oc, client); + outgoingChannels.add(channel); + return channel.getSubscriber(); + } +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/connectors/MyConnectorWithPartials.java b/content/docs/smallrye-reactive-messaging/4/_examples/connectors/MyConnectorWithPartials.java new file mode 100644 index 000000000..55d0cc2fa --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/connectors/MyConnectorWithPartials.java @@ -0,0 +1,106 @@ +package connectors; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Flow; + +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.spi.Connector; + +import connectors.api.BrokerClient; +import io.smallrye.reactive.messaging.connector.InboundConnector; +import io.smallrye.reactive.messaging.connector.OutboundConnector; +import io.smallrye.reactive.messaging.health.HealthReport; +import io.smallrye.reactive.messaging.health.HealthReporter; +import io.smallrye.reactive.messaging.providers.connectors.ExecutionHolder; +import io.vertx.mutiny.core.Vertx; + +// tag::health-report[] +@ApplicationScoped +@Connector(MyConnectorWithPartials.CONNECTOR_NAME) +public class MyConnectorWithPartials implements InboundConnector, OutboundConnector, HealthReporter { + + public static final String CONNECTOR_NAME = "smallrye-my-connector"; + + List incomingChannels = new CopyOnWriteArrayList<>(); + List outgoingChannels = new CopyOnWriteArrayList<>(); + + @Override + public HealthReport getReadiness() { + HealthReport.HealthReportBuilder builder = HealthReport.builder(); + for (MyIncomingChannel channel : incomingChannels) { + builder.add(channel.getChannel(), true); + } + for (MyOutgoingChannel channel : outgoingChannels) { + builder.add(channel.getChannel(), true); + } + return builder.build(); + } + + @Override + public HealthReport getLiveness() { + HealthReport.HealthReportBuilder builder = HealthReport.builder(); + for (MyIncomingChannel channel : incomingChannels) { + builder.add(channel.getChannel(), true); + } + for (MyOutgoingChannel channel : outgoingChannels) { + builder.add(channel.getChannel(), true); + } + return builder.build(); + } + + @Override + public HealthReport getStartup() { + HealthReport.HealthReportBuilder builder = HealthReport.builder(); + for (MyIncomingChannel channel : incomingChannels) { + builder.add(channel.getChannel(), true); + } + for (MyOutgoingChannel channel : outgoingChannels) { + builder.add(channel.getChannel(), true); + } + return builder.build(); + } + + // end::health-report[] + + @Inject + ExecutionHolder executionHolder; + + Vertx vertx; + + @PostConstruct + void init() { + this.vertx = executionHolder.vertx(); + } + + @Override + public Flow.Publisher> getPublisher(Config config) { + MyConnectorIncomingConfiguration ic = new MyConnectorIncomingConfiguration(config); + String channelName = ic.getChannel(); + String clientId = ic.getClientId(); + int bufferSize = ic.getBufferSize(); + // ... + BrokerClient client = BrokerClient.create(clientId); + MyIncomingChannel channel = new MyIncomingChannel(vertx, ic, client); + incomingChannels.add(channel); + return channel.getStream(); + } + + @Override + public Flow.Subscriber> getSubscriber(Config config) { + MyConnectorOutgoingConfiguration oc = new MyConnectorOutgoingConfiguration(config); + String channelName = oc.getChannel(); + String clientId = oc.getClientId(); + int pendingMessages = oc.getMaxPendingMessages(); + // ... + BrokerClient client = BrokerClient.create(clientId); + MyOutgoingChannel channel = new MyOutgoingChannel(vertx, oc, client); + outgoingChannels.add(channel); + return channel.getSubscriber(); + } +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/connectors/MyFailureHandler.java b/content/docs/smallrye-reactive-messaging/4/_examples/connectors/MyFailureHandler.java new file mode 100644 index 000000000..f41b78055 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/connectors/MyFailureHandler.java @@ -0,0 +1,27 @@ +package connectors; + +import java.util.concurrent.CompletionStage; + +import org.eclipse.microprofile.reactive.messaging.Metadata; + +import connectors.api.BrokerClient; +import io.smallrye.mutiny.Uni; + +public class MyFailureHandler { + + private final BrokerClient client; + + static MyFailureHandler create(BrokerClient client) { + return new MyFailureHandler(client); + } + + public MyFailureHandler(BrokerClient client) { + this.client = client; + } + + public CompletionStage handle(MyMessage msg, Throwable reason, Metadata metadata) { + return Uni.createFrom().completionStage(() -> client.reject(msg.getConsumedMessage(), reason.getMessage())) + .emitOn(msg::runOnMessageContext) + .subscribeAsCompletionStage(); + } +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/connectors/MyIncomingChannel.java b/content/docs/smallrye-reactive-messaging/4/_examples/connectors/MyIncomingChannel.java new file mode 100644 index 000000000..deded1eaa --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/connectors/MyIncomingChannel.java @@ -0,0 +1,57 @@ +package connectors; + +import java.util.concurrent.Flow; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import connectors.api.BrokerClient; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.health.HealthReport; +import io.vertx.core.impl.VertxInternal; +import io.vertx.mutiny.core.Context; +import io.vertx.mutiny.core.Vertx; + +public class MyIncomingChannel { + + private final String channel; + private final BrokerClient client; + private final Context context; + private final MyAckHandler ackHandler; + private final MyFailureHandler failureHandler; + private final AtomicBoolean closed = new AtomicBoolean(false); + private Flow.Publisher> stream; + + public MyIncomingChannel(Vertx vertx, MyConnectorIncomingConfiguration cfg, BrokerClient client) { + // create and configure the client with MyConnectorIncomingConfiguration + this.channel = cfg.getChannel(); + this.client = client; + this.context = Context.newInstance(((VertxInternal) vertx.getDelegate()).createEventLoopContext()); + this.ackHandler = MyAckHandler.create(this.client); + this.failureHandler = MyFailureHandler.create(this.client); + this.stream = Multi.createBy().repeating() + .uni(() -> Uni.createFrom().completionStage(this.client.poll())) + .until(__ -> closed.get()) + .emitOn(context::runOnContext) + .map(consumed -> new MyMessage<>(consumed, ackHandler, failureHandler)); + } + + public String getChannel() { + return channel; + } + + public Flow.Publisher> getStream() { + return this.stream; + } + + public void close() { + closed.compareAndSet(false, true); + client.close(); + } + + void isReady(HealthReport.HealthReportBuilder healthReportBuilder) { + + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/connectors/MyIncomingChannelWithPartials.java b/content/docs/smallrye-reactive-messaging/4/_examples/connectors/MyIncomingChannelWithPartials.java new file mode 100644 index 000000000..d7f2dd967 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/connectors/MyIncomingChannelWithPartials.java @@ -0,0 +1,79 @@ +package connectors; + +import java.util.concurrent.Flow; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import connectors.api.BrokerClient; +import connectors.api.ConsumedMessage; +import connectors.tracing.MyOpenTelemetryInstrumenter; +import connectors.tracing.MyTrace; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.health.HealthReport; +import io.smallrye.reactive.messaging.tracing.TracingUtils; +import io.vertx.core.impl.VertxInternal; +import io.vertx.mutiny.core.Context; +import io.vertx.mutiny.core.Vertx; + +public class MyIncomingChannelWithPartials { + + private final String channel; + private final BrokerClient client; + private final Context context; + private final MyAckHandler ackHandler; + private final MyFailureHandler failureHandler; + private final AtomicBoolean closed = new AtomicBoolean(false); + private final boolean tracingEnabled; + private Flow.Publisher> stream; + + public MyIncomingChannelWithPartials(Vertx vertx, MyConnectorIncomingConfiguration cfg, BrokerClient client) { + // create and configure the client with MyConnectorIncomingConfiguration + this.channel = cfg.getChannel(); + this.client = client; + this.context = Context.newInstance(((VertxInternal) vertx.getDelegate()).createEventLoopContext()); + this.ackHandler = MyAckHandler.create(this.client); + this.failureHandler = MyFailureHandler.create(this.client); + this.tracingEnabled = true; + // tag::incoming-tracing[] + Multi> receiveMulti = Multi.createBy().repeating() + .uni(() -> Uni.createFrom().completionStage(this.client.poll())) + .until(__ -> closed.get()) + .emitOn(context::runOnContext) + .map(consumed -> new MyMessage<>(consumed, ackHandler, failureHandler)); + + Instrumenter instrumenter = MyOpenTelemetryInstrumenter.createInstrumenter(true); + if (tracingEnabled) { + receiveMulti = receiveMulti.map(message -> { + ConsumedMessage consumedMessage = message.getMetadata(MyIncomingMetadata.class).get().getCustomMessage(); + return TracingUtils.traceIncoming(instrumenter, message, new MyTrace.Builder() + .withClientId(consumedMessage.clientId()) + .withTopic(consumedMessage.topic()) + .withProperties(consumedMessage.properties()) + .build()); + }); + } + // end::incoming-tracing[] + this.stream = receiveMulti; + } + + public String getChannel() { + return channel; + } + + public Flow.Publisher> getStream() { + return this.stream; + } + + public void close() { + closed.compareAndSet(false, true); + client.close(); + } + + void isReady(HealthReport.HealthReportBuilder healthReportBuilder) { + + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/connectors/MyIncomingMetadata.java b/content/docs/smallrye-reactive-messaging/4/_examples/connectors/MyIncomingMetadata.java new file mode 100644 index 000000000..c288085be --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/connectors/MyIncomingMetadata.java @@ -0,0 +1,34 @@ +package connectors; + +import java.util.Map; + +import connectors.api.ConsumedMessage; + +public class MyIncomingMetadata { + + private final ConsumedMessage msg; + + public MyIncomingMetadata(ConsumedMessage msg) { + this.msg = msg; + } + + public ConsumedMessage getCustomMessage() { + return msg; + } + + public T getBody() { + return msg.body(); + } + + public String getKey() { + return msg.key(); + } + + public long getTimestamp() { + return msg.timestamp(); + } + + public Map getProperties() { + return msg.properties(); + } +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/connectors/MyMessage.java b/content/docs/smallrye-reactive-messaging/4/_examples/connectors/MyMessage.java new file mode 100644 index 000000000..cd432f0f3 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/connectors/MyMessage.java @@ -0,0 +1,51 @@ +package connectors; + +import java.util.concurrent.CompletionStage; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Metadata; + +import connectors.api.ConsumedMessage; +import io.smallrye.reactive.messaging.providers.locals.ContextAwareMessage; + +public class MyMessage implements Message, ContextAwareMessage { + + private final T payload; + private final Metadata metadata; + private final MyAckHandler ackHandler; + + private final MyFailureHandler nackHandler; + private ConsumedMessage consumed; + + public MyMessage(ConsumedMessage message, MyAckHandler ackHandler, MyFailureHandler nackHandler) { + this.consumed = message; + this.payload = message.body(); + this.ackHandler = ackHandler; + this.nackHandler = nackHandler; + this.metadata = ContextAwareMessage.captureContextMetadata(new MyIncomingMetadata<>(message)); + } + + public ConsumedMessage getConsumedMessage() { + return consumed; + } + + @Override + public T getPayload() { + return payload; + } + + @Override + public Metadata getMetadata() { + return metadata; + } + + @Override + public CompletionStage ack() { + return ackHandler.handle(this); + } + + @Override + public CompletionStage nack(Throwable reason, Metadata nackMetadata) { + return nackHandler.handle(this, reason, nackMetadata); + } +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/connectors/MyMessageConverter.java b/content/docs/smallrye-reactive-messaging/4/_examples/connectors/MyMessageConverter.java new file mode 100644 index 000000000..6f9f180e9 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/connectors/MyMessageConverter.java @@ -0,0 +1,27 @@ +package connectors; + +import java.lang.reflect.Type; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import connectors.api.ConsumedMessage; +import io.smallrye.reactive.messaging.MessageConverter; +import io.smallrye.reactive.messaging.providers.helpers.TypeUtils; + +@ApplicationScoped +public class MyMessageConverter implements MessageConverter { + @Override + public boolean canConvert(Message in, Type target) { + return TypeUtils.isAssignable(target, ConsumedMessage.class) + && in.getMetadata(MyIncomingMetadata.class).isPresent(); + } + + @Override + public Message convert(Message in, Type target) { + return in.withPayload(in.getMetadata(MyIncomingMetadata.class) + .map(MyIncomingMetadata::getCustomMessage) + .orElse(null)); + } +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/connectors/MyOutgoingChannel.java b/content/docs/smallrye-reactive-messaging/4/_examples/connectors/MyOutgoingChannel.java new file mode 100644 index 000000000..013defda1 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/connectors/MyOutgoingChannel.java @@ -0,0 +1,49 @@ +package connectors; + +import java.util.concurrent.Flow; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import connectors.api.BrokerClient; +import connectors.api.SendMessage; +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.providers.helpers.MultiUtils; +import io.vertx.mutiny.core.Vertx; + +public class MyOutgoingChannel { + private final String channel; + private Flow.Subscriber> subscriber; + private final BrokerClient client; + private final String topic; + + public MyOutgoingChannel(Vertx vertx, MyConnectorOutgoingConfiguration oc, BrokerClient client) { + this.channel = oc.getChannel(); + this.client = client; + this.topic = oc.getTopic().orElse(oc.getChannel()); + this.subscriber = MultiUtils.via(multi -> multi.call(m -> publishMessage(this.client, m))); + } + + private Uni publishMessage(BrokerClient client, Message m) { + // construct the outgoing message + SendMessage sendMessage = new SendMessage(); + Object payload = m.getPayload(); + sendMessage.setPayload(payload); + sendMessage.setTopic(topic); + m.getMetadata(MyOutgoingMetadata.class).ifPresent(out -> { + sendMessage.setTopic(out.getTopic()); + sendMessage.setKey(out.getKey()); + //... + }); + return Uni.createFrom().completionStage(() -> client.send(sendMessage)) + .onItem().transformToUni(receipt -> Uni.createFrom().completionStage(m.ack())) + .onFailure().recoverWithUni(t -> Uni.createFrom().completionStage(m.nack(t))); + } + + public Flow.Subscriber> getSubscriber() { + return this.subscriber; + } + + public String getChannel() { + return this.channel; + } +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/connectors/MyOutgoingChannelWithPartials.java b/content/docs/smallrye-reactive-messaging/4/_examples/connectors/MyOutgoingChannelWithPartials.java new file mode 100644 index 000000000..2ba3d9e59 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/connectors/MyOutgoingChannelWithPartials.java @@ -0,0 +1,105 @@ +package connectors; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.Flow; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import connectors.api.BrokerClient; +import connectors.api.SendMessage; +import connectors.tracing.MyOpenTelemetryInstrumenter; +import connectors.tracing.MyTrace; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.providers.helpers.MultiUtils; +import io.smallrye.reactive.messaging.providers.helpers.SenderProcessor; +import io.smallrye.reactive.messaging.tracing.TracingUtils; +import io.vertx.mutiny.core.Vertx; + +public class MyOutgoingChannelWithPartials { + private SenderProcessor processor; + private Flow.Subscriber> subscriber; + private final BrokerClient client; + private final String topic; + private final boolean tracingEnabled; + + private Instrumenter instrumenter; + + public MyOutgoingChannelWithPartials(Vertx vertx, MyConnectorOutgoingConfiguration oc, BrokerClient client) { + this.client = client; + this.topic = oc.getTopic().orElse(oc.getChannel()); + this.tracingEnabled = true; + // tag::sender-processor[] + long requests = oc.getMaxPendingMessages(); + boolean waitForWriteCompletion = oc.getWaitForWriteCompletion(); + if (requests <= 0) { + requests = Long.MAX_VALUE; + } + this.processor = new SenderProcessor(requests, waitForWriteCompletion, m -> publishMessage(client, m)); + this.subscriber = MultiUtils.via(processor, m -> m.onFailure().invoke(f -> { + // log the failure + })); + // end::sender-processor[] + instrumenter = MyOpenTelemetryInstrumenter.createInstrumenter(false); + + } + + // tag::send-message[] + private Uni publishMessage(BrokerClient client, Message message) { + // construct the outgoing message + SendMessage sendMessage; + Object payload = message.getPayload(); + if (payload instanceof SendMessage) { + sendMessage = (SendMessage) message.getPayload(); + } else { + sendMessage = new SendMessage(); + sendMessage.setPayload(payload); + sendMessage.setTopic(topic); + message.getMetadata(MyOutgoingMetadata.class).ifPresent(out -> { + sendMessage.setTopic(out.getTopic()); + sendMessage.setKey(out.getKey()); + //... + }); + } + return Uni.createFrom().completionStage(() -> client.send(sendMessage)) + .onItem().transformToUni(receipt -> Uni.createFrom().completionStage(message.ack())) + .onFailure().recoverWithUni(t -> Uni.createFrom().completionStage(message.nack(t))); + } + // end::send-message[] + + // tag::outgoing-tracing[] + private Uni publishMessageWithTracing(BrokerClient client, Message message) { + // construct the outgoing message + SendMessage sendMessage; + Object payload = message.getPayload(); + if (payload instanceof SendMessage) { + sendMessage = (SendMessage) message.getPayload(); + } else { + sendMessage = new SendMessage(); + sendMessage.setPayload(payload); + sendMessage.setTopic(topic); + message.getMetadata(MyOutgoingMetadata.class).ifPresent(out -> { + sendMessage.setTopic(out.getTopic()); + sendMessage.setKey(out.getKey()); + //... + }); + } + if (tracingEnabled) { + Map properties = new HashMap<>(); + TracingUtils.traceOutgoing(instrumenter, message, new MyTrace.Builder() + .withProperties(properties) + .withTopic(sendMessage.getTopic()) + .build()); + sendMessage.setProperties(properties); + } + return Uni.createFrom().completionStage(() -> client.send(sendMessage)) + .onItem().transformToUni(receipt -> Uni.createFrom().completionStage(message.ack())) + .onFailure().recoverWithUni(t -> Uni.createFrom().completionStage(message.nack(t))); + } + // end::outgoing-tracing[] + + public Flow.Subscriber> getSubscriber() { + return this.subscriber; + } +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/connectors/MyOutgoingMessage.java b/content/docs/smallrye-reactive-messaging/4/_examples/connectors/MyOutgoingMessage.java new file mode 100644 index 000000000..86c8a3636 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/connectors/MyOutgoingMessage.java @@ -0,0 +1,70 @@ +package connectors; + +import java.util.concurrent.CompletionStage; +import java.util.function.Function; +import java.util.function.Supplier; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Metadata; + +import io.smallrye.reactive.messaging.providers.locals.ContextAwareMessage; + +public class MyOutgoingMessage implements Message, ContextAwareMessage { + + private final T payload; + private final Metadata metadata; + + private final Supplier> ack; + private final Function> nack; + + public static MyOutgoingMessage from(Message message) { + return new MyOutgoingMessage<>(message.getPayload(), message.getMetadata(), message.getAck(), message.getNack()); + } + + public MyOutgoingMessage(T payload, Metadata metadata, + Supplier> ack, + Function> nack) { + this.payload = payload; + this.metadata = metadata; + this.ack = ack; + this.nack = nack; + } + + public MyOutgoingMessage(T payload, String key, String topic, + Supplier> ack, + Function> nack) { + this(payload, Metadata.of(new MyOutgoingMetadata(topic, key)), ack, nack); + } + + @Override + public T getPayload() { + return payload; + } + + @Override + public Metadata getMetadata() { + return metadata; + } + + @Override + public Supplier> getAck() { + return this.ack; + } + + @Override + public Function> getNack() { + return this.nack; + } + + public MyOutgoingMessage withKey(String key) { + this.metadata.with(this.metadata.get(MyOutgoingMetadata.class) + .map(m -> MyOutgoingMetadata.builder(m).withKey(key).build())); + return this; + } + + public MyOutgoingMessage withTopic(String topic) { + this.metadata.with(this.metadata.get(MyOutgoingMetadata.class) + .map(m -> MyOutgoingMetadata.builder(m).withTopic(topic).build())); + return this; + } +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/connectors/MyOutgoingMetadata.java b/content/docs/smallrye-reactive-messaging/4/_examples/connectors/MyOutgoingMetadata.java new file mode 100644 index 000000000..15c47cc1b --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/connectors/MyOutgoingMetadata.java @@ -0,0 +1,56 @@ +package connectors; + +public class MyOutgoingMetadata { + private String topic; + private String key; + + public static MyOutgoingMetadataBuilder builder() { + return new MyOutgoingMetadataBuilder(); + } + + public static MyOutgoingMetadataBuilder builder(MyOutgoingMetadata metadata) { + return new MyOutgoingMetadataBuilder(metadata); + } + + public MyOutgoingMetadata(String topic, String key) { + this.topic = topic; + this.key = key; + } + + public String getTopic() { + return topic; + } + + public String getKey() { + return key; + } + + public static class MyOutgoingMetadataBuilder { + private String topic; + private String key; + + public MyOutgoingMetadataBuilder() { + + } + + public MyOutgoingMetadataBuilder(MyOutgoingMetadata metadata) { + this.key = metadata.getKey(); + this.topic = metadata.getTopic(); + } + + public MyOutgoingMetadataBuilder withTopic(String topic) { + this.topic = topic; + return this; + } + + public MyOutgoingMetadataBuilder withKey(String key) { + this.key = key; + return this; + } + + public MyOutgoingMetadata build() { + return new MyOutgoingMetadata(topic, key); + } + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/connectors/test/MyConnectorTest.java b/content/docs/smallrye-reactive-messaging/4/_examples/connectors/test/MyConnectorTest.java new file mode 100644 index 000000000..718aac41f --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/connectors/test/MyConnectorTest.java @@ -0,0 +1,52 @@ +package connectors.test; + +import static org.awaitility.Awaitility.await; + +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CopyOnWriteArrayList; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.junit.jupiter.api.Test; + +import connectors.MyConnector; +import io.smallrye.reactive.messaging.test.common.config.MapBasedConfig; + +public class MyConnectorTest extends WeldTestBase { + + @Test + void incomingChannel() { + String host = ""; + int port = 0; + String myTopic = UUID.randomUUID().toString(); + MapBasedConfig config = new MapBasedConfig() + .with("mp.messaging.incoming.data.topic", myTopic) + .with("mp.messaging.incoming.data.host", host) + .with("mp.messaging.incoming.data.port", port) + .with("mp.messaging.incoming.data.connector", MyConnector.CONNECTOR_NAME); + MyApp app = runApplication(config, MyApp.class); + + int expected = 10; + // produce expected number of messages to myTopic + + // wait until app received + await().until(() -> app.received().size() == expected); + } + + @ApplicationScoped + public static class MyApp { + + List received = new CopyOnWriteArrayList<>(); + + @Incoming("data") + void consume(String msg) { + received.add(msg); + } + + public List received() { + return received; + } + } +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/connectors/test/WeldTestBase.java b/content/docs/smallrye-reactive-messaging/4/_examples/connectors/test/WeldTestBase.java new file mode 100644 index 000000000..35cf9555b --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/connectors/test/WeldTestBase.java @@ -0,0 +1,140 @@ +package connectors.test; + +import jakarta.enterprise.inject.spi.BeanManager; + +import org.eclipse.microprofile.reactive.messaging.spi.ConnectorLiteral; +import org.jboss.weld.environment.se.Weld; +import org.jboss.weld.environment.se.WeldContainer; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; + +import connectors.MyConnector; +import connectors.MyMessageConverter; +import io.smallrye.config.inject.ConfigExtension; +import io.smallrye.reactive.messaging.providers.MediatorFactory; +import io.smallrye.reactive.messaging.providers.connectors.ExecutionHolder; +import io.smallrye.reactive.messaging.providers.connectors.WorkerPoolRegistry; +import io.smallrye.reactive.messaging.providers.extension.ChannelProducer; +import io.smallrye.reactive.messaging.providers.extension.EmitterFactoryImpl; +import io.smallrye.reactive.messaging.providers.extension.HealthCenter; +import io.smallrye.reactive.messaging.providers.extension.LegacyEmitterFactoryImpl; +import io.smallrye.reactive.messaging.providers.extension.MediatorManager; +import io.smallrye.reactive.messaging.providers.extension.MutinyEmitterFactoryImpl; +import io.smallrye.reactive.messaging.providers.extension.ReactiveMessagingExtension; +import io.smallrye.reactive.messaging.providers.impl.ConfiguredChannelFactory; +import io.smallrye.reactive.messaging.providers.impl.ConnectorFactories; +import io.smallrye.reactive.messaging.providers.impl.InternalChannelRegistry; +import io.smallrye.reactive.messaging.providers.metrics.MetricDecorator; +import io.smallrye.reactive.messaging.providers.metrics.MicrometerDecorator; +import io.smallrye.reactive.messaging.providers.wiring.Wiring; +import io.smallrye.reactive.messaging.test.common.config.MapBasedConfig; +import io.smallrye.reactive.messaging.test.common.config.SmallRyeConfigTestUtil; + +public class WeldTestBase { + + protected Weld weld; + protected WeldContainer container; + + @BeforeEach + public void initWeld() { + weld = new Weld(); + + // SmallRye config + ConfigExtension extension = new ConfigExtension(); + weld.addExtension(extension); + + weld.addBeanClass(MediatorFactory.class); + weld.addBeanClass(MediatorManager.class); + weld.addBeanClass(InternalChannelRegistry.class); + weld.addBeanClass(ConnectorFactories.class); + weld.addBeanClass(ConfiguredChannelFactory.class); + weld.addBeanClass(ChannelProducer.class); + weld.addBeanClass(ExecutionHolder.class); + weld.addBeanClass(WorkerPoolRegistry.class); + weld.addBeanClass(HealthCenter.class); + weld.addBeanClass(Wiring.class); + weld.addExtension(new ReactiveMessagingExtension()); + + weld.addBeanClass(EmitterFactoryImpl.class); + weld.addBeanClass(MutinyEmitterFactoryImpl.class); + weld.addBeanClass(LegacyEmitterFactoryImpl.class); + + weld.addBeanClass(MyConnector.class); + weld.addBeanClass(MyMessageConverter.class); + weld.addBeanClass(MetricDecorator.class); + weld.addBeanClass(MicrometerDecorator.class); + weld.disableDiscovery(); + } + + @AfterEach + public void stopContainer() { + if (container != null) { + // TODO Explicitly close the connector + getBeanManager().createInstance() + .select(MyConnector.class, ConnectorLiteral.of(MyConnector.CONNECTOR_NAME)).get(); + container.close(); + } + // Release the config objects + SmallRyeConfigTestUtil.releaseConfig(); + } + + public BeanManager getBeanManager() { + if (container == null) { + runApplication(new MapBasedConfig()); + } + return container.getBeanManager(); + } + + public void addBeans(Class... clazzes) { + weld.addBeanClasses(clazzes); + } + + public T get(Class clazz) { + return getBeanManager().createInstance().select(clazz).get(); + } + + public T runApplication(MapBasedConfig config, Class clazz) { + weld.addBeanClass(clazz); + runApplication(config); + return get(clazz); + } + + public void runApplication(MapBasedConfig config) { + if (config != null) { + config.write(); + } else { + MapBasedConfig.cleanup(); + } + + SmallRyeConfigTestUtil.installConfig(); + container = weld.initialize(); + } + + public static void addConfig(MapBasedConfig config) { + if (config != null) { + config.write(); + } else { + MapBasedConfig.cleanup(); + } + } + + public HealthCenter getHealth() { + if (container == null) { + throw new IllegalStateException("Application not started"); + } + return container.getBeanManager().createInstance().select(HealthCenter.class).get(); + } + + public boolean isStarted() { + return getHealth().getStartup().isOk(); + } + + public boolean isReady() { + return getHealth().getReadiness().isOk(); + } + + public boolean isAlive() { + return getHealth().getLiveness().isOk(); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/connectors/tracing/MyOpenTelemetryInstrumenter.java b/content/docs/smallrye-reactive-messaging/4/_examples/connectors/tracing/MyOpenTelemetryInstrumenter.java new file mode 100644 index 000000000..9454e254f --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/connectors/tracing/MyOpenTelemetryInstrumenter.java @@ -0,0 +1,64 @@ +package connectors.tracing; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessageOperation; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingSpanNameExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; +import io.smallrye.reactive.messaging.tracing.TracingUtils; + +/** + * Encapsulates the OpenTelemetry instrumentation API so that those classes are only needed if + * users explicitly enable tracing. + */ +public class MyOpenTelemetryInstrumenter { + + private final Instrumenter instrumenter; + + private MyOpenTelemetryInstrumenter(Instrumenter instrumenter) { + this.instrumenter = instrumenter; + } + + public static MyOpenTelemetryInstrumenter createIncoming() { + return new MyOpenTelemetryInstrumenter(createInstrumenter(true)); + } + + public static MyOpenTelemetryInstrumenter createOutgoing() { + return new MyOpenTelemetryInstrumenter(createInstrumenter(false)); + } + + // tag::create-instrumenter[] + public static Instrumenter createInstrumenter(boolean incoming) { + MessageOperation messageOperation = incoming ? MessageOperation.RECEIVE : MessageOperation.PUBLISH; + + MyAttributesExtractor myExtractor = new MyAttributesExtractor(); + MessagingAttributesGetter attributesGetter = myExtractor.getMessagingAttributesGetter(); + var spanNameExtractor = MessagingSpanNameExtractor.create(attributesGetter, messageOperation); + InstrumenterBuilder builder = Instrumenter.builder(GlobalOpenTelemetry.get(), + "io.smallrye.reactive.messaging", spanNameExtractor); + var attributesExtractor = MessagingAttributesExtractor.create(attributesGetter, messageOperation); + + builder + .addAttributesExtractor(attributesExtractor) + .addAttributesExtractor(myExtractor); + + if (incoming) { + return builder.buildConsumerInstrumenter(MyTraceTextMapGetter.INSTANCE); + } else { + return builder.buildProducerInstrumenter(MyTraceTextMapSetter.INSTANCE); + } + } + // + + public Message traceIncoming(Message message, MyTrace myTrace, boolean makeCurrent) { + return TracingUtils.traceIncoming(instrumenter, message, myTrace, makeCurrent); + } + + public void traceOutgoing(Message message, MyTrace myTrace) { + TracingUtils.traceOutgoing(instrumenter, message, myTrace); + } +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/consumption/ConsumptionExamples.java b/content/docs/smallrye-reactive-messaging/4/_examples/consumption/ConsumptionExamples.java new file mode 100644 index 000000000..2094888b6 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/consumption/ConsumptionExamples.java @@ -0,0 +1,70 @@ +package consumption; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.mutiny.Uni; + +public class ConsumptionExamples { + + //tag::message-cs[] + @Incoming("my-channel") + public CompletionStage consumeMessage(Message message) { + handle(message.getPayload()); + return message.ack(); + } + //end::message-cs[] + + //tag::payload-sync[] + @Incoming("my-channel") + public void consumePayload(Price payload) { + // do something + } + //end::payload-sync[] + + //tag::payload-cs[] + @Incoming("my-channel") + public CompletionStage consumePayloadCS(Price payload) { + CompletionStage cs = handleAsync(payload); + return cs; + } + //end::payload-cs[] + + //tag::payload-uni[] + @Incoming("my-channel") + public Uni consumePayloadUni(Price payload) { + return Uni.createFrom().item(payload) + .onItem().invoke(this::handle) + .onItem().ignore().andContinueWithNull(); + } + //end::payload-uni[] + + //tag::message-uni[] + @Incoming("my-channel") + public Uni consumeMessageUni(Message message) { + return Uni.createFrom().item(message) + .onItem().invoke(m -> handle(m.getPayload())) + .onItem().transformToUni(x -> Uni.createFrom().completionStage(message.ack())); + } + //end::message-uni[] + + private CompletionStage handleAsync(Price price) { + return CompletableFuture.completedFuture(null); + } + + @SuppressWarnings("UnusedReturnValue") + private Price handle(Price price) { + return price; + } + + public static class Price { + final double price; + + public Price(double p) { + this.price = p; + } + } +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/converters/ConverterExample.java b/content/docs/smallrye-reactive-messaging/4/_examples/converters/ConverterExample.java new file mode 100644 index 000000000..a9d3b661e --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/converters/ConverterExample.java @@ -0,0 +1,24 @@ +package converters; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +public class ConverterExample { + + // tag::code[] + @Outgoing("persons") + public Multi source() { + return Multi.createFrom().items("Neo", "Morpheus", "Trinity"); + } + + // The messages need to be converted as they are emitted as Message + // and consumed as Message + @Incoming("persons") + public void consume(Person p) { + // ... + } + // end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/converters/MyConverter.java b/content/docs/smallrye-reactive-messaging/4/_examples/converters/MyConverter.java new file mode 100644 index 000000000..3f517ece0 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/converters/MyConverter.java @@ -0,0 +1,31 @@ +package converters; + +import java.lang.reflect.Type; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.MessageConverter; + +// tag::code[] +@ApplicationScoped +public class MyConverter implements MessageConverter { + @Override + public boolean canConvert(Message in, Type target) { + // Checks whether this converter can be used to convert + // the incoming message into a message containing a payload + // of the type `target`. + return in.getPayload().getClass().equals(String.class) + && target.equals(Person.class); + } + + @Override + public Message convert(Message in, Type target) { + // Convert the incoming message into the new message. + // It's important to build the new message **from** + // the received one. + return in.withPayload(new Person((String) in.getPayload())); + } +} +// end::code[] diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/customizers/MyClientCustomizer.java b/content/docs/smallrye-reactive-messaging/4/_examples/customizers/MyClientCustomizer.java new file mode 100644 index 000000000..9e07288be --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/customizers/MyClientCustomizer.java @@ -0,0 +1,17 @@ +package customizers; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.config.Config; + +import io.smallrye.reactive.messaging.ClientCustomizer; + +@ApplicationScoped +public class MyClientCustomizer implements ClientCustomizer { + @Override + public ClientConfig customize(String channel, Config channelConfig, ClientConfig config) { + // customize the client configuration + return config; + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/decorators/ConsumedMessageDecorator.java b/content/docs/smallrye-reactive-messaging/4/_examples/decorators/ConsumedMessageDecorator.java new file mode 100644 index 000000000..d1b403fde --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/decorators/ConsumedMessageDecorator.java @@ -0,0 +1,41 @@ +package decorators; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.mutiny.Multi; +import io.smallrye.reactive.messaging.PublisherDecorator; + +// tag::code[] +@ApplicationScoped +public class ConsumedMessageDecorator implements PublisherDecorator { + + private final Map counters = new HashMap<>(); + + @Override + public Multi> decorate(Multi> publisher, String channelName, + boolean isConnector) { + if (isConnector) { + AtomicLong counter = new AtomicLong(); + counters.put(channelName, counter); + return publisher.onItem().invoke(counter::incrementAndGet); + } else { + return publisher; + } + } + + @Override + public int getPriority() { + return 10; + } + + public long getMessageCount(String channel) { + return counters.get(channel).get(); + } +} +// end::code[] diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/emitter/ChannelExamples.java b/content/docs/smallrye-reactive-messaging/4/_examples/emitter/ChannelExamples.java new file mode 100644 index 000000000..1b2f021ba --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/emitter/ChannelExamples.java @@ -0,0 +1,31 @@ +package emitter; + +import java.util.concurrent.Flow.Publisher; + +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.mutiny.Multi; + +public class ChannelExamples { + + // tag::channel[] + @Inject + @Channel("my-channel") + Multi streamOfPayloads; + + @Inject + @Channel("my-channel") + Multi> streamOfMessages; + + @Inject + @Channel("my-channel") + Publisher publisherOfPayloads; + + @Inject + @Channel("my-channel") + Publisher> publisherOfMessages; + // end::channel[] +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/emitter/EmitterExamples.java b/content/docs/smallrye-reactive-messaging/4/_examples/emitter/EmitterExamples.java new file mode 100644 index 000000000..4c5c4ed99 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/emitter/EmitterExamples.java @@ -0,0 +1,119 @@ +package emitter; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.*; + +import io.smallrye.reactive.messaging.annotations.Broadcast; +import messages.MyMetadata; + +public class EmitterExamples { + + // tag::payload[] + @Inject + @Channel("prices") + Emitter emitterForPrices; + + public void send(double d) { + emitterForPrices.send(d); + } + // end::payload[] + + // tag::cs[] + public void sendAndAwaitAcknowledgement(double d) { + CompletionStage acked = emitterForPrices.send(d); + // sending a payload returns a CompletionStage completed + // when the message is acknowledged + acked.toCompletableFuture().join(); + } + // end::cs[] + + // tag::message[] + public void sendAsMessage(double d) { + emitterForPrices.send(Message.of(d)); + } + // end::message[] + + // tag::message-ack[] + public void sendAsMessageWithAck(double d) { + emitterForPrices.send(Message.of(d, () -> { + // Called when the message is acknowledged. + return CompletableFuture.completedFuture(null); + }, + reason -> { + // Called when the message is acknowledged negatively. + return CompletableFuture.completedFuture(null); + })); + } + // end::message-ack[] + + // tag::message-meta[] + public void sendAsMessageWithAckAndMetadata(double d) { + MyMetadata metadata = new MyMetadata(); + emitterForPrices.send(Message.of(d, Metadata.of(metadata), + () -> { + // Called when the message is acknowledged. + return CompletableFuture.completedFuture(null); + }, + reason -> { + // Called when the message is acknowledged negatively. + return CompletableFuture.completedFuture(null); + })); + } + // end::message-meta[] + + // tag::broadcast[] + @Inject + @Broadcast + @Channel("prices") + Emitter emitter; + + public void emit(double d) { + emitter.send(d); + } + + @Incoming("prices") + public void handle(double d) { + // Handle the new price + } + + @Incoming("prices") + public void audit(double d) { + // Audit the price change + } + // end::broadcast[] + + static class OverflowExample { + // tag::overflow[] + + // Set the max size to 10 and fail if reached + @OnOverflow(value = OnOverflow.Strategy.BUFFER, bufferSize = 10) + @Inject + @Channel("channel") + Emitter emitterWithBuffer; + + // [DANGER ZONE] no limit + @OnOverflow(OnOverflow.Strategy.UNBOUNDED_BUFFER) + @Inject + @Channel("channel") + Emitter danger; + + // Drop the new messages if the size is reached + @OnOverflow(OnOverflow.Strategy.DROP) + @Inject + @Channel("channel") + Emitter dropping; + + // Drop the previously sent messages if the size is reached + @OnOverflow(OnOverflow.Strategy.LATEST) + @Inject + @Channel("channel") + Emitter dropOldMessages; + + // end::overflow[] + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/emitter/EmitterFactoryExamples.java b/content/docs/smallrye-reactive-messaging/4/_examples/emitter/EmitterFactoryExamples.java new file mode 100644 index 000000000..d9884a773 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/emitter/EmitterFactoryExamples.java @@ -0,0 +1,88 @@ +package emitter; + +import java.util.concurrent.Flow.Publisher; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; +import jakarta.enterprise.inject.spi.InjectionPoint; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.ChannelRegistry; +import io.smallrye.reactive.messaging.EmitterConfiguration; +import io.smallrye.reactive.messaging.EmitterFactory; +import io.smallrye.reactive.messaging.EmitterType; +import io.smallrye.reactive.messaging.MessagePublisherProvider; +import io.smallrye.reactive.messaging.annotations.EmitterFactoryFor; +import io.smallrye.reactive.messaging.providers.extension.ChannelProducer; + +public class EmitterFactoryExamples { + + // tag::custom-emitter-declaration[] + + public interface CustomEmitter extends EmitterType { + + > void sendAndForget(M msg); + + } + + public static class CustomEmitterImpl implements CustomEmitter, MessagePublisherProvider { + + Publisher> publisher; + + public CustomEmitterImpl(EmitterConfiguration configuration, long defaultBufferSize) { + //... initialize emitter with configuration + } + + @Override + public Publisher> getPublisher() { + return publisher; + } + + @Override + public > void sendAndForget(M msg) { + //... send to stream + } + } + // end::custom-emitter-declaration[] + + // tag::custom-emitter-factory[] + @EmitterFactoryFor(CustomEmitter.class) + @ApplicationScoped + public static class CustomEmitterFactory implements EmitterFactory> { + + @Inject + ChannelRegistry channelRegistry; + + @Override + public CustomEmitterImpl createEmitter(EmitterConfiguration configuration, long defaultBufferSize) { + return new CustomEmitterImpl<>(configuration, defaultBufferSize); + } + + @Produces + @Channel("") // Stream name is ignored during type-safe resolution + CustomEmitter produce(InjectionPoint injectionPoint) { + String channelName = ChannelProducer.getChannelName(injectionPoint); + return channelRegistry.getEmitter(channelName, CustomEmitter.class); + } + } + // end::custom-emitter-factory[] + + // tag::custom-emitter-usage[] + + @Inject + @Channel("custom-emitter-channel") + CustomEmitter customEmitter; + + //... + + public void emitMessage() { + customEmitter.sendAndForget(Message.of("a")); + customEmitter.sendAndForget(Message.of("b")); + customEmitter.sendAndForget(Message.of("c")); + } + // end::custom-emitter-usage[] + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/emitter/MutinyExamples.java b/content/docs/smallrye-reactive-messaging/4/_examples/emitter/MutinyExamples.java new file mode 100644 index 000000000..d6d2321b5 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/emitter/MutinyExamples.java @@ -0,0 +1,33 @@ +package emitter; + +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; + +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.subscription.Cancellable; +import io.smallrye.reactive.messaging.MutinyEmitter; + +public class MutinyExamples { + // tag::uni[] + @Inject + @Channel("prices") + MutinyEmitter emitter; + + public Uni send(double d) { + return emitter.send(d); + } + // end::uni[] + + // tag::uni-await[] + public void sendAwait(double d) { + emitter.sendAndAwait(d); + } + // end::uni-await[] + + // tag::uni-forget[] + public Cancellable sendForget(double d) { + return emitter.sendAndForget(d); + } + // end::uni-forget[] +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/emitter/MyImperativeBean.java b/content/docs/smallrye-reactive-messaging/4/_examples/emitter/MyImperativeBean.java new file mode 100644 index 000000000..a4619004f --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/emitter/MyImperativeBean.java @@ -0,0 +1,24 @@ +package emitter; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +// tag::intro[] +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Emitter; + +@ApplicationScoped +public class MyImperativeBean { + + @Inject + @Channel("prices") + Emitter emitter; + + // ... + + public void send(double d) { + emitter.send(d); + } +} + +// end::intro[] diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/generation/GenerationExamples.java b/content/docs/smallrye-reactive-messaging/4/_examples/generation/GenerationExamples.java new file mode 100644 index 000000000..c4c0afdc5 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/generation/GenerationExamples.java @@ -0,0 +1,95 @@ +package generation; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.Flow; +import java.util.concurrent.atomic.AtomicInteger; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; + +public class GenerationExamples { + + AtomicInteger counter = new AtomicInteger(); + + // tag::message-sync[] + @Outgoing("my-channel") + public Message generateMessagesSynchronously() { + return Message.of(counter.getAndIncrement()); + } + // end::message-sync[] + + // tag::message-cs[] + @Outgoing("my-channel") + public CompletionStage> generateMessagesAsCompletionStage() { + return asyncClient.poll() + .thenApply(Message::of); + } + // end::message-cs[] + + public static class Price { + final double price; + + public Price(double p) { + this.price = p; + } + } + + private Pollable asyncClient = () -> CompletableFuture.completedFuture(new Price(10.0)); + + private interface Pollable { + CompletionStage poll(); + } + + // tag::message-uni[] + @Outgoing("my-channel") + public Uni> generateMessagesAsync() { + return Uni.createFrom().item(() -> Message.of(counter.getAndIncrement())); + } + // end::message-uni[] + + // tag::message-stream[] + public Flow.Publisher> generateMessageStream() { + Multi multi = reactiveClient.getStream(); + return multi.map(Message::of); + } + // end::message-stream[] + + private ReactiveClient reactiveClient = () -> Multi.createFrom().items("a", "b", "c"); + + private interface ReactiveClient { + Multi getStream(); + } + + // tag::payload-sync[] + @Outgoing("my-channel") + public Integer generatePayloadsSynchronously() { + return counter.getAndIncrement(); + } + // end::payload-sync[] + + // tag::payload-cs[] + @Outgoing("my-channel") + public CompletionStage generatePayloadsAsCompletionStage() { + return asyncClient.poll(); + } + // end::payload-cs[] + + // tag::payload-uni[] + @Outgoing("my-channel") + public Uni generatePayloadsAsync() { + return Uni.createFrom().item(() -> counter.getAndIncrement()); + } + // end::payload-uni[] + + // tag::payload-stream[] + @Outgoing("my-channel") + public Multi generatePayloadsStream() { + Multi multi = reactiveClient.getStream(); + return multi; + } + // end::payload-stream[] +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/genericpayload/GenericPayloadExample.java b/content/docs/smallrye-reactive-messaging/4/_examples/genericpayload/GenericPayloadExample.java new file mode 100644 index 000000000..583cfcec0 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/genericpayload/GenericPayloadExample.java @@ -0,0 +1,49 @@ +package genericpayload; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Metadata; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; +import io.smallrye.reactive.messaging.GenericPayload; +import io.smallrye.reactive.messaging.MutinyEmitter; +import messages.MyMetadata; + +@ApplicationScoped +public class GenericPayloadExample { + + // tag::code[] + @Outgoing("out") + Multi> produce() { + return Multi.createFrom().range(0, 100) + .map(i -> GenericPayload.of(">> " + i, Metadata.of(new MyMetadata()))); + } + // end::code[] + + // tag::injection[] + @Incoming("in") + @Outgoing("out") + GenericPayload process(int payload, MyMetadata metadata) { + // use the injected metadata + String id = metadata.getId(); + return GenericPayload.of(">> " + payload + " " + id, + Metadata.of(metadata, new MyMetadata("Bob", "Alice"))); + } + // end::injection[] + + // tag::emitter[] + @Inject + @Channel("out") + MutinyEmitter> emitter; + + public void emitWithMetadata(String payload) { + emitter.sendAndForget( + GenericPayload.of(payload, Metadata.of(new MyMetadata()))); + } + // end::emitter[] + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/incomings/IncomingsExamples.java b/content/docs/smallrye-reactive-messaging/4/_examples/incomings/IncomingsExamples.java new file mode 100644 index 000000000..f1d727df7 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/incomings/IncomingsExamples.java @@ -0,0 +1,16 @@ +package incomings; + +import org.eclipse.microprofile.reactive.messaging.Incoming; + +public class IncomingsExamples { + + //tag::code[] + @Incoming("channel-1") + @Incoming("channel-2") + public String process(String s) { + // get messages from channel-1 and channel-2 + return s.toUpperCase(); + } + //end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/incomings/MetadataInjectionExample.java b/content/docs/smallrye-reactive-messaging/4/_examples/incomings/MetadataInjectionExample.java new file mode 100644 index 000000000..79c3cc3ef --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/incomings/MetadataInjectionExample.java @@ -0,0 +1,44 @@ +package incomings; + +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Uni; +import jdk.jfr.Period; +import messages.MyMetadata; + +public class MetadataInjectionExample { + + //tag::code[] + @Incoming("in") + @Outgoing("out") + public String process(String payload, MyMetadata metadata) { + // ... + return payload.toUpperCase(); + } + + @Incoming("in") + @Outgoing("out") + public Uni process2(String payload, MyMetadata metadata) { + // ... + return Uni.createFrom().item(payload.toUpperCase()); + } + + // ... + @Incoming("in") + public void consume(String payload, Optional metadata, Period metadata2) { + // ... + } + + @Incoming("in") + public CompletionStage consume2(String payload, Optional metadata, Period metadata2) { + // ... + return CompletableFuture.completedFuture(null); + } + //end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/interceptors/MyIncomingInterceptor.java b/content/docs/smallrye-reactive-messaging/4/_examples/interceptors/MyIncomingInterceptor.java new file mode 100644 index 000000000..c322a7553 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/interceptors/MyIncomingInterceptor.java @@ -0,0 +1,28 @@ +package interceptors; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.common.annotation.Identifier; +import io.smallrye.reactive.messaging.IncomingInterceptor; + +@Identifier("channel-a") +@ApplicationScoped +public class MyIncomingInterceptor implements IncomingInterceptor { + + @Override + public Message afterMessageReceive(Message message) { + return message.withPayload("changed " + message.getPayload()); + } + + @Override + public void onMessageAck(Message message) { + // Called after message ack + } + + @Override + public void onMessageNack(Message message, Throwable failure) { + // Called after message nack + } +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/interceptors/MyOutgoingInterceptor.java b/content/docs/smallrye-reactive-messaging/4/_examples/interceptors/MyOutgoingInterceptor.java new file mode 100644 index 000000000..6efdc8835 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/interceptors/MyOutgoingInterceptor.java @@ -0,0 +1,30 @@ +package interceptors; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.common.annotation.Identifier; +import io.smallrye.reactive.messaging.OutgoingInterceptor; +import io.smallrye.reactive.messaging.OutgoingMessageMetadata; + +@Identifier("channel-a") +@ApplicationScoped +public class MyOutgoingInterceptor implements OutgoingInterceptor { + + @Override + public Message onMessage(Message message) { + return message.withPayload("changed " + message.getPayload()); + } + + @Override + public void onMessageAck(Message message) { + message.getMetadata(OutgoingMessageMetadata.class) + .ifPresent(m -> m.getResult()); + } + + @Override + public void onMessageNack(Message message, Throwable failure) { + + } +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/jms/inbound/JmsMetadataExample.java b/content/docs/smallrye-reactive-messaging/4/_examples/jms/inbound/JmsMetadataExample.java new file mode 100644 index 000000000..adb2a0a65 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/jms/inbound/JmsMetadataExample.java @@ -0,0 +1,25 @@ +package jms.inbound; + +import java.util.Optional; + +import jakarta.jms.Destination; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.jms.IncomingJmsMessageMetadata; + +public class JmsMetadataExample { + + public void metadata() { + Message incoming = Message.of(12.0); + // tag::code[] + Optional metadata = incoming.getMetadata(IncomingJmsMessageMetadata.class); + metadata.ifPresent(meta -> { + long expiration = meta.getExpiration(); + Destination destination = meta.getDestination(); + String value = meta.getStringProperty("my-property"); + }); + // end::code[] + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/jms/inbound/JmsPriceConsumer.java b/content/docs/smallrye-reactive-messaging/4/_examples/jms/inbound/JmsPriceConsumer.java new file mode 100644 index 000000000..ac4cdd774 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/jms/inbound/JmsPriceConsumer.java @@ -0,0 +1,15 @@ +package jms.inbound; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; + +@ApplicationScoped +public class JmsPriceConsumer { + + @Incoming("prices") + public void consume(double price) { + // process your price. + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/jms/inbound/JmsPriceMessageConsumer.java b/content/docs/smallrye-reactive-messaging/4/_examples/jms/inbound/JmsPriceMessageConsumer.java new file mode 100644 index 000000000..6dfdf485e --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/jms/inbound/JmsPriceMessageConsumer.java @@ -0,0 +1,21 @@ +package jms.inbound; + +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; + +@ApplicationScoped +public class JmsPriceMessageConsumer { + + @Incoming("prices") + public CompletionStage consume(Message price) { + // process your price. + + // Acknowledge the incoming message + return price.ack(); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/jms/outbound/JmsOutboundMetadataExample.java b/content/docs/smallrye-reactive-messaging/4/_examples/jms/outbound/JmsOutboundMetadataExample.java new file mode 100644 index 000000000..6142bccf7 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/jms/outbound/JmsOutboundMetadataExample.java @@ -0,0 +1,23 @@ +package jms.outbound; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.jms.JmsProperties; +import io.smallrye.reactive.messaging.jms.OutgoingJmsMessageMetadata; + +public class JmsOutboundMetadataExample { + + public Message metadata(Message incoming) { + + // tag::code[] + OutgoingJmsMessageMetadata metadata = OutgoingJmsMessageMetadata.builder() + .withProperties(JmsProperties.builder().with("some-property", "some-value").build()) + .build(); + + // Create a new message from the `incoming` message + // Add `metadata` to the metadata from the `incoming` message. + return incoming.addMetadata(metadata); + // end::code[] + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/jms/outbound/JmsPriceMessageProducer.java b/content/docs/smallrye-reactive-messaging/4/_examples/jms/outbound/JmsPriceMessageProducer.java new file mode 100644 index 000000000..bd57c863b --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/jms/outbound/JmsPriceMessageProducer.java @@ -0,0 +1,26 @@ +package jms.outbound; + +import java.time.Duration; +import java.util.Random; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +@ApplicationScoped +public class JmsPriceMessageProducer { + + private Random random = new Random(); + + @Outgoing("prices") + public Multi> generate() { + // Build an infinite stream of random prices + // It emits a price every second + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .map(x -> Message.of(random.nextDouble())); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/jms/outbound/JmsPriceProducer.java b/content/docs/smallrye-reactive-messaging/4/_examples/jms/outbound/JmsPriceProducer.java new file mode 100644 index 000000000..b693a91f7 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/jms/outbound/JmsPriceProducer.java @@ -0,0 +1,25 @@ +package jms.outbound; + +import java.time.Duration; +import java.util.Random; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +@ApplicationScoped +public class JmsPriceProducer { + + private Random random = new Random(); + + @Outgoing("prices") + public Multi generate() { + // Build an infinite stream of random prices + // It emits a price every second + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .map(x -> random.nextDouble()); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/kafka/companion/ConsumerTest.java b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/companion/ConsumerTest.java new file mode 100644 index 000000000..fe0e60637 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/companion/ConsumerTest.java @@ -0,0 +1,45 @@ +package kafka.companion; + +import static org.testcontainers.shaded.org.awaitility.Awaitility.await; + +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; + +import org.apache.kafka.common.TopicPartition; +import org.junit.jupiter.api.Test; + +import io.smallrye.reactive.messaging.kafka.companion.ConsumerBuilder; +import io.smallrye.reactive.messaging.kafka.companion.test.KafkaCompanionTestBase; + +public class ConsumerTest extends KafkaCompanionTestBase { + + @Test + public void fromTopics() { + // tag::topics[] + companion.consumeIntegers().fromTopics("topic1", "topic2"); + // end::topics[] + } + + @Test + public void fromOffset() { + // tag::offsets[] + Map offsets = new HashMap<>(); + offsets.put(new TopicPartition("topic1", 0), 100L); + offsets.put(new TopicPartition("topic2", 0), 100L); + companion.consumeIntegers().fromOffsets(offsets, Duration.ofSeconds(10)); + // end::offsets[] + } + + @Test + public void committed() { + // tag::committed[] + ConsumerBuilder consumer = companion.consumeIntegers() + .withAutoCommit(); + consumer.fromTopics("topic"); + // ... + await().untilAsserted(consumer::waitForAssignment); + consumer.committed(new TopicPartition("topic", 1)); + // end::committed[] + } +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/kafka/companion/KafkaTest.java b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/companion/KafkaTest.java new file mode 100644 index 000000000..e10717dc0 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/companion/KafkaTest.java @@ -0,0 +1,76 @@ +package kafka.companion; + +import java.time.Duration; + +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.apache.kafka.common.serialization.Deserializer; +import org.apache.kafka.common.serialization.Serializer; +import org.junit.jupiter.api.Assertions; + +import io.smallrye.reactive.messaging.kafka.companion.ConsumerBuilder; +import io.smallrye.reactive.messaging.kafka.companion.ConsumerTask; +import io.smallrye.reactive.messaging.kafka.companion.KafkaCompanion; +import io.smallrye.reactive.messaging.kafka.companion.ProducerBuilder; + +public class KafkaTest { + + public void newCompanion() { + // tag::companion[] + // Create companion with bootstrap servers and API timeout (default is 10 seconds) + KafkaCompanion companion = new KafkaCompanion("localhost:9092", Duration.ofSeconds(5)); + + // Create producer and start producer task + ProducerBuilder producer = companion.produceIntegers() + .withClientId("my-producer") + .withProp("max.block.ms", "5000"); + producer.usingGenerator(i -> new ProducerRecord<>("topic", i), 100); + + // Create consumer and start consumer task + ConsumerBuilder consumer = companion.consumeIntegers() + .withGroupId("my-group") + .withCommitAsyncWhen(record -> true); + ConsumerTask records = consumer.fromTopics("topic", Duration.ofSeconds(10)); + // Await completion and assert consumed record count + Assertions.assertEquals(records.awaitCompletion().count(), 100); + // end::companion[] + } + + public void serdes() { + // tag::serdes[] + KafkaCompanion companion = new KafkaCompanion("localhost:9092"); + + // Register serde to the companion + companion.registerSerde(Orders.class, new OrdersSerializer(), new OrdersDeserializer()); + + // Companion will configure consumer accordingly + ConsumerTask orders = companion.consume(Integer.class, Orders.class) + .fromTopics("orders", 1000).awaitCompletion(); + + for (ConsumerRecord order : orders) { + // ... check consumed records + } + // end::serdes[] + } + + public static class Orders { + + } + + public static class OrdersSerializer implements Serializer { + + @Override + public byte[] serialize(String s, Orders orders) { + return new byte[0]; + } + } + + public static class OrdersDeserializer implements Deserializer { + + @Override + public Orders deserialize(String s, byte[] bytes) { + return null; + } + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/kafka/companion/KafkaWithBaseTest.java b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/companion/KafkaWithBaseTest.java new file mode 100644 index 000000000..63bd8634a --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/companion/KafkaWithBaseTest.java @@ -0,0 +1,32 @@ +package kafka.companion; + +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import io.smallrye.reactive.messaging.kafka.companion.ConsumerTask; +import io.smallrye.reactive.messaging.kafka.companion.ProducerTask; +import io.smallrye.reactive.messaging.kafka.companion.test.KafkaCompanionTestBase; + +// tag::code[] +public class KafkaWithBaseTest extends KafkaCompanionTestBase { + + @Test + public void testWithBase() { + // companion is created by the base class + + // produce 100 records to messages topic + ProducerTask producerTask = companion.produceIntegers() + .usingGenerator(i -> new ProducerRecord<>("messages", i), 100); + long msgCount = producerTask.awaitCompletion().count(); + Assertions.assertEquals(msgCount, 100); + + // consume 100 records from messages topic + ConsumerTask consumerTask = companion.consumeIntegers() + .fromTopics("messages", 100); + ConsumerRecord lastRecord = consumerTask.awaitCompletion().getLastRecord(); + Assertions.assertEquals(lastRecord.value(), 99); + } +} +// end::code[] diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/kafka/companion/OffsetsTest.java b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/companion/OffsetsTest.java new file mode 100644 index 000000000..f7b00fa6b --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/companion/OffsetsTest.java @@ -0,0 +1,36 @@ +package kafka.companion; + +import java.util.Collection; + +import org.apache.kafka.clients.admin.ConsumerGroupListing; +import org.apache.kafka.clients.admin.OffsetSpec; +import org.apache.kafka.common.TopicPartition; +import org.junit.jupiter.api.Test; + +import io.smallrye.reactive.messaging.kafka.companion.KafkaCompanion; +import io.smallrye.reactive.messaging.kafka.companion.test.KafkaCompanionTestBase; + +public class OffsetsTest extends KafkaCompanionTestBase { + + @Test + public void offsets() { + // tag::offsets[] + TopicPartition topicPartition = KafkaCompanion.tp("topic", 1); + long latestOffset = companion.offsets().get(topicPartition, OffsetSpec.latest()).offset(); + // end::offsets[] + } + + @Test + public void consumerGroups() { + // tag::consumerGroups[] + Collection consumerGroups = companion.consumerGroups().list(); + for (ConsumerGroupListing consumerGroup : consumerGroups) { + // check consumer groups + } + + TopicPartition topicPartition = KafkaCompanion.tp("topic", 1); + companion.consumerGroups().resetOffsets("consumer-group", topicPartition); + // end::consumerGroups[] + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/kafka/companion/ProducerTest.java b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/companion/ProducerTest.java new file mode 100644 index 000000000..a60561829 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/companion/ProducerTest.java @@ -0,0 +1,34 @@ +package kafka.companion; + +import org.apache.kafka.clients.producer.ProducerRecord; +import org.junit.jupiter.api.Test; + +import io.smallrye.reactive.messaging.kafka.companion.test.KafkaCompanionTestBase; + +public class ProducerTest extends KafkaCompanionTestBase { + + @Test + public void fromRecords() { + // tag::records[] + companion.produce(byte[].class).fromRecords( + new ProducerRecord<>("topic1", "k1", "1".getBytes()), + new ProducerRecord<>("topic1", "k2", "2".getBytes()), + new ProducerRecord<>("topic1", "k3", "3".getBytes())); + // end::records[] + } + + @Test + public void fromGenerator() { + // tag::generator[] + companion.produceIntegers().usingGenerator(i -> new ProducerRecord<>("topic", i), 10); + // end::generator[] + } + + @Test + public void fromCsv() { + // tag::csv[] + companion.produceStrings().fromCsv("records.csv"); + // end::csv[] + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/kafka/companion/TopicsTest.java b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/companion/TopicsTest.java new file mode 100644 index 000000000..01e048611 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/companion/TopicsTest.java @@ -0,0 +1,20 @@ +package kafka.companion; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import io.smallrye.reactive.messaging.kafka.companion.test.KafkaCompanionTestBase; + +public class TopicsTest extends KafkaCompanionTestBase { + + @Test + public void topics() { + // tag::code[] + companion.topics().create("topic1", 1); + companion.topics().createAndWait("topic2", 3); + Assertions.assertEquals(companion.topics().list().size(), 2); + + companion.topics().delete("topic1"); + // end::code[] + } +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/kafka/inbound/Converters.java b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/inbound/Converters.java new file mode 100644 index 000000000..646c5ae54 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/inbound/Converters.java @@ -0,0 +1,30 @@ +package kafka.inbound; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.eclipse.microprofile.reactive.messaging.Incoming; + +import io.smallrye.reactive.messaging.kafka.Record; + +@ApplicationScoped +public class Converters { + + // tag::code[] + @Incoming("topic-a") + public void consume(Record record) { + String key = record.key(); // Can be `null` if the incoming record has no key + String value = record.value(); // Can be `null` if the incoming record has no value + } + + @Incoming("topic-b") + public void consume(ConsumerRecord record) { + String key = record.key(); // Can be `null` if the incoming record has no key + String value = record.value(); // Can be `null` if the incoming record has no value + String topic = record.topic(); + int partition = record.partition(); + // ... + } + // end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/kafka/inbound/KafkaCheckpointExample.java b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/inbound/KafkaCheckpointExample.java new file mode 100644 index 000000000..70e7bf13c --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/inbound/KafkaCheckpointExample.java @@ -0,0 +1,31 @@ +package kafka.inbound; + +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.kafka.commit.CheckpointMetadata; + +@ApplicationScoped +public class KafkaCheckpointExample { + + // tag::code[] + @Incoming("prices") + public CompletionStage consume(Message record) { + // Get the `CheckpointMetadata` from the incoming message + CheckpointMetadata checkpoint = CheckpointMetadata.fromMessage(record); + + // `CheckpointMetadata` allows transforming the processing state + // Applies the given function, starting from the value `0.0` when no previous state exists + checkpoint.transform(0.0, current -> current + record.getPayload(), /* persistOnAck */ true); + + // `persistOnAck` flag set to true, ack will persist the processing state + // associated with the latest offset (per partition). + return record.ack(); + } + // end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/kafka/inbound/KafkaCustomCommit.java b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/inbound/KafkaCustomCommit.java new file mode 100644 index 000000000..f57e3b87a --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/inbound/KafkaCustomCommit.java @@ -0,0 +1,60 @@ +package kafka.inbound; + +import java.util.Collection; +import java.util.function.BiConsumer; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.apache.kafka.common.TopicPartition; + +import io.smallrye.common.annotation.Identifier; +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.kafka.IncomingKafkaRecord; +import io.smallrye.reactive.messaging.kafka.KafkaConnectorIncomingConfiguration; +import io.smallrye.reactive.messaging.kafka.KafkaConsumer; +import io.smallrye.reactive.messaging.kafka.commit.KafkaCommitHandler; +import io.vertx.mutiny.core.Vertx; + +public class KafkaCustomCommit implements KafkaCommitHandler { + + @Override + public Uni handle(IncomingKafkaRecord record) { + // called on message ack + return Uni.createFrom().voidItem(); + } + + @Override + public Uni> received(IncomingKafkaRecord record) { + // called before message processing + return Uni.createFrom().item(record); + } + + @Override + public void terminate(boolean graceful) { + // called on channel shutdown + } + + @Override + public void partitionsAssigned(Collection partitions) { + // called on partitions assignment + } + + @Override + public void partitionsRevoked(Collection partitions) { + // called on partitions revoked + } + + @ApplicationScoped + @Identifier("custom") + public static class Factory implements KafkaCommitHandler.Factory { + + @Override + public KafkaCommitHandler create(KafkaConnectorIncomingConfiguration config, + Vertx vertx, + KafkaConsumer consumer, + BiConsumer reportFailure) { + return new KafkaCustomCommit(/* ... */); + } + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/kafka/inbound/KafkaDeadLetterExample.java b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/inbound/KafkaDeadLetterExample.java new file mode 100644 index 000000000..82cb5af95 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/inbound/KafkaDeadLetterExample.java @@ -0,0 +1,30 @@ +package kafka.inbound; + +import java.nio.charset.StandardCharsets; +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.apache.kafka.common.header.internals.RecordHeaders; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Metadata; + +import io.smallrye.reactive.messaging.kafka.api.OutgoingKafkaRecordMetadata; + +@ApplicationScoped +public class KafkaDeadLetterExample { + + // tag::code[] + @Incoming("in") + public CompletionStage consume(Message message) { + return message.nack(new Exception("Failed!"), Metadata.of( + OutgoingKafkaRecordMetadata.builder() + .withKey("failed-record") + .withHeaders(new RecordHeaders() + .add("my-header", "my-header-value".getBytes(StandardCharsets.UTF_8))) + .build())); + } + // end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/kafka/inbound/KafkaMetadataExample.java b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/inbound/KafkaMetadataExample.java new file mode 100644 index 000000000..8400fe0df --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/inbound/KafkaMetadataExample.java @@ -0,0 +1,36 @@ +package kafka.inbound; + +import java.time.Instant; + +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.kafka.api.IncomingKafkaRecordMetadata; + +public class KafkaMetadataExample { + + @SuppressWarnings("unchecked") + public void metadata() { + Message incoming = Message.of(12.0); + // tag::code[] + IncomingKafkaRecordMetadata metadata = incoming.getMetadata(IncomingKafkaRecordMetadata.class) + .orElse(null); + if (metadata != null) { + // The topic + String topic = metadata.getTopic(); + + // The key + String key = metadata.getKey(); + + // The timestamp + Instant timestamp = metadata.getTimestamp(); + + // The underlying record + ConsumerRecord record = metadata.getRecord(); + + // ... + } + // end::code[] + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/kafka/inbound/KafkaPriceConsumer.java b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/inbound/KafkaPriceConsumer.java new file mode 100644 index 000000000..7d6d28b94 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/inbound/KafkaPriceConsumer.java @@ -0,0 +1,15 @@ +package kafka.inbound; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; + +@ApplicationScoped +public class KafkaPriceConsumer { + + @Incoming("prices") + public void consume(double price) { + // process your price. + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/kafka/inbound/KafkaPriceMessageConsumer.java b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/inbound/KafkaPriceMessageConsumer.java new file mode 100644 index 000000000..324b7b94f --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/inbound/KafkaPriceMessageConsumer.java @@ -0,0 +1,21 @@ +package kafka.inbound; + +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; + +@ApplicationScoped +public class KafkaPriceMessageConsumer { + + @Incoming("prices") + public CompletionStage consume(Message price) { + // process your price. + + // Acknowledge the incoming message (commit the offset) + return price.ack(); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/kafka/inbound/KafkaRebalancedConsumer.java b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/inbound/KafkaRebalancedConsumer.java new file mode 100644 index 000000000..814461dc2 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/inbound/KafkaRebalancedConsumer.java @@ -0,0 +1,22 @@ +package kafka.inbound; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Acknowledgment; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; + +@ApplicationScoped +public class KafkaRebalancedConsumer { + + @Incoming("rebalanced-example") + @Acknowledgment(Acknowledgment.Strategy.NONE) + public CompletionStage consume(Message message) { + // We don't need to ACK messages because in this example we set offset during consumer re-balance + return CompletableFuture.completedFuture(null); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/kafka/inbound/KafkaRebalancedConsumerRebalanceListener.java b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/inbound/KafkaRebalancedConsumerRebalanceListener.java new file mode 100644 index 000000000..512b65117 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/inbound/KafkaRebalancedConsumerRebalanceListener.java @@ -0,0 +1,49 @@ +package kafka.inbound; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Logger; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.apache.kafka.clients.consumer.Consumer; +import org.apache.kafka.clients.consumer.OffsetAndTimestamp; + +import io.smallrye.common.annotation.Identifier; +import io.smallrye.reactive.messaging.kafka.KafkaConsumerRebalanceListener; + +@ApplicationScoped +@Identifier("rebalanced-example.rebalancer") +public class KafkaRebalancedConsumerRebalanceListener implements KafkaConsumerRebalanceListener { + + private static final Logger LOGGER = Logger.getLogger(KafkaRebalancedConsumerRebalanceListener.class.getName()); + + /** + * When receiving a list of partitions will search for the earliest offset within 10 minutes + * and seek the consumer to it. + * + * @param consumer underlying consumer + * @param partitions set of assigned topic partitions + */ + @Override + public void onPartitionsAssigned(Consumer consumer, + Collection partitions) { + long now = System.currentTimeMillis(); + long shouldStartAt = now - 600_000L; //10 minute ago + + Map request = new HashMap<>(); + for (org.apache.kafka.common.TopicPartition partition : partitions) { + LOGGER.info("Assigned " + partition); + request.put(partition, shouldStartAt); + } + Map offsets = consumer + .offsetsForTimes(request); + for (Map.Entry position : offsets.entrySet()) { + long target = position.getValue() == null ? 0L : position.getValue().offset(); + LOGGER.info("Seeking position " + target + " for " + position.getKey()); + consumer.seek(position.getKey(), target); + } + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/kafka/inbound/KafkaRecordBatchExample.java b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/inbound/KafkaRecordBatchExample.java new file mode 100644 index 000000000..85d329af6 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/inbound/KafkaRecordBatchExample.java @@ -0,0 +1,60 @@ +package kafka.inbound; + +import java.util.List; +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.clients.consumer.ConsumerRecords; +import org.apache.kafka.common.TopicPartition; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.TracingMetadata; +import io.smallrye.reactive.messaging.kafka.api.IncomingKafkaRecordBatchMetadata; + +@ApplicationScoped +public class KafkaRecordBatchExample { + + // tag::code[] + @Incoming("prices") + public CompletionStage consumeMessage(Message> msg) { + IncomingKafkaRecordBatchMetadata batchMetadata = msg.getMetadata(IncomingKafkaRecordBatchMetadata.class) + .get(); + for (ConsumerRecord record : batchMetadata.getRecords()) { + int partition = record.partition(); + long offset = record.offset(); + long timestamp = record.timestamp(); + } + // ack will commit the latest offsets (per partition) of the batch. + return msg.ack(); + } + + @Incoming("prices") + public void consumeRecords(ConsumerRecords records) { + for (TopicPartition partition : records.partitions()) { + for (ConsumerRecord record : records.records(partition)) { + //... process messages + } + } + } + // end::code[] + + // tag::batch[] + @Incoming("prices") + public void consumeRecords(ConsumerRecords records, + IncomingKafkaRecordBatchMetadata metadata) { + for (TopicPartition partition : records.partitions()) { + for (ConsumerRecord record : records.records(partition)) { + TracingMetadata tracing = metadata.getMetadataForRecord(record, TracingMetadata.class); + if (tracing != null) { + tracing.getCurrentContext().makeCurrent(); + } + //... process messages + } + } + } + // end::batch[] + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/kafka/inbound/KafkaRecordBatchPayloadExample.java b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/inbound/KafkaRecordBatchPayloadExample.java new file mode 100644 index 000000000..9a3d11100 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/inbound/KafkaRecordBatchPayloadExample.java @@ -0,0 +1,21 @@ +package kafka.inbound; + +import java.util.List; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; + +@ApplicationScoped +public class KafkaRecordBatchPayloadExample { + + // tag::code[] + @Incoming("prices") + public void consume(List prices) { + for (double price : prices) { + // process price + } + } + // end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/kafka/inbound/KafkaShareGroupAckExample.java b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/inbound/KafkaShareGroupAckExample.java new file mode 100644 index 000000000..2ee56bd59 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/inbound/KafkaShareGroupAckExample.java @@ -0,0 +1,29 @@ +package kafka.inbound; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; + +import io.smallrye.reactive.messaging.kafka.queues.ShareGroupAcknowledgement; + +@ApplicationScoped +public class KafkaShareGroupAckExample { + + // tag::code[] + @Incoming("queue") + public void consume(double price, ShareGroupAcknowledgement ack) { + if (price < 0) { + ack.reject(); // Permanently reject the record + } else if (needsRetry(price)) { + ack.release(); // Release for re-delivery to another consumer + } else { + ack.accept(); // Mark as successfully processed + } + } + // end::code[] + + private boolean needsRetry(double price) { + return false; + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/kafka/inbound/KafkaShareGroupBatchAckExample.java b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/inbound/KafkaShareGroupBatchAckExample.java new file mode 100644 index 000000000..383d4ec6c --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/inbound/KafkaShareGroupBatchAckExample.java @@ -0,0 +1,30 @@ +package kafka.inbound; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.clients.consumer.ConsumerRecords; +import org.eclipse.microprofile.reactive.messaging.Incoming; + +import io.smallrye.reactive.messaging.kafka.api.IncomingKafkaRecordBatchMetadata; +import io.smallrye.reactive.messaging.kafka.queues.ShareGroupAcknowledgement; + +@ApplicationScoped +public class KafkaShareGroupBatchAckExample { + + // tag::code[] + @Incoming("queue") + public void consume(ConsumerRecords records, + IncomingKafkaRecordBatchMetadata batchMetadata) { + for (ConsumerRecord record : records) { + ShareGroupAcknowledgement ack = batchMetadata.getMetadataForRecord(record, ShareGroupAcknowledgement.class); + if (record.value() < 0) { + ack.reject(); // Reject invalid records + } else { + ack.accept(); // Accept valid records + } + } + } + // end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/kafka/inbound/KafkaShareGroupConsumer.java b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/inbound/KafkaShareGroupConsumer.java new file mode 100644 index 000000000..5df915512 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/inbound/KafkaShareGroupConsumer.java @@ -0,0 +1,15 @@ +package kafka.inbound; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; + +@ApplicationScoped +public class KafkaShareGroupConsumer { + + @Incoming("queue") + public void consume(double price) { + // process your price. + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/kafka/inbound/KafkaShareGroupNackExample.java b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/inbound/KafkaShareGroupNackExample.java new file mode 100644 index 000000000..ffebef1c2 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/inbound/KafkaShareGroupNackExample.java @@ -0,0 +1,41 @@ +package kafka.inbound; + +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.apache.kafka.clients.consumer.AcknowledgeType; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Metadata; + +import io.smallrye.reactive.messaging.kafka.queues.ShareGroupAcknowledgement; + +@ApplicationScoped +public class KafkaShareGroupNackExample { + + // tag::code[] + @Incoming("queue") + public CompletionStage consume(Message msg) { + try { + process(msg.getPayload()); + return msg.ack(); + } catch (TransientException e) { + // Release for re-delivery + return msg.nack(e, + Metadata.of(ShareGroupAcknowledgement.from(AcknowledgeType.RELEASE))); + } catch (Exception e) { + // Reject permanently + return msg.nack(e, + Metadata.of(ShareGroupAcknowledgement.from(AcknowledgeType.REJECT))); + } + } + // end::code[] + + private void process(double price) { + } + + private static class TransientException extends RuntimeException { + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/kafka/inbound/MyCheckpointStateStore.java b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/inbound/MyCheckpointStateStore.java new file mode 100644 index 000000000..3387c4cc6 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/inbound/MyCheckpointStateStore.java @@ -0,0 +1,68 @@ +package kafka.inbound; + +import java.util.Collection; +import java.util.Map; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.apache.kafka.clients.consumer.ConsumerConfig; +import org.apache.kafka.common.TopicPartition; + +import io.smallrye.common.annotation.Identifier; +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.kafka.KafkaConnectorIncomingConfiguration; +import io.smallrye.reactive.messaging.kafka.KafkaConsumer; +import io.smallrye.reactive.messaging.kafka.commit.CheckpointStateStore; +import io.smallrye.reactive.messaging.kafka.commit.ProcessingState; +import io.vertx.mutiny.core.Vertx; + +public class MyCheckpointStateStore implements CheckpointStateStore { + + private final String consumerGroupId; + private final Class stateType; + + public MyCheckpointStateStore(String consumerGroupId, Class stateType) { + this.consumerGroupId = consumerGroupId; + this.stateType = stateType; + } + + /** + * Can be used with + * {@code mp.reactive.messaging.incoming.my-channel.commit-strategy=checkpoint} + * {@code mp.reactive.messaging.incoming.my-channel.checkpoint.state-store=my-store} + */ + @ApplicationScoped + @Identifier("my-store") + public static class Factory implements CheckpointStateStore.Factory { + + @Override + public CheckpointStateStore create(KafkaConnectorIncomingConfiguration config, + Vertx vertx, + KafkaConsumer consumer, + Class stateType /* if configured, otherwise null */) { + String consumerGroupId = (String) consumer.configuration().get(ConsumerConfig.GROUP_ID_CONFIG); + return new MyCheckpointStateStore(consumerGroupId, stateType); + } + } + + @Override + public Uni>> fetchProcessingState(Collection partitions) { + // Called on Vert.x event loop + // Return a Uni completing with the map of topic-partition to processing state + // The Uni will be subscribed also on Vert.x event loop + return Uni.createFrom().nullItem(); + } + + @Override + public Uni persistProcessingState(Map> state) { + // Called on Vert.x event loop + // Return a Uni completing with void when the given states are persisted + // The Uni will be subscribed also on Vert.x event loop + return Uni.createFrom().voidItem(); + } + + @Override + public void close() { + /* Called when channel is closing, no-op by default */ + } +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/kafka/outbound/KafkaCloudEventMultipleProcessors.java b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/outbound/KafkaCloudEventMultipleProcessors.java new file mode 100644 index 000000000..17cea321d --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/outbound/KafkaCloudEventMultipleProcessors.java @@ -0,0 +1,39 @@ +package kafka.outbound; + +import java.net.URI; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.reactive.messaging.ce.OutgoingCloudEventMetadata; + +@ApplicationScoped +public class KafkaCloudEventMultipleProcessors { + + @Incoming("source") + @Outgoing("processed") + public Message process(Message in) { + return in.addMetadata(OutgoingCloudEventMetadata.builder() + .withId("id-" + in.getPayload()) + .withType("greeting") + .build()); + } + + @SuppressWarnings("unchecked") + @Incoming("processed") + @Outgoing("cloud-events") + public Message process2(Message in) { + OutgoingCloudEventMetadata metadata = in + .getMetadata(OutgoingCloudEventMetadata.class) + .orElseGet(() -> OutgoingCloudEventMetadata.builder().build()); + + return in.addMetadata(OutgoingCloudEventMetadata.from(metadata) + .withSource(URI.create("source://me")) + .withSubject("test") + .build()); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/kafka/outbound/KafkaCloudEventProcessor.java b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/outbound/KafkaCloudEventProcessor.java new file mode 100644 index 000000000..8b5d4d91c --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/outbound/KafkaCloudEventProcessor.java @@ -0,0 +1,25 @@ +package kafka.outbound; + +import java.net.URI; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.reactive.messaging.ce.OutgoingCloudEventMetadata; + +@ApplicationScoped +public class KafkaCloudEventProcessor { + + @Outgoing("cloud-events") + public Message toCloudEvents(Message in) { + return in.addMetadata(OutgoingCloudEventMetadata.builder() + .withId("id-" + in.getPayload()) + .withType("greetings") + .withSource(URI.create("http://example.com")) + .withSubject("greeting-message") + .build()); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/kafka/outbound/KafkaCustomHeaderReplier.java b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/outbound/KafkaCustomHeaderReplier.java new file mode 100644 index 000000000..9612a1c60 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/outbound/KafkaCustomHeaderReplier.java @@ -0,0 +1,39 @@ +package kafka.outbound; + +import java.util.Random; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.apache.kafka.common.header.Header; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.reactive.messaging.kafka.reply.KafkaRequestReply; + +@ApplicationScoped +public class KafkaCustomHeaderReplier { + + Random rand = new Random(); + + @Incoming("request") + @Outgoing("reply") + ProducerRecord process(ConsumerRecord request) { + Header topicHeader = request.headers().lastHeader("MY_TOPIC"); + if (topicHeader == null) { + // Skip + return null; + } + String myTopic = new String(topicHeader.value()); + int generateValue = rand.nextInt(100); + Header partitionHeader = request.headers().lastHeader("MY_PARTITION"); + if (partitionHeader == null) { + // Propagate incoming headers, including the correlation id header + return new ProducerRecord<>(myTopic, null, request.key(), generateValue, request.headers()); + } + // Send the replies to extracted myTopic-myPartition + int myPartition = KafkaRequestReply.replyPartitionFromBytes(partitionHeader.value()); + return new ProducerRecord<>(myTopic, myPartition, request.key(), generateValue, request.headers()); + } +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/kafka/outbound/KafkaExactlyOnceProcessor.java b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/outbound/KafkaExactlyOnceProcessor.java new file mode 100644 index 000000000..49b973232 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/outbound/KafkaExactlyOnceProcessor.java @@ -0,0 +1,34 @@ +package kafka.outbound; + +import java.util.List; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.kafka.KafkaRecord; +import io.smallrye.reactive.messaging.kafka.Record; +import io.smallrye.reactive.messaging.kafka.transactions.KafkaTransactions; + +@ApplicationScoped +public class KafkaExactlyOnceProcessor { + + @Inject + @Channel("tx-out-example") + KafkaTransactions txProducer; + + @Incoming("in-channel") + public Uni emitInTransaction(Message>> batch) { + return txProducer.withTransaction(batch, emitter -> { + for (Record record : batch.getPayload()) { + emitter.send(KafkaRecord.of(record.key(), record.value() + 1)); + } + return Uni.createFrom().voidItem(); + }); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/kafka/outbound/KafkaOutboundDynamicTopicExample.java b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/outbound/KafkaOutboundDynamicTopicExample.java new file mode 100644 index 000000000..cca404431 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/outbound/KafkaOutboundDynamicTopicExample.java @@ -0,0 +1,25 @@ +package kafka.outbound; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.kafka.api.OutgoingKafkaRecordMetadata; + +public class KafkaOutboundDynamicTopicExample { + + public Message metadata(Message incoming) { + // tag::code[] + String topicName = selectTopicFromIncommingMessage(incoming); + OutgoingKafkaRecordMetadata metadata = OutgoingKafkaRecordMetadata. builder() + .withTopic(topicName) + .build(); + + // Create a new message from the `incoming` message + // Add `metadata` to the metadata from the `incoming` message. + return incoming.addMetadata(metadata); + // end::code[] + } + + private String selectTopicFromIncommingMessage(Message incoming) { + return "fake"; + } +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/kafka/outbound/KafkaOutboundMetadataExample.java b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/outbound/KafkaOutboundMetadataExample.java new file mode 100644 index 000000000..645f290d2 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/outbound/KafkaOutboundMetadataExample.java @@ -0,0 +1,25 @@ +package kafka.outbound; + +import org.apache.kafka.common.header.internals.RecordHeaders; +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.kafka.api.OutgoingKafkaRecordMetadata; + +public class KafkaOutboundMetadataExample { + + public Message metadata(Message incoming) { + // tag::code[] + // Creates an OutgoingKafkaRecordMetadata + // The type parameter is the type of the record's key + OutgoingKafkaRecordMetadata metadata = OutgoingKafkaRecordMetadata. builder() + .withKey("my-key") + .withHeaders(new RecordHeaders().add("my-header", "value".getBytes())) + .build(); + + // Create a new message from the `incoming` message + // Add `metadata` to the metadata from the `incoming` message. + return incoming.addMetadata(metadata); + // end::code[] + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/kafka/outbound/KafkaPooledExactlyOnceProcessor.java b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/outbound/KafkaPooledExactlyOnceProcessor.java new file mode 100644 index 000000000..8bdf4142c --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/outbound/KafkaPooledExactlyOnceProcessor.java @@ -0,0 +1,32 @@ +package kafka.outbound; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Incoming; + +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.annotations.Blocking; +import io.smallrye.reactive.messaging.kafka.KafkaRecord; +import io.smallrye.reactive.messaging.kafka.api.IncomingKafkaRecordMetadata; +import io.smallrye.reactive.messaging.kafka.transactions.KafkaTransactions; + +@ApplicationScoped +public class KafkaPooledExactlyOnceProcessor { + + @Inject + @Channel("tx-out-example") + KafkaTransactions txProducer; + + @Incoming("in-channel") + @Blocking(ordered = false) + public void process(ConsumerRecord record, IncomingKafkaRecordMetadata metadata) { + txProducer.withTransactionAndAwait(metadata, emitter -> { + emitter.send(KafkaRecord.of(record.key(), record.value() + 1)); + return Uni.createFrom().voidItem(); + }); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/kafka/outbound/KafkaPriceMessageProducer.java b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/outbound/KafkaPriceMessageProducer.java new file mode 100644 index 000000000..52e86b093 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/outbound/KafkaPriceMessageProducer.java @@ -0,0 +1,26 @@ +package kafka.outbound; + +import java.time.Duration; +import java.util.Random; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +@ApplicationScoped +public class KafkaPriceMessageProducer { + + private final Random random = new Random(); + + @Outgoing("prices-out") + public Multi> generate() { + // Build an infinite stream of random prices + // It emits a price every second + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .map(x -> Message.of(random.nextDouble())); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/kafka/outbound/KafkaPriceProcessor.java b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/outbound/KafkaPriceProcessor.java new file mode 100644 index 000000000..7d423ae89 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/outbound/KafkaPriceProcessor.java @@ -0,0 +1,19 @@ +package kafka.outbound; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +@ApplicationScoped +public class KafkaPriceProcessor { + + // tag::code[] + @Incoming("in") + @Outgoing("out") + public double process(int in) { + return in * 0.88; + } + // end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/kafka/outbound/KafkaPriceProducer.java b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/outbound/KafkaPriceProducer.java new file mode 100644 index 000000000..220a0214f --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/outbound/KafkaPriceProducer.java @@ -0,0 +1,25 @@ +package kafka.outbound; + +import java.time.Duration; +import java.util.Random; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +@ApplicationScoped +public class KafkaPriceProducer { + + private final Random random = new Random(); + + @Outgoing("prices-out") + public Multi generate() { + // Build an infinite stream of random prices + // It emits a price every second + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .map(x -> random.nextDouble()); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/kafka/outbound/KafkaProducerRecordExample.java b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/outbound/KafkaProducerRecordExample.java new file mode 100644 index 000000000..0fb3f3dc4 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/outbound/KafkaProducerRecordExample.java @@ -0,0 +1,18 @@ +package kafka.outbound; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.apache.kafka.clients.producer.ProducerRecord; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +@ApplicationScoped +public class KafkaProducerRecordExample { + + // tag::code[] + @Outgoing("out") + public ProducerRecord generate() { + return new ProducerRecord<>("my-topic", "key", "value"); + } + // end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/kafka/outbound/KafkaRecordExample.java b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/outbound/KafkaRecordExample.java new file mode 100644 index 000000000..560850605 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/outbound/KafkaRecordExample.java @@ -0,0 +1,21 @@ +package kafka.outbound; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.reactive.messaging.kafka.Record; + +@ApplicationScoped +public class KafkaRecordExample { + + // tag::code[] + @Incoming("in") + @Outgoing("out") + public Record process(String in) { + return Record.of("my-key", in); + } + // end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/kafka/outbound/KafkaReplier.java b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/outbound/KafkaReplier.java new file mode 100644 index 000000000..8bad449e8 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/outbound/KafkaReplier.java @@ -0,0 +1,20 @@ +package kafka.outbound; + +import java.util.Random; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +@ApplicationScoped +public class KafkaReplier { + + Random rand = new Random(); + + @Incoming("request") + @Outgoing("reply") + int handleRequest(String request) { + return rand.nextInt(100); + } +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/kafka/outbound/KafkaRequestReplyEmitter.java b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/outbound/KafkaRequestReplyEmitter.java new file mode 100644 index 000000000..c1994d090 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/outbound/KafkaRequestReplyEmitter.java @@ -0,0 +1,21 @@ +package kafka.outbound; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; + +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.kafka.reply.KafkaRequestReply; + +@ApplicationScoped +public class KafkaRequestReplyEmitter { + + @Inject + @Channel("my-request") + KafkaRequestReply quoteRequest; + + public Uni requestQuote(String request) { + return quoteRequest.request(request); + } +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/kafka/outbound/KafkaRequestReplyMessageEmitter.java b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/outbound/KafkaRequestReplyMessageEmitter.java new file mode 100644 index 000000000..e70379b62 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/outbound/KafkaRequestReplyMessageEmitter.java @@ -0,0 +1,26 @@ +package kafka.outbound; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.kafka.api.OutgoingKafkaRecordMetadata; +import io.smallrye.reactive.messaging.kafka.reply.KafkaRequestReply; + +@ApplicationScoped +public class KafkaRequestReplyMessageEmitter { + + @Inject + @Channel("my-request") + KafkaRequestReply quoteRequest; + + public Uni> requestMessage(String request) { + return quoteRequest.request(Message.of(request) + .addMetadata(OutgoingKafkaRecordMetadata.builder() + .withKey(request) + .build())); + } +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/kafka/outbound/KafkaRequestReplyMultiEmitter.java b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/outbound/KafkaRequestReplyMultiEmitter.java new file mode 100644 index 000000000..8d8897ba1 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/outbound/KafkaRequestReplyMultiEmitter.java @@ -0,0 +1,21 @@ +package org.acme; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; + +import io.smallrye.mutiny.Multi; +import io.smallrye.reactive.messaging.kafka.reply.KafkaRequestReply; + +@ApplicationScoped +public class KafkaRequestReplyMultiEmitter { + + @Inject + @Channel("my-request") + KafkaRequestReply quoteRequest; + + public Multi requestQuote(String request) { + return quoteRequest.requestMulti(request).select().first(5); + } +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/kafka/outbound/KafkaTransactionalProducer.java b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/outbound/KafkaTransactionalProducer.java new file mode 100644 index 000000000..597c33cc2 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/outbound/KafkaTransactionalProducer.java @@ -0,0 +1,28 @@ +package kafka.outbound; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; + +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.kafka.KafkaRecord; +import io.smallrye.reactive.messaging.kafka.transactions.KafkaTransactions; + +@ApplicationScoped +public class KafkaTransactionalProducer { + + @Inject + @Channel("tx-out-example") + KafkaTransactions txProducer; + + public Uni emitInTransaction() { + return txProducer.withTransaction(emitter -> { + emitter.send(KafkaRecord.of(1, "a")); + emitter.send(KafkaRecord.of(2, "b")); + emitter.send(KafkaRecord.of(3, "c")); + return Uni.createFrom().voidItem(); + }); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/kafka/outbound/MyReplyFailureHandler.java b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/outbound/MyReplyFailureHandler.java new file mode 100644 index 000000000..fcd7f8f9c --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/outbound/MyReplyFailureHandler.java @@ -0,0 +1,23 @@ +package kafka.outbound; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.apache.kafka.common.header.Header; + +import io.smallrye.common.annotation.Identifier; +import io.smallrye.reactive.messaging.kafka.KafkaRecord; +import io.smallrye.reactive.messaging.kafka.reply.ReplyFailureHandler; + +@ApplicationScoped +@Identifier("my-reply-error") +public class MyReplyFailureHandler implements ReplyFailureHandler { + + @Override + public Throwable handleReply(KafkaRecord replyRecord) { + Header header = replyRecord.getHeaders().lastHeader("REPLY_ERROR"); + if (header != null) { + return new IllegalArgumentException(new String(header.value())); + } + return null; + } +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/kafka/outbound/ProducerInterceptorBeanExample.java b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/outbound/ProducerInterceptorBeanExample.java new file mode 100644 index 000000000..a01e555cd --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/kafka/outbound/ProducerInterceptorBeanExample.java @@ -0,0 +1,37 @@ +package kafka.outbound; + +import java.util.Map; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.apache.kafka.clients.producer.ProducerInterceptor; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.apache.kafka.clients.producer.RecordMetadata; + +import io.smallrye.common.annotation.Identifier; + +@ApplicationScoped +@Identifier("my-producer-interceptor") +public class ProducerInterceptorBeanExample implements ProducerInterceptor { + + @Override + public ProducerRecord onSend(ProducerRecord producerRecord) { + // called before send + return producerRecord; + } + + @Override + public void onAcknowledgement(RecordMetadata recordMetadata, Exception e) { + // called on send acknowledgement callback + } + + @Override + public void close() { + // called on client close + } + + @Override + public void configure(Map map) { + // called on client configuration + } +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/keyed/KeyValueExtractorFromPayload.java b/content/docs/smallrye-reactive-messaging/4/_examples/keyed/KeyValueExtractorFromPayload.java new file mode 100644 index 000000000..6191d081b --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/keyed/KeyValueExtractorFromPayload.java @@ -0,0 +1,35 @@ +package keyed; + +import java.lang.reflect.Type; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; + +// tag::code[] +import io.smallrye.reactive.messaging.keyed.KeyValueExtractor; + +@ApplicationScoped +public class KeyValueExtractorFromPayload implements KeyValueExtractor { + + @Override + public boolean canExtract(Message msg, Type keyType, Type valueType) { + // Called for the first message of the stream to select the extractor. + // Here we only check for the type, but the logic can be more complex + return keyType.equals(String.class) && valueType.equals(String.class); + } + + @Override + public String extractKey(Message message, Type keyType) { + String string = message.getPayload().toString(); + return string.substring(0, string.indexOf(":")); + } + + @Override + public String extractValue(Message message, Type valueType) { + String string = message.getPayload().toString(); + return string.substring(string.indexOf(":") + 1); + } + +} +// end::code[] diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/keyed/KeyedExample.java b/content/docs/smallrye-reactive-messaging/4/_examples/keyed/KeyedExample.java new file mode 100644 index 000000000..438a0185e --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/keyed/KeyedExample.java @@ -0,0 +1,25 @@ +package keyed; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; +import io.smallrye.reactive.messaging.keyed.Keyed; +import io.smallrye.reactive.messaging.keyed.KeyedMulti; + +public class KeyedExample { + + // tag::code[] + @Incoming("in") + @Outgoing("out") + public Multi reshape( + @Keyed(KeyValueExtractorFromPayload.class) KeyedMulti multi) { + return multi.onItem().scan(AtomicInteger::new, (i, s) -> { + i.incrementAndGet(); + return i; + }).map(i -> Integer.toString(i.get())); + } + // end::code[] +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/keyed/KeyedMultiExample.java b/content/docs/smallrye-reactive-messaging/4/_examples/keyed/KeyedMultiExample.java new file mode 100644 index 000000000..c917cc8d1 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/keyed/KeyedMultiExample.java @@ -0,0 +1,27 @@ +package keyed; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; +import io.smallrye.reactive.messaging.keyed.KeyedMulti; + +public class KeyedMultiExample { + + // tag::code[] + @Incoming("in") + @Outgoing("out") + public Multi reshape(KeyedMulti multi) { + // Called once per key and receive the stream of value for that specific key + String key = multi.key(); + return multi.onItem().scan(AtomicInteger::new, (i, s) -> { + i.incrementAndGet(); + return i; + }) + .map(i -> Integer.toString(i.get())); + } + // end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/merge/MergeExamples.java b/content/docs/smallrye-reactive-messaging/4/_examples/merge/MergeExamples.java new file mode 100644 index 000000000..fbf4d4ac8 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/merge/MergeExamples.java @@ -0,0 +1,31 @@ +package merge; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.reactive.messaging.annotations.Merge; + +public class MergeExamples { + + // tag::chain[] + @Incoming("in1") + @Outgoing("out") + public int increment(int i) { + return i + 1; + } + + @Incoming("in2") + @Outgoing("out") + public int multiply(int i) { + return i * 2; + } + + @Incoming("out") + @Merge + public void getAll(int i) { + //... + } + + // end::chain[] + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/messages/MessageExamples.java b/content/docs/smallrye-reactive-messaging/4/_examples/messages/MessageExamples.java new file mode 100644 index 000000000..15dbf7e0f --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/messages/MessageExamples.java @@ -0,0 +1,96 @@ +package messages; + +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Metadata; + +@SuppressWarnings({ "unused" }) +public class MessageExamples { + + public void example1() { + Message message = Message.of("hello", Metadata.of(new MyMetadata())); + // tag::message[] + String payload = message.getPayload(); + Optional metadata = message.getMetadata(MyMetadata.class); + // end::message[] + } + + public void creation() { + Price price = new Price(20.5); + // tag::creation[] + // Create a simple message wrapping a payload + Message m1 = Message.of(price); + + // Create a message with metadata + Message m2 = Message.of(price, Metadata.of(new PriceMetadata())); + + // Create a message with several metadata + Message m3 = Message.of(price, + Metadata.of(new PriceMetadata(), new MyMetadata())); + + // Create a message with an acknowledgement callback + Message m4 = Message.of(price, () -> { + // Called when the message is acknowledged by the next consumer. + return CompletableFuture.completedFuture(null); + }); + + // Create a message with both metadata and acknowledgement callback + Message m5 = Message.of(price, + Metadata.of(new PriceMetadata()), + () -> { + // Called when the message is acknowledged by the next consumer. + return CompletableFuture.completedFuture(null); + }); + // end::creation[] + } + + public void copy() { + Price price = new Price(20.5); + Message message = Message.of(price); + // tag::copy[] + + // Create a new message with a new payload but with the same metadata + Message m1 = message.withPayload(new Price(12.4)); + + // Create a new message with a new payload and add another metadata + Message m2 = message + .withPayload(new Price(15.0)) + .withMetadata(Metadata.of(new PriceMetadata())); + + // Create a new message with a new payload and a custom acknowledgement + Message m3 = message + .withPayload(new Price(15.0)) + .withAck(() -> + // acknowledge the incoming message + message.ack() + .thenAccept(x -> { + // do something + })); + // end::copy[] + } + + public void nullPayload() { + // tag::null-payload[] + // Create a message with null payload + Message m6 = Message.of(null, Metadata.of(new PriceMetadata())); + // end::null-payload[] + } + + public static class Price { + final double price; + + public Price(double p) { + this.price = p; + } + } + + static class PriceMetadata { + final long timestamp; + + public PriceMetadata() { + this.timestamp = System.currentTimeMillis(); + } + } +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/mqtt/customization/ClientProducers.java b/content/docs/smallrye-reactive-messaging/4/_examples/mqtt/customization/ClientProducers.java new file mode 100644 index 000000000..51ee88023 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/mqtt/customization/ClientProducers.java @@ -0,0 +1,32 @@ +package mqtt.customization; + +import jakarta.enterprise.inject.Produces; + +import io.smallrye.common.annotation.Identifier; +import io.smallrye.reactive.messaging.mqtt.session.MqttClientSessionOptions; +import io.vertx.core.net.PemKeyCertOptions; +import io.vertx.core.net.PemTrustOptions; + +public class ClientProducers { + + // tag::named[] + @Produces + @Identifier("my-options") + public MqttClientSessionOptions getOptions() { + // You can use the produced options to configure the TLS connection + PemKeyCertOptions keycert = new PemKeyCertOptions() + .addCertPath("./tls/tls.crt") + .addKeyPath("./tls/tls.key"); + PemTrustOptions trust = new PemTrustOptions().addCertPath("./tlc/ca.crt"); + + return new MqttClientSessionOptions() + .setSsl(true) + .setPemKeyCertOptions(keycert) + .setPemTrustOptions(trust) + .setHostnameVerificationAlgorithm("HTTPS") + .setConnectTimeout(30000) + .setReconnectInterval(5000); + } + // end::named[] + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/mqtt/inbound/MqttPriceConsumer.java b/content/docs/smallrye-reactive-messaging/4/_examples/mqtt/inbound/MqttPriceConsumer.java new file mode 100644 index 000000000..9115d809a --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/mqtt/inbound/MqttPriceConsumer.java @@ -0,0 +1,17 @@ +package mqtt.inbound; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; + +@ApplicationScoped +public class MqttPriceConsumer { + + @Incoming("prices") + public void consume(byte[] raw) { + double price = Double.parseDouble(new String(raw)); + + // process your price. + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/mqtt/inbound/MqttPriceMessageConsumer.java b/content/docs/smallrye-reactive-messaging/4/_examples/mqtt/inbound/MqttPriceMessageConsumer.java new file mode 100644 index 000000000..ed40c96db --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/mqtt/inbound/MqttPriceMessageConsumer.java @@ -0,0 +1,21 @@ +package mqtt.inbound; + +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; + +@ApplicationScoped +public class MqttPriceMessageConsumer { + + @Incoming("prices") + public CompletionStage consume(Message price) { + // process your price. + + // Acknowledge the incoming message + return price.ack(); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/mqtt/outbound/MqttPriceMessageProducer.java b/content/docs/smallrye-reactive-messaging/4/_examples/mqtt/outbound/MqttPriceMessageProducer.java new file mode 100644 index 000000000..744ce54b5 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/mqtt/outbound/MqttPriceMessageProducer.java @@ -0,0 +1,26 @@ +package mqtt.outbound; + +import java.time.Duration; +import java.util.Random; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +@ApplicationScoped +public class MqttPriceMessageProducer { + + private Random random = new Random(); + + @Outgoing("prices") + public Multi> generate() { + // Build an infinite stream of random prices + // It emits a price every second + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .map(x -> Message.of(random.nextDouble())); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/mqtt/outbound/MqttPriceProducer.java b/content/docs/smallrye-reactive-messaging/4/_examples/mqtt/outbound/MqttPriceProducer.java new file mode 100644 index 000000000..22c0998e9 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/mqtt/outbound/MqttPriceProducer.java @@ -0,0 +1,25 @@ +package mqtt.outbound; + +import java.time.Duration; +import java.util.Random; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +@ApplicationScoped +public class MqttPriceProducer { + + private Random random = new Random(); + + @Outgoing("prices") + public Multi generate() { + // Build an infinite stream of random prices + // It emits a price every second + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .map(x -> random.nextDouble()); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/observability/ContextMessageObservationCollector.java b/content/docs/smallrye-reactive-messaging/4/_examples/observability/ContextMessageObservationCollector.java new file mode 100644 index 000000000..608e368dc --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/observability/ContextMessageObservationCollector.java @@ -0,0 +1,57 @@ +package observability; + +import java.time.Duration; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.observation.DefaultMessageObservation; +import io.smallrye.reactive.messaging.observation.MessageObservation; +import io.smallrye.reactive.messaging.observation.MessageObservationCollector; +import io.smallrye.reactive.messaging.observation.ObservationContext; + +@ApplicationScoped +public class ContextMessageObservationCollector + implements MessageObservationCollector { + + @Override + public MyContext initObservation(String channel, boolean incoming, boolean emitter) { + // Called on observation setup, per channel + // if returned null the observation for that channel is disabled + return new MyContext(channel, incoming, emitter); + } + + @Override + public MessageObservation onNewMessage(String channel, Message message, MyContext ctx) { + // Called after message has been created + return new DefaultMessageObservation(channel); + } + + public static class MyContext implements ObservationContext { + + private final String channel; + private final boolean incoming; + private final boolean emitter; + + public MyContext(String channel, boolean incoming, boolean emitter) { + this.channel = channel; + this.incoming = incoming; + this.emitter = emitter; + } + + @Override + public void complete(MessageObservation observation) { + // called after message processing has completed and observation is done + // register duration + Duration duration = observation.getCompletionDuration(); + Throwable reason = observation.getReason(); + if (reason != null) { + // message was nacked + } else { + // message was acked successfully + } + } + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/observability/SimpleMessageObservationCollector.java b/content/docs/smallrye-reactive-messaging/4/_examples/observability/SimpleMessageObservationCollector.java new file mode 100644 index 000000000..838bceb5f --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/observability/SimpleMessageObservationCollector.java @@ -0,0 +1,21 @@ +package observability; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.observation.DefaultMessageObservation; +import io.smallrye.reactive.messaging.observation.MessageObservation; +import io.smallrye.reactive.messaging.observation.MessageObservationCollector; +import io.smallrye.reactive.messaging.observation.ObservationContext; + +@ApplicationScoped +public class SimpleMessageObservationCollector implements MessageObservationCollector { + + @Override + public MessageObservation onNewMessage(String channel, Message message, ObservationContext ctx) { + // Called after message has been created + return new DefaultMessageObservation(channel); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/outgoings/OutgoingsExample.java b/content/docs/smallrye-reactive-messaging/4/_examples/outgoings/OutgoingsExample.java new file mode 100644 index 000000000..4e5bf44db --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/outgoings/OutgoingsExample.java @@ -0,0 +1,18 @@ +package outgoings; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +public class OutgoingsExample { + + //tag::code[] + @Incoming("in") + @Outgoing("out1") + @Outgoing("out2") + public String process(String s) { + // send messages from channel-in to both channel-out1 and channel-out2 + return s.toUpperCase(); + } + //end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/outgoings/SplitterMultiExample.java b/content/docs/smallrye-reactive-messaging/4/_examples/outgoings/SplitterMultiExample.java new file mode 100644 index 000000000..550443c61 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/outgoings/SplitterMultiExample.java @@ -0,0 +1,37 @@ +package outgoings; + +import java.util.Objects; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.operators.multi.split.MultiSplitter; + +public class SplitterMultiExample { + + // tag::code[] + enum Caps { + ALL_CAPS, + ALL_LOW, + MIXED + } + + @Incoming("in") + @Outgoing("sink1") + @Outgoing("sink2") + @Outgoing("sink3") + public MultiSplitter reshape(Multi in) { + return in.split(Caps.class, s -> { + if (Objects.equals(s, s.toLowerCase())) { + return Caps.ALL_LOW; + } else if (Objects.equals(s, s.toUpperCase())) { + return Caps.ALL_CAPS; + } else { + return Caps.MIXED; + } + }); + } + // end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/outgoings/TargetedExample.java b/content/docs/smallrye-reactive-messaging/4/_examples/outgoings/TargetedExample.java new file mode 100644 index 000000000..7835ef9d8 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/outgoings/TargetedExample.java @@ -0,0 +1,44 @@ +package outgoings; + +import java.util.Map; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.reactive.messaging.Messages; +import io.smallrye.reactive.messaging.Targeted; +import io.smallrye.reactive.messaging.TargetedMessages; + +public class TargetedExample { + + //tag::code[] + @Incoming("in") + @Outgoing("out1") + @Outgoing("out2") + @Outgoing("out3") + public Targeted process(double price) { + // send messages from channel-in to both channel-out1 and channel-out2 + Targeted targeted = Targeted.of("out1", "Price: " + price, + "out2", "Quote: " + price); + if (price > 90.0) { + return targeted.with("out3", price); + } + return targeted; + } + //end::code[] + + //tag::targeted-messages[] + @Incoming("channel-in") + @Outgoing("channel-out1") + @Outgoing("channel-out2") + @Outgoing("channel-out3") + public TargetedMessages processMessage(Message msg) { + // send messages from channel-in to both channel-out1 and channel-out2 + return Messages.chain(msg) + .with(Map.of("channel-out1", msg.withPayload(msg.getPayload().toUpperCase()), + "channel-out2", msg.withPayload(msg.getPayload().toLowerCase()))); + } + //end::targeted-messages[] + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/pausable/PausableController.java b/content/docs/smallrye-reactive-messaging/4/_examples/pausable/PausableController.java new file mode 100644 index 000000000..f95612833 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/pausable/PausableController.java @@ -0,0 +1,46 @@ +package pausable; + +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Incoming; + +import io.smallrye.reactive.messaging.ChannelRegistry; +import io.smallrye.reactive.messaging.PausableChannel; + +@ApplicationScoped +public class PausableController { + + // Option 1: Inject ChannelRegistry and retrieve the pausable channel + @Inject + ChannelRegistry registry; + + // Option 2: Directly inject the pausable channel + @Inject + @Channel("my-channel") + PausableChannel myChannel; + + @PostConstruct + public void resume() { + // Wait for the application to be ready + // Option 1: Retrieve the pausable channel from the registry + PausableChannel pausable = registry.getPausable("my-channel"); + // Resume the processing of the messages + pausable.resume(); + } + + public void pause() { + // Option 2: Use the injected pausable channel + + // Pause the processing of the messages + myChannel.pause(); + } + + @Incoming("my-channel") + void process(String message) { + // Process the message + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/processing/ProcessingExamples.java b/content/docs/smallrye-reactive-messaging/4/_examples/processing/ProcessingExamples.java new file mode 100644 index 000000000..548722816 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/processing/ProcessingExamples.java @@ -0,0 +1,72 @@ +package processing; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Uni; + +public class ProcessingExamples { + + // tag::process-message[] + @Incoming("in") + @Outgoing("out") + public Message processMessage(Message in) { + return in.withPayload(Integer.toString(in.getPayload())); + } + // end::process-message[] + + // tag::process-message-cs[] + @Incoming("in") + @Outgoing("out") + public CompletionStage> processMessageCS(Message in) { + CompletionStage cs = invokeService(in.getPayload()); + return cs.thenApply(in::withPayload); + } + // end::process-message-cs[] + + // tag::process-message-uni[] + @Incoming("in") + @Outgoing("out") + public Uni> processMessageUni(Message in) { + return invokeService(in.getPayload()) + .map(in::withPayload); + } + // end::process-message-uni[] + + // tag::process-payload[] + @Incoming("in") + @Outgoing("out") + public String processPayload(int in) { + return Integer.toString(in); + } + // end::process-payload[] + + // tag::process-payload-cs[] + @Incoming("in") + @Outgoing("out") + public CompletionStage processPayloadCS(int in) { + return invokeService(in); + } + // end::process-payload-cs[] + + // tag::process-payload-uni[] + @Incoming("in") + @Outgoing("out") + public Uni processPayload(String in) { + return invokeService(in); + } + // end::process-payload-uni[] + + private CompletionStage invokeService(int param) { + return CompletableFuture.completedFuture("ok"); + } + + private Uni invokeService(String param) { + return Uni.createFrom().item("ok"); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/processing/StreamExamples.java b/content/docs/smallrye-reactive-messaging/4/_examples/processing/StreamExamples.java new file mode 100644 index 000000000..c33bfecd8 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/processing/StreamExamples.java @@ -0,0 +1,41 @@ +package processing; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; + +public class StreamExamples { + + // tag::processing-stream-message[] + @Incoming("in") + @Outgoing("out") + public Multi> processMessageStream(Multi> stream) { + return stream + .onItem().transformToUni(message -> invokeService(message.getPayload()) + .onFailure().recoverWithItem("fallback") + .onItem().transform(message::withPayload)) + .concatenate(); + + } + // end::processing-stream-message[] + + // tag::processing-stream-payload[] + @Incoming("in") + @Outgoing("out") + public Multi processPayloadStream(Multi stream) { + return stream + .onItem().transformToUni(payload -> invokeService(payload) + .onFailure().recoverWithItem("fallback")) + .concatenate(); + + } + // end::processing-stream-payload[] + + private Uni invokeService(int i) { + return Uni.createFrom().item("ok " + i); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/pulsar/configuration/PulsarClientConfigProducers.java b/content/docs/smallrye-reactive-messaging/4/_examples/pulsar/configuration/PulsarClientConfigProducers.java new file mode 100644 index 000000000..89f1d31c5 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/pulsar/configuration/PulsarClientConfigProducers.java @@ -0,0 +1,66 @@ +package pulsar.configuration; + +import java.util.HashMap; +import java.util.Map; + +import jakarta.enterprise.inject.Produces; + +import org.apache.pulsar.client.api.BatcherBuilder; +import org.apache.pulsar.client.api.PulsarClientSharedResources; +import org.apache.pulsar.client.impl.AutoClusterFailover; +import org.apache.pulsar.client.impl.DefaultCryptoKeyReader; +import org.apache.pulsar.client.impl.conf.ClientConfigurationData; +import org.apache.pulsar.client.impl.conf.ConsumerConfigurationData; +import org.apache.pulsar.client.impl.customroute.PartialRoundRobinMessageRouterImpl; + +import io.smallrye.common.annotation.Identifier; + +public class PulsarClientConfigProducers { + + // tag::consumer[] + @Produces + @Identifier("my-consumer-options") + public ConsumerConfigurationData getConsumerConfig() { + ConsumerConfigurationData data = new ConsumerConfigurationData<>(); + data.setAckReceiptEnabled(true); + data.setCryptoKeyReader(DefaultCryptoKeyReader.builder() + //... + .build()); + return data; + } + // end::consumer[] + + // tag::client[] + @Produces + @Identifier("prices") + public ClientConfigurationData getClientConfig() { + ClientConfigurationData data = new ClientConfigurationData(); + data.setEnableTransaction(true); + data.setServiceUrlProvider(AutoClusterFailover.builder() + // ... + .build()); + return data; + } + // end::client[] + + // tag::shared-resources[] + @Produces + public PulsarClientSharedResources getSharedResources() { + return PulsarClientSharedResources.builder() + .configureEventLoop(config -> config.numberOfThreads(4)) + .build(); + } + // end::shared-resources[] + + // tag::producer[] + @Produces + @Identifier("prices") + public Map getProducerconfig() { + Map map = new HashMap<>(); + map.put("batcherBuilder", BatcherBuilder.KEY_BASED); + map.put("sendTimeoutMs", 3000); + map.put("customMessageRouter", new PartialRoundRobinMessageRouterImpl(4)); + return map; + } + // end::producer[] +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/pulsar/configuration/PulsarSchemaProvider.java b/content/docs/smallrye-reactive-messaging/4/_examples/pulsar/configuration/PulsarSchemaProvider.java new file mode 100644 index 000000000..3fe6141d0 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/pulsar/configuration/PulsarSchemaProvider.java @@ -0,0 +1,30 @@ +package pulsar.configuration; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; + +import org.apache.pulsar.client.api.Schema; +import org.apache.pulsar.common.schema.KeyValue; +import org.apache.pulsar.common.schema.KeyValueEncodingType; + +import io.smallrye.common.annotation.Identifier; + +@ApplicationScoped +public class PulsarSchemaProvider { + + @Produces + @Identifier("user-schema") + Schema userSchema = Schema.AVRO(User.class); + + @Produces + @Identifier("a-channel") + Schema> keyValueSchema() { + return Schema.KeyValue(Schema.INT32, Schema.JSON(User.class), KeyValueEncodingType.SEPARATED); + } + + public static class User { + String name; + int age; + + } +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/pulsar/inbound/PulsarMessageBatchExample.java b/content/docs/smallrye-reactive-messaging/4/_examples/pulsar/inbound/PulsarMessageBatchExample.java new file mode 100644 index 000000000..9ce9eead1 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/pulsar/inbound/PulsarMessageBatchExample.java @@ -0,0 +1,54 @@ +package pulsar.inbound; + +import java.util.List; +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.TracingMetadata; +import io.smallrye.reactive.messaging.pulsar.PulsarIncomingBatchMessageMetadata; + +@ApplicationScoped +public class PulsarMessageBatchExample { + + // tag::code[] + @Incoming("prices") + public CompletionStage consumeMessage(Message> messages) { + messages.getMetadata(PulsarIncomingBatchMessageMetadata.class).ifPresent(metadata -> { + for (org.apache.pulsar.client.api.Message message : metadata.getMessages()) { + String key = message.getKey(); + String topic = message.getTopicName(); + long timestamp = message.getEventTime(); + //... process messages + } + }); + // ack will commit the latest offsets (per partition) of the batch. + return messages.ack(); + } + + @Incoming("prices") + public void consumeMessages(org.apache.pulsar.client.api.Messages messages) { + for (org.apache.pulsar.client.api.Message msg : messages) { + //... process messages + } + } + // end::code[] + + // tag::batch[] + @Incoming("prices") + public void consumeMessages(org.apache.pulsar.client.api.Messages messages, + PulsarIncomingBatchMessageMetadata metadata) { + for (org.apache.pulsar.client.api.Message message : messages) { + TracingMetadata tracing = metadata.getMetadataForMessage(message, TracingMetadata.class); + if (tracing != null) { + tracing.getCurrentContext().makeCurrent(); + } + //... process messages + } + } + // end::batch[] + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/pulsar/inbound/PulsarMessageBatchPayloadExample.java b/content/docs/smallrye-reactive-messaging/4/_examples/pulsar/inbound/PulsarMessageBatchPayloadExample.java new file mode 100644 index 000000000..649b7d1ae --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/pulsar/inbound/PulsarMessageBatchPayloadExample.java @@ -0,0 +1,21 @@ +package pulsar.inbound; + +import java.util.List; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; + +@ApplicationScoped +public class PulsarMessageBatchPayloadExample { + + // tag::code[] + @Incoming("prices") + public void consume(List prices) { + for (double price : prices) { + // process price + } + } + // end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/pulsar/inbound/PulsarMetadataExample.java b/content/docs/smallrye-reactive-messaging/4/_examples/pulsar/inbound/PulsarMetadataExample.java new file mode 100644 index 000000000..a4add7ea1 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/pulsar/inbound/PulsarMetadataExample.java @@ -0,0 +1,35 @@ +package pulsar.inbound; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.pulsar.PulsarIncomingMessageMetadata; + +public class PulsarMetadataExample { + + @SuppressWarnings("unchecked") + public void metadata() { + Message incoming = Message.of(12.0); + // tag::code[] + PulsarIncomingMessageMetadata metadata = incoming.getMetadata(PulsarIncomingMessageMetadata.class).orElse(null); + if (metadata != null) { + // The topic + String topic = metadata.getTopicName(); + + // The key + String key = metadata.getKey(); + + // The event time + long timestamp = metadata.getEventTime(); + + // The raw data + byte[] rawData = metadata.getData(); + + // The underlying message + org.apache.pulsar.client.api.Message message = metadata.getMessage(); + + // ... + } + // end::code[] + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/pulsar/inbound/PulsarPriceConsumer.java b/content/docs/smallrye-reactive-messaging/4/_examples/pulsar/inbound/PulsarPriceConsumer.java new file mode 100644 index 000000000..9973a6158 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/pulsar/inbound/PulsarPriceConsumer.java @@ -0,0 +1,15 @@ +package pulsar.inbound; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; + +@ApplicationScoped +public class PulsarPriceConsumer { + + @Incoming("prices") + public void consume(double price) { + // process your price. + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/pulsar/inbound/PulsarPriceMessageConsumer.java b/content/docs/smallrye-reactive-messaging/4/_examples/pulsar/inbound/PulsarPriceMessageConsumer.java new file mode 100644 index 000000000..4cdc3f12a --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/pulsar/inbound/PulsarPriceMessageConsumer.java @@ -0,0 +1,21 @@ +package pulsar.inbound; + +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; + +@ApplicationScoped +public class PulsarPriceMessageConsumer { + + @Incoming("prices") + public CompletionStage consume(Message price) { + // process your price. + + // Acknowledge the incoming message + return price.ack(); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/pulsar/outbound/PulsarExactlyOnceProcessor.java b/content/docs/smallrye-reactive-messaging/4/_examples/pulsar/outbound/PulsarExactlyOnceProcessor.java new file mode 100644 index 000000000..c207cba70 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/pulsar/outbound/PulsarExactlyOnceProcessor.java @@ -0,0 +1,35 @@ +package pulsar.outbound; + +import java.util.List; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.pulsar.PulsarIncomingBatchMessageMetadata; +import io.smallrye.reactive.messaging.pulsar.PulsarMessage; +import io.smallrye.reactive.messaging.pulsar.transactions.PulsarTransactions; + +@ApplicationScoped +public class PulsarExactlyOnceProcessor { + + @Inject + @Channel("tx-out-example") + PulsarTransactions txProducer; + + @Incoming("in-channel") + public Uni emitInTransaction(Message> batch) { + return txProducer.withTransactionAndAck(batch, emitter -> { + PulsarIncomingBatchMessageMetadata metadata = batch.getMetadata(PulsarIncomingBatchMessageMetadata.class).get(); + for (org.apache.pulsar.client.api.Message message : metadata. getMessages()) { + emitter.send(PulsarMessage.of(message.getValue() + 1, message.getKey())); + } + return Uni.createFrom().voidItem(); + }); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/pulsar/outbound/PulsarKeyValueExample.java b/content/docs/smallrye-reactive-messaging/4/_examples/pulsar/outbound/PulsarKeyValueExample.java new file mode 100644 index 000000000..ab0b43a6c --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/pulsar/outbound/PulsarKeyValueExample.java @@ -0,0 +1,28 @@ +package pulsar.outbound; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; + +import org.apache.pulsar.client.api.Schema; +import org.apache.pulsar.common.schema.KeyValue; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.common.annotation.Identifier; + +@ApplicationScoped +public class PulsarKeyValueExample { + + // tag::code[] + @Identifier("out") + @Produces + Schema> schema = Schema.KeyValue(Schema.STRING, Schema.INT64); + + @Incoming("in") + @Outgoing("out") + public KeyValue process(long in) { + return new KeyValue<>("my-key", in); + } + // end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/pulsar/outbound/PulsarOutboundMetadataExample.java b/content/docs/smallrye-reactive-messaging/4/_examples/pulsar/outbound/PulsarOutboundMetadataExample.java new file mode 100644 index 000000000..152974fe2 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/pulsar/outbound/PulsarOutboundMetadataExample.java @@ -0,0 +1,26 @@ +package pulsar.outbound; + +import java.util.Map; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.pulsar.PulsarOutgoingMessageMetadata; + +public class PulsarOutboundMetadataExample { + + public Message metadata(Message incoming) { + // tag::code[] + // Creates an PulsarOutgoingMessageMetadata + // The type parameter is the type of the record's key + PulsarOutgoingMessageMetadata metadata = PulsarOutgoingMessageMetadata.builder() + .withKey("my-key") + .withProperties(Map.of("property-key", "value")) + .build(); + + // Create a new message from the `incoming` message + // Add `metadata` to the metadata from the `incoming` message. + return incoming.addMetadata(metadata); + // end::code[] + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/pulsar/outbound/PulsarOutgoingMessageExample.java b/content/docs/smallrye-reactive-messaging/4/_examples/pulsar/outbound/PulsarOutgoingMessageExample.java new file mode 100644 index 000000000..f452f437e --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/pulsar/outbound/PulsarOutgoingMessageExample.java @@ -0,0 +1,18 @@ +package pulsar.outbound; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.reactive.messaging.pulsar.OutgoingMessage; + +public class PulsarOutgoingMessageExample { + + // tag::code[] + @Incoming("in") + @Outgoing("out") + OutgoingMessage process(org.apache.pulsar.client.api.Message in) { + return OutgoingMessage.from(in) + .withValue(Long.valueOf(in.getValue())); + } + // end::code[] +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/pulsar/outbound/PulsarPriceMessageProducer.java b/content/docs/smallrye-reactive-messaging/4/_examples/pulsar/outbound/PulsarPriceMessageProducer.java new file mode 100644 index 000000000..1f53f7c68 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/pulsar/outbound/PulsarPriceMessageProducer.java @@ -0,0 +1,26 @@ +package pulsar.outbound; + +import java.time.Duration; +import java.util.Random; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +@ApplicationScoped +public class PulsarPriceMessageProducer { + + private Random random = new Random(); + + @Outgoing("prices") + public Multi> generate() { + // Build an infinite stream of random prices + // It emits a price every second + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .map(x -> Message.of(random.nextDouble())); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/pulsar/outbound/PulsarPriceProducer.java b/content/docs/smallrye-reactive-messaging/4/_examples/pulsar/outbound/PulsarPriceProducer.java new file mode 100644 index 000000000..2073b7763 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/pulsar/outbound/PulsarPriceProducer.java @@ -0,0 +1,25 @@ +package pulsar.outbound; + +import java.time.Duration; +import java.util.Random; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +@ApplicationScoped +public class PulsarPriceProducer { + + private Random random = new Random(); + + @Outgoing("prices") + public Multi generate() { + // Build an infinite stream of random prices + // It emits a price every second + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .map(x -> random.nextDouble()); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/pulsar/outbound/PulsarTransactionalProducer.java b/content/docs/smallrye-reactive-messaging/4/_examples/pulsar/outbound/PulsarTransactionalProducer.java new file mode 100644 index 000000000..8bee09bb3 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/pulsar/outbound/PulsarTransactionalProducer.java @@ -0,0 +1,38 @@ +package pulsar.outbound; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.pulsar.OutgoingMessage; +import io.smallrye.reactive.messaging.pulsar.transactions.PulsarTransactions; + +@ApplicationScoped +public class PulsarTransactionalProducer { + + @Inject + @Channel("tx-out-example") + PulsarTransactions> txProducer; + + @Inject + @Channel("other-producer") + PulsarTransactions producer; + + @Incoming("in") + public Uni emitInTransaction(Message in) { + return txProducer.withTransaction(emitter -> { + emitter.send(OutgoingMessage.of("a", 1)); + emitter.send(OutgoingMessage.of("b", 2)); + emitter.send(OutgoingMessage.of("c", 3)); + producer.send(emitter, "4"); + producer.send(emitter, "5"); + producer.send(emitter, "6"); + return Uni.createFrom().completionStage(in::ack); + }); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/quickstart/ReactiveMessagingExample.java b/content/docs/smallrye-reactive-messaging/4/_examples/quickstart/ReactiveMessagingExample.java new file mode 100644 index 000000000..318b39739 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/quickstart/ReactiveMessagingExample.java @@ -0,0 +1,35 @@ +package quickstart; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +@ApplicationScoped +public class ReactiveMessagingExample { + + @Outgoing("source") + public Multi source() { + return Multi.createFrom().items("hello", "from", "SmallRye", "reactive", "messaging"); + } + + @Incoming("source") + @Outgoing("processed-a") + public String toUpperCase(String payload) { + return payload.toUpperCase(); + } + + @Incoming("processed-a") + @Outgoing("processed-b") + public Multi filter(Multi input) { + return input.select().where(item -> item.length() > 4); + } + + @Incoming("processed-b") + public void sink(String word) { + System.out.println(">> " + word); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/rabbitmq/customization/ArgumentProducers.java b/content/docs/smallrye-reactive-messaging/4/_examples/rabbitmq/customization/ArgumentProducers.java new file mode 100644 index 000000000..04a196c1c --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/rabbitmq/customization/ArgumentProducers.java @@ -0,0 +1,17 @@ +package rabbitmq.customization; + +import java.util.Map; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; + +import io.smallrye.common.annotation.Identifier; + +@ApplicationScoped +public class ArgumentProducers { + @Produces + @Identifier("my-arguments") + Map customArguments() { + return Map.of("custom-arg", "value"); + } +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/rabbitmq/customization/RabbitMQProducers.java b/content/docs/smallrye-reactive-messaging/4/_examples/rabbitmq/customization/RabbitMQProducers.java new file mode 100644 index 000000000..0aa46219e --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/rabbitmq/customization/RabbitMQProducers.java @@ -0,0 +1,34 @@ +package rabbitmq.customization; + +import jakarta.enterprise.inject.Produces; + +import io.smallrye.common.annotation.Identifier; +import io.vertx.core.net.PemKeyCertOptions; +import io.vertx.core.net.PemTrustOptions; +import io.vertx.rabbitmq.RabbitMQOptions; + +public class RabbitMQProducers { + + // tag::named[] + @Produces + @Identifier("my-named-options") + public RabbitMQOptions getNamedOptions() { + // You can use the produced options to configure the TLS connection + PemKeyCertOptions keycert = new PemKeyCertOptions() + .addCertPath("./tls/tls.crt") + .addKeyPath("./tls/tls.key"); + PemTrustOptions trust = new PemTrustOptions().addCertPath("./tlc/ca.crt"); + + return (RabbitMQOptions) new RabbitMQOptions() + .setUser("admin") + .setPassword("test") + .setSsl(true) + .setPemKeyCertOptions(keycert) + .setPemTrustOptions(trust) + .setHostnameVerificationAlgorithm("HTTPS") + .setConnectTimeout(30000) + .setReconnectInterval(5000); + } + // end::named[] + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/rabbitmq/inbound/RabbitMQMetadataExample.java b/content/docs/smallrye-reactive-messaging/4/_examples/rabbitmq/inbound/RabbitMQMetadataExample.java new file mode 100644 index 000000000..c5b826b20 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/rabbitmq/inbound/RabbitMQMetadataExample.java @@ -0,0 +1,36 @@ +package rabbitmq.inbound; + +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Map; +import java.util.Optional; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.rabbitmq.IncomingRabbitMQMetadata; + +public class RabbitMQMetadataExample { + + public void metadata(final Message incomingMessage) { + // tag::code[] + final Optional metadata = incomingMessage.getMetadata(IncomingRabbitMQMetadata.class); + metadata.ifPresent(meta -> { + final Optional contentEncoding = meta.getContentEncoding(); + final Optional contentType = meta.getContentType(); + final Optional correlationId = meta.getCorrelationId(); + final Optional timestamp = meta.getTimestamp(ZoneId.systemDefault()); + final Optional priority = meta.getPriority(); + final Optional replyTo = meta.getReplyTo(); + final Optional userId = meta.getUserId(); + + // Access a single String-valued header + final Optional stringHeader = meta.getHeader("my-header", String.class); + + // Access all headers + final Map headers = meta.getHeaders(); + // ... + }); + // end::code[] + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/rabbitmq/inbound/RabbitMQPriceConsumer.java b/content/docs/smallrye-reactive-messaging/4/_examples/rabbitmq/inbound/RabbitMQPriceConsumer.java new file mode 100644 index 000000000..033ae9c54 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/rabbitmq/inbound/RabbitMQPriceConsumer.java @@ -0,0 +1,15 @@ +package rabbitmq.inbound; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; + +@ApplicationScoped +public class RabbitMQPriceConsumer { + + @Incoming("prices") + public void consume(String price) { + // process your price. + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/rabbitmq/inbound/RabbitMQPriceMessageConsumer.java b/content/docs/smallrye-reactive-messaging/4/_examples/rabbitmq/inbound/RabbitMQPriceMessageConsumer.java new file mode 100644 index 000000000..1e8a97043 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/rabbitmq/inbound/RabbitMQPriceMessageConsumer.java @@ -0,0 +1,21 @@ +package rabbitmq.inbound; + +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; + +@ApplicationScoped +public class RabbitMQPriceMessageConsumer { + + @Incoming("prices") + public CompletionStage consume(Message price) { + // process your price. + + // Acknowledge the incoming message, marking the RabbitMQ message as `accepted`. + return price.ack(); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/rabbitmq/inbound/RabbitMQRejectMetadataExample.java b/content/docs/smallrye-reactive-messaging/4/_examples/rabbitmq/inbound/RabbitMQRejectMetadataExample.java new file mode 100644 index 000000000..9bd26cc35 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/rabbitmq/inbound/RabbitMQRejectMetadataExample.java @@ -0,0 +1,24 @@ +package rabbitmq.inbound; + +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Metadata; + +import io.smallrye.reactive.messaging.rabbitmq.RabbitMQRejectMetadata; + +@ApplicationScoped +public class RabbitMQRejectMetadataExample { + + // tag::code[] + @Incoming("in") + public CompletionStage consume(Message message) { + return message.nack(new Exception("Failed!"), Metadata.of( + new RabbitMQRejectMetadata(true))); + } + // end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/rabbitmq/outbound/MyRabbitMQReplyFailureHandler.java b/content/docs/smallrye-reactive-messaging/4/_examples/rabbitmq/outbound/MyRabbitMQReplyFailureHandler.java new file mode 100644 index 000000000..637edb2bc --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/rabbitmq/outbound/MyRabbitMQReplyFailureHandler.java @@ -0,0 +1,21 @@ +package rabbitmq.outbound; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.smallrye.common.annotation.Identifier; +import io.smallrye.reactive.messaging.rabbitmq.IncomingRabbitMQMessage; +import io.smallrye.reactive.messaging.rabbitmq.reply.ReplyFailureHandler; + +@ApplicationScoped +@Identifier("my-reply-error") +public class MyRabbitMQReplyFailureHandler implements ReplyFailureHandler { + + @Override + public Throwable handleReply(IncomingRabbitMQMessage replyMessage) { + var error = (String) replyMessage.getHeaders().get("REPLY_ERROR"); + if (error != null) { + return (Throwable) new IllegalArgumentException(error); + } + return null; + } +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/rabbitmq/outbound/RabbitMQOutboundMetadataExample.java b/content/docs/smallrye-reactive-messaging/4/_examples/rabbitmq/outbound/RabbitMQOutboundMetadataExample.java new file mode 100644 index 000000000..225a6f0a4 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/rabbitmq/outbound/RabbitMQOutboundMetadataExample.java @@ -0,0 +1,26 @@ +package rabbitmq.outbound; + +import java.time.ZonedDateTime; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Metadata; + +import io.smallrye.reactive.messaging.rabbitmq.OutgoingRabbitMQMetadata; + +public class RabbitMQOutboundMetadataExample { + + public Message metadata(Message incoming) { + + // tag::code[] + final OutgoingRabbitMQMetadata metadata = new OutgoingRabbitMQMetadata.Builder() + .withHeader("my-header", "xyzzy") + .withRoutingKey("urgent") + .withTimestamp(ZonedDateTime.now()) + .build(); + + // Add `metadata` to the metadata of the outgoing message. + return Message.of("Hello", Metadata.of(metadata)); + // end::code[] + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/rabbitmq/outbound/RabbitMQPriceMessageProducer.java b/content/docs/smallrye-reactive-messaging/4/_examples/rabbitmq/outbound/RabbitMQPriceMessageProducer.java new file mode 100644 index 000000000..bc50fb93b --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/rabbitmq/outbound/RabbitMQPriceMessageProducer.java @@ -0,0 +1,33 @@ +package rabbitmq.outbound; + +import java.time.Duration; +import java.time.ZonedDateTime; +import java.util.Random; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Metadata; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; +import io.smallrye.reactive.messaging.rabbitmq.OutgoingRabbitMQMetadata; + +@ApplicationScoped +public class RabbitMQPriceMessageProducer { + + private Random random = new Random(); + + @Outgoing("prices") + public Multi> generate() { + // Build an infinite stream of random prices + // It emits a price every second + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .map(x -> Message.of(random.nextDouble(), + Metadata.of(new OutgoingRabbitMQMetadata.Builder() + .withRoutingKey("normal") + .withTimestamp(ZonedDateTime.now()) + .build()))); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/rabbitmq/outbound/RabbitMQPriceProducer.java b/content/docs/smallrye-reactive-messaging/4/_examples/rabbitmq/outbound/RabbitMQPriceProducer.java new file mode 100644 index 000000000..e6477cf30 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/rabbitmq/outbound/RabbitMQPriceProducer.java @@ -0,0 +1,25 @@ +package rabbitmq.outbound; + +import java.time.Duration; +import java.util.Random; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +@ApplicationScoped +public class RabbitMQPriceProducer { + + private Random random = new Random(); + + @Outgoing("prices") + public Multi generate() { + // Build an infinite stream of random prices + // It emits a price every second + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .map(x -> random.nextDouble()); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/rabbitmq/outbound/RabbitMQReplier.java b/content/docs/smallrye-reactive-messaging/4/_examples/rabbitmq/outbound/RabbitMQReplier.java new file mode 100644 index 000000000..aec8bfa73 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/rabbitmq/outbound/RabbitMQReplier.java @@ -0,0 +1,20 @@ +package rabbitmq.outbound; + +import java.util.Random; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +@ApplicationScoped +public class RabbitMQReplier { + + Random rand = new Random(); + + @Incoming("request") + @Outgoing("reply") + String handleRequest(String request) { + return request + "-" + rand.nextInt(100); + } +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/rabbitmq/outbound/RabbitMQRequestReplyEmitter.java b/content/docs/smallrye-reactive-messaging/4/_examples/rabbitmq/outbound/RabbitMQRequestReplyEmitter.java new file mode 100644 index 000000000..019255318 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/rabbitmq/outbound/RabbitMQRequestReplyEmitter.java @@ -0,0 +1,21 @@ +package rabbitmq.outbound; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; + +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.rabbitmq.reply.RabbitMQRequestReply; + +@ApplicationScoped +public class RabbitMQRequestReplyEmitter { + + @Inject + @Channel("my-request") + RabbitMQRequestReply quoteRequest; + + public Uni requestQuote(String request) { + return quoteRequest.request(request); + } +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/rabbitmq/outbound/RabbitMQRequestReplyMessageEmitter.java b/content/docs/smallrye-reactive-messaging/4/_examples/rabbitmq/outbound/RabbitMQRequestReplyMessageEmitter.java new file mode 100644 index 000000000..96b9b0f81 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/rabbitmq/outbound/RabbitMQRequestReplyMessageEmitter.java @@ -0,0 +1,22 @@ +package rabbitmq.outbound; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.rabbitmq.reply.RabbitMQRequestReply; + +@ApplicationScoped +public class RabbitMQRequestReplyMessageEmitter { + + @Inject + @Channel("my-request") + RabbitMQRequestReply quoteRequest; + + public Uni> requestMessage(String request) { + return quoteRequest.request(Message.of(request)); + } +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/rabbitmq/outbound/RabbitMQRequestReplyMultiEmitter.java b/content/docs/smallrye-reactive-messaging/4/_examples/rabbitmq/outbound/RabbitMQRequestReplyMultiEmitter.java new file mode 100644 index 000000000..2069164d0 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/rabbitmq/outbound/RabbitMQRequestReplyMultiEmitter.java @@ -0,0 +1,21 @@ +package rabbitmq.outbound; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; + +import io.smallrye.mutiny.Multi; +import io.smallrye.reactive.messaging.rabbitmq.reply.RabbitMQRequestReply; + +@ApplicationScoped +public class RabbitMQRequestReplyMultiEmitter { + + @Inject + @Channel("my-request") + RabbitMQRequestReply quoteRequest; + + public Multi requestQuote(String request) { + return quoteRequest.requestMulti(request).select().first(5); + } +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/skip/SingleSkip.java b/content/docs/smallrye-reactive-messaging/4/_examples/skip/SingleSkip.java new file mode 100644 index 000000000..f7209220b --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/skip/SingleSkip.java @@ -0,0 +1,59 @@ +package skip; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Uni; + +public class SingleSkip { + + // tag::skip[] + // Skip when processing payload synchronously - returning `null` + @Incoming("in") + @Outgoing("out") + public String processPayload(String s) { + if (s.equalsIgnoreCase("skip")) { + return null; + } + return s.toUpperCase(); + } + + // Skip when processing message synchronously - returning `null` + @Incoming("in") + @Outgoing("out") + public Message processMessage(Message m) { + String s = m.getPayload(); + if (s.equalsIgnoreCase("skip")) { + m.ack(); + return null; + } + return m.withPayload(s.toUpperCase()); + } + + // Skip when processing payload asynchronously - returning a `Uni` with a `null` value + @Incoming("in") + @Outgoing("out") + public Uni processPayloadAsync(String s) { + if (s.equalsIgnoreCase("skip")) { + // Important, you must not return `null`, but a `null` content + return Uni.createFrom().nullItem(); + } + return Uni.createFrom().item(s.toUpperCase()); + } + + // Skip when processing message asynchronously - returning a `Uni` with a `null` value + @Incoming("in") + @Outgoing("out") + public Uni> processMessageAsync(Message m) { + String s = m.getPayload(); + if (s.equalsIgnoreCase("skip")) { + m.ack(); + return Uni.createFrom().nullItem(); + } + return Uni.createFrom().item(m.withPayload(s.toUpperCase())); + } + + // end::skip[] + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/skip/StreamSkip.java b/content/docs/smallrye-reactive-messaging/4/_examples/skip/StreamSkip.java new file mode 100644 index 000000000..2108a1077 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/skip/StreamSkip.java @@ -0,0 +1,48 @@ +package skip; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +public class StreamSkip { + + // tag::skip[] + @Incoming("in") + @Outgoing("out-1") + public Multi processPayload(String s) { + if (s.equalsIgnoreCase("skip")) { + return Multi.createFrom().empty(); + } + return Multi.createFrom().item(s.toUpperCase()); + } + + @Incoming("in") + @Outgoing("out-2") + public Multi> processMessage(Message m) { + String s = m.getPayload(); + if (s.equalsIgnoreCase("skip")) { + return Multi.createFrom().empty(); + } + return Multi.createFrom().item(m.withPayload(s.toUpperCase())); + } + + @Incoming("in") + @Outgoing("out-3") + public Multi processPayloadStream(Multi stream) { + return stream + .select().where(s -> !s.equalsIgnoreCase("skip")) + .onItem().transform(String::toUpperCase); + } + + @Incoming("in") + @Outgoing("out-4") + public Multi> processMessageStream(Multi> stream) { + return stream + .select().where(m -> !m.getPayload().equalsIgnoreCase("skip")) + .onItem().transform(m -> m.withPayload(m.getPayload().toUpperCase())); + } + // end::skip[] + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/sns/outbound/SnsMessageProducer.java b/content/docs/smallrye-reactive-messaging/4/_examples/sns/outbound/SnsMessageProducer.java new file mode 100644 index 000000000..cc2e815d6 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/sns/outbound/SnsMessageProducer.java @@ -0,0 +1,27 @@ +package sns.outbound; + +import java.time.Duration; +import java.util.UUID; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Metadata; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; +import io.smallrye.reactive.messaging.aws.sns.SnsOutboundMetadata; + +@ApplicationScoped +public class SnsMessageProducer { + + @Outgoing("prices") + public Multi> generate() { + // It emits a UUID every second + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .map(x -> Message.of(UUID.randomUUID().toString(), + Metadata.of(SnsOutboundMetadata.builder() + .groupId("group-1") + .build()))); + } +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/sns/outbound/SnsOutboundMetadataExample.java b/content/docs/smallrye-reactive-messaging/4/_examples/sns/outbound/SnsOutboundMetadataExample.java new file mode 100644 index 000000000..9d44dacb0 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/sns/outbound/SnsOutboundMetadataExample.java @@ -0,0 +1,29 @@ +package sns.outbound; + +import java.util.Map; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Metadata; + +import io.smallrye.reactive.messaging.aws.sns.SnsOutboundMetadata; +import software.amazon.awssdk.services.sns.model.MessageAttributeValue; + +@ApplicationScoped +public class SnsOutboundMetadataExample { + + public Message metadata(Message incoming) { + // tag::code[] + final SnsOutboundMetadata metadata = SnsOutboundMetadata.builder() + .messageAttributes(Map.of("my-attribute", MessageAttributeValue.builder() + .dataType("String").stringValue("my-value").build())) + .groupId("group-1") + .smsPhoneNumber("+1234567890") + .build(); + + // Add `metadata` to the metadata of the outgoing message. + return Message.of("Hello", Metadata.of(metadata)); + // end::code[] + } +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/sns/outbound/SnsProducer.java b/content/docs/smallrye-reactive-messaging/4/_examples/sns/outbound/SnsProducer.java new file mode 100644 index 000000000..cde773fa6 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/sns/outbound/SnsProducer.java @@ -0,0 +1,22 @@ +package sns.outbound; + +import java.time.Duration; +import java.util.UUID; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +@ApplicationScoped +public class SnsProducer { + + @Outgoing("data") + public Multi generate() { + // It emits a UUID every second + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .map(x -> UUID.randomUUID().toString()); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/sqs/inbound/SqsCustomAckStrategy.java b/content/docs/smallrye-reactive-messaging/4/_examples/sqs/inbound/SqsCustomAckStrategy.java new file mode 100644 index 000000000..ad8b679ba --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/sqs/inbound/SqsCustomAckStrategy.java @@ -0,0 +1,36 @@ +package sqs.inbound; + +import java.util.function.BiConsumer; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.smallrye.common.annotation.Identifier; +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.aws.sqs.SqsAckHandler; +import io.smallrye.reactive.messaging.aws.sqs.SqsConnectorIncomingConfiguration; +import io.smallrye.reactive.messaging.aws.sqs.SqsMessage; +import io.vertx.mutiny.core.Vertx; +import software.amazon.awssdk.services.sqs.SqsAsyncClient; + +public class SqsCustomAckStrategy implements SqsAckHandler { + + @ApplicationScoped + @Identifier("custom") + public static class Factory implements SqsAckHandler.Factory { + + @Override + public SqsAckHandler create(SqsConnectorIncomingConfiguration conf, + Vertx vertx, + SqsAsyncClient client, + Uni queueUrlUni, + BiConsumer reportFailure) { + return new SqsCustomAckStrategy(); + } + } + + @Override + public Uni handle(SqsMessage message) { + return Uni.createFrom().voidItem() + .emitOn(message::runOnMessageContext); + } +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/sqs/inbound/SqsCustomNackStrategy.java b/content/docs/smallrye-reactive-messaging/4/_examples/sqs/inbound/SqsCustomNackStrategy.java new file mode 100644 index 000000000..6daeec399 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/sqs/inbound/SqsCustomNackStrategy.java @@ -0,0 +1,33 @@ +package sqs.inbound; + +import java.util.function.BiConsumer; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Metadata; + +import io.smallrye.common.annotation.Identifier; +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.aws.sqs.SqsFailureHandler; +import io.smallrye.reactive.messaging.aws.sqs.SqsMessage; +import software.amazon.awssdk.services.sqs.SqsAsyncClient; + +public class SqsCustomNackStrategy implements SqsFailureHandler { + + @ApplicationScoped + @Identifier("custom") + public static class Factory implements SqsFailureHandler.Factory { + + @Override + public SqsFailureHandler create(String channel, SqsAsyncClient client, Uni queueUrlUni, + BiConsumer reportFailure) { + return new SqsCustomNackStrategy(); + } + } + + @Override + public Uni handle(SqsMessage message, Metadata metadata, Throwable throwable) { + return Uni.createFrom().voidItem() + .emitOn(message::runOnMessageContext); + } +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/sqs/inbound/SqsMessageStringConsumer.java b/content/docs/smallrye-reactive-messaging/4/_examples/sqs/inbound/SqsMessageStringConsumer.java new file mode 100644 index 000000000..218e26f0c --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/sqs/inbound/SqsMessageStringConsumer.java @@ -0,0 +1,17 @@ +package sqs.inbound; + +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; + +@ApplicationScoped +public class SqsMessageStringConsumer { + @Incoming("data") + CompletionStage consume(Message msg) { + System.out.println("Received: " + msg.getPayload()); + return msg.ack(); + } +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/sqs/inbound/SqsMetadataExample.java b/content/docs/smallrye-reactive-messaging/4/_examples/sqs/inbound/SqsMetadataExample.java new file mode 100644 index 000000000..cd1b4a5aa --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/sqs/inbound/SqsMetadataExample.java @@ -0,0 +1,21 @@ +package sqs.inbound; + +import java.util.Map; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; + +import io.smallrye.reactive.messaging.aws.sqs.SqsIncomingMetadata; +import software.amazon.awssdk.services.sqs.model.MessageAttributeValue; + +@ApplicationScoped +public class SqsMetadataExample { + + @Incoming("queue") + public void metadata(String body, SqsIncomingMetadata metadata) { + Map attributes = metadata.getMessage().messageAttributes(); + attributes.forEach((k, v) -> System.out.println(k + " -> " + v.stringValue())); + System.out.println("Message body: " + body); + } +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/sqs/inbound/SqsReceiveMessageRequestCustomizerExample.java b/content/docs/smallrye-reactive-messaging/4/_examples/sqs/inbound/SqsReceiveMessageRequestCustomizerExample.java new file mode 100644 index 000000000..1316a6c0a --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/sqs/inbound/SqsReceiveMessageRequestCustomizerExample.java @@ -0,0 +1,17 @@ +package sqs.inbound; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.smallrye.common.annotation.Identifier; +import io.smallrye.reactive.messaging.aws.sqs.SqsReceiveMessageRequestCustomizer; +import software.amazon.awssdk.services.sqs.model.ReceiveMessageRequest; + +@Identifier("my-customizer") // or with channel name @Identifier("data") +@ApplicationScoped +public class SqsReceiveMessageRequestCustomizerExample implements SqsReceiveMessageRequestCustomizer { + @Override + public void customize(ReceiveMessageRequest.Builder builder) { + builder.visibilityTimeout(10) + .messageAttributeNames("my-attribute"); + } +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/sqs/inbound/SqsSdkMessageConsumer.java b/content/docs/smallrye-reactive-messaging/4/_examples/sqs/inbound/SqsSdkMessageConsumer.java new file mode 100644 index 000000000..bdaf6fd90 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/sqs/inbound/SqsSdkMessageConsumer.java @@ -0,0 +1,21 @@ +package sqs.inbound; + +import java.util.Map; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; + +import software.amazon.awssdk.services.sqs.model.Message; +import software.amazon.awssdk.services.sqs.model.MessageAttributeValue; + +@ApplicationScoped +public class SqsSdkMessageConsumer { + + @Incoming("data") + void consume(Message msg) { + System.out.println("Received: " + msg.body()); + Map attributes = msg.messageAttributes(); + // ... + } +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/sqs/inbound/SqsStringConsumer.java b/content/docs/smallrye-reactive-messaging/4/_examples/sqs/inbound/SqsStringConsumer.java new file mode 100644 index 000000000..ffdfcf6d9 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/sqs/inbound/SqsStringConsumer.java @@ -0,0 +1,13 @@ +package sqs.inbound; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; + +@ApplicationScoped +public class SqsStringConsumer { + @Incoming("data") + void consume(String messageBody) { + System.out.println("Received: " + messageBody); + } +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/sqs/json/SqsJsonMapping.java b/content/docs/smallrye-reactive-messaging/4/_examples/sqs/json/SqsJsonMapping.java new file mode 100644 index 000000000..4980edbb8 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/sqs/json/SqsJsonMapping.java @@ -0,0 +1,56 @@ +package sqs.json; + +import java.time.Duration; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +public class SqsJsonMapping { + + // tag::code[] + @ApplicationScoped + public static class Generator { + + @Outgoing("to-rabbitmq") + public Multi prices() { + AtomicInteger count = new AtomicInteger(); + return Multi.createFrom().ticks().every(Duration.ofMillis(1000)) + .map(l -> new Price().setPrice(count.incrementAndGet())) + .onOverflow().drop(); + } + + } + + @ApplicationScoped + public static class Consumer { + + List prices = new CopyOnWriteArrayList<>(); + + @Incoming("from-rabbitmq") + public void consume(Price price) { + prices.add(price); + } + + public List list() { + return prices; + } + } + + public static class Price { + public int price; + + public Price setPrice(int price) { + this.price = price; + return this; + } + } + // end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/sqs/outbound/SqsMessageStringProducer.java b/content/docs/smallrye-reactive-messaging/4/_examples/sqs/outbound/SqsMessageStringProducer.java new file mode 100644 index 000000000..eac978671 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/sqs/outbound/SqsMessageStringProducer.java @@ -0,0 +1,27 @@ +package sqs.outbound; + +import java.time.Duration; +import java.util.UUID; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Metadata; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; +import io.smallrye.reactive.messaging.aws.sqs.SqsOutboundMetadata; + +@ApplicationScoped +public class SqsMessageStringProducer { + + @Outgoing("prices") + public Multi> generate() { + // It emits a UUID every second + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .map(x -> Message.of(UUID.randomUUID().toString(), + Metadata.of(SqsOutboundMetadata.builder() + .groupId("group-1") + .build()))); + } +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/sqs/outbound/SqsOutboundMetadataExample.java b/content/docs/smallrye-reactive-messaging/4/_examples/sqs/outbound/SqsOutboundMetadataExample.java new file mode 100644 index 000000000..e512f4743 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/sqs/outbound/SqsOutboundMetadataExample.java @@ -0,0 +1,27 @@ +package sqs.outbound; + +import java.util.Map; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Metadata; + +import io.smallrye.reactive.messaging.aws.sqs.SqsOutboundMetadata; +import software.amazon.awssdk.services.sqs.model.MessageAttributeValue; + +public class SqsOutboundMetadataExample { + + public Message metadata(Message incoming) { + + // tag::code[] + final SqsOutboundMetadata metadata = SqsOutboundMetadata.builder() + .messageAttributes(Map.of("my-attribute", MessageAttributeValue.builder() + .dataType("String").stringValue("my-value").build())) + .groupId("group-1") + .build(); + + // Add `metadata` to the metadata of the outgoing message. + return Message.of("Hello", Metadata.of(metadata)); + // end::code[] + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/sqs/outbound/SqsStringProducer.java b/content/docs/smallrye-reactive-messaging/4/_examples/sqs/outbound/SqsStringProducer.java new file mode 100644 index 000000000..4a39f5b4e --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/sqs/outbound/SqsStringProducer.java @@ -0,0 +1,22 @@ +package sqs.outbound; + +import java.time.Duration; +import java.util.UUID; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +@ApplicationScoped +public class SqsStringProducer { + + @Outgoing("data") + public Multi generate() { + // It emits a UUID every second + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .map(x -> UUID.randomUUID().toString()); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/testing/MyTest.java b/content/docs/smallrye-reactive-messaging/4/_examples/testing/MyTest.java new file mode 100644 index 000000000..115a6e3d2 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/testing/MyTest.java @@ -0,0 +1,52 @@ +package testing; + +import jakarta.enterprise.inject.Any; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import io.smallrye.reactive.messaging.memory.InMemoryConnector; +import io.smallrye.reactive.messaging.memory.InMemorySink; +import io.smallrye.reactive.messaging.memory.InMemorySource; + +// @io.quarkus.test.junit.QuarkusTest or the Junit 5 extension that allows injection in tests +public class MyTest { + + // 1. Switch the channels to the in-memory connector: + @BeforeAll + public static void switchMyChannels() { + InMemoryConnector.switchIncomingChannelsToInMemory("prices"); + InMemoryConnector.switchOutgoingChannelsToInMemory("processed-prices"); + } + + // 2. Don't forget to reset the channel after the tests: + @AfterAll + public static void revertMyChannels() { + InMemoryConnector.clear(); + } + + // 3. Inject the in-memory connector in your test, + // or use the bean manager to retrieve the instance + @Inject + @Any + InMemoryConnector connector; + + @Test + void test() { + // 4. Retrieves the in-memory source to send message + InMemorySource prices = connector.source("prices"); + // 5. Retrieves the in-memory sink to check what is received + InMemorySink results = connector.sink("processed-prices"); + + // 6. Send fake messages: + prices.send(1); + prices.send(2); + prices.send(3); + + // 7. Check you have received the expected messages + Assertions.assertEquals(3, results.received().size()); + } +} diff --git a/content/docs/smallrye-reactive-messaging/4/_examples/testing/MyTestSetup.java b/content/docs/smallrye-reactive-messaging/4/_examples/testing/MyTestSetup.java new file mode 100644 index 000000000..01d0f6b91 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_examples/testing/MyTestSetup.java @@ -0,0 +1,23 @@ +package testing; + +import java.util.HashMap; +import java.util.Map; + +import io.smallrye.reactive.messaging.memory.InMemoryConnector; + +public class MyTestSetup { + + // tag::code[] + public Map start() { + Map env = new HashMap<>(); + env.putAll(InMemoryConnector.switchIncomingChannelsToInMemory("prices")); + env.putAll(InMemoryConnector.switchOutgoingChannelsToInMemory("my-data-stream")); + return env; + } + + public void stop() { + InMemoryConnector.clear(); + } + // end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/4/_generated/smallrye-amqp-incoming.adoc b/content/docs/smallrye-reactive-messaging/4/_generated/smallrye-amqp-incoming.adoc new file mode 100644 index 000000000..9804e5883 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_generated/smallrye-amqp-incoming.adoc @@ -0,0 +1,37 @@ +[cols="<,<,^,^,<",options="header",] +|=== +|Attribute (_alias_) |Description |Type |Mandatory |Default +|_address_ |The AMQP address. If not set, the channel name is used |string |false | +|_auto-acknowledgement_ |Whether the received AMQP messages must be acknowledged when received |boolean |false |`false` +|_broadcast_ |Whether the received AMQP messages must be dispatched to multiple _subscribers_ |boolean |false |`false` +|_capabilities_ |A comma-separated list of capabilities proposed by the sender or receiver client. |string |false | +|_cbs.audience_ |The audience used to authenticate with the broker using the CBS exchange. If not set, the audience is derived from the address. |string |false |++``++ +|_cbs.enabled_ |Whether the CBS authorization is enabled. If enabled, the connector will use the claims-based security to authenticate with the broker. If disabled, the connector will not use the CBS exchange and will not authenticate with the broker. |boolean |false |`false` +|_cbs.exchange_ |The name of the CBS exchange used to authenticate with the broker. |string |false |`set-token` +|_cbs.scopes_ |The scopes used to authenticate with the broker using the CBS exchange. If not set, the scopes are derived from the address. |string |false |++``++ +|_cbs.token-manager_ |The name of the CbsTokenManager bean used to manage the CBS tokens. |string |false |`default-cbs-token-manager` +|_client-options-name_ _(amqp-client-options-name)_ |The name of the AMQP Client Option bean used to customize the AMQP client configuration |string |false | +|_cloud-events_ |Enables (default) or disables the Cloud Event support. If enabled on an _incoming_ channel, the connector analyzes the incoming records and try to create Cloud Event metadata. If enabled on an _outgoing_, the connector sends the outgoing messages as Cloud Event if the message includes Cloud Event Metadata. |boolean |false |`true` +|_connect-timeout_ _(amqp-connect-timeout)_ |The connection timeout in milliseconds |int |false |`1000` +|_container-id_ |The AMQP container id. Channels with the same container id share the same AMQP connection. |string |false | +|_durable_ |Whether AMQP subscription is durable |boolean |false |`false` +|_failure-strategy_ |Specify the failure strategy to apply when a message produced from an AMQP message is nacked. Accepted values are `fail` (default), `accept`, `release`, `reject`, `modified-failed`, `modified-failed-undeliverable-here` |string |false |`fail` +|_health-enabled_ |Whether health reporting is enabled (default) or disabled |boolean |false |`true` +|_health-timeout_ |The max number of seconds to wait to determine if the connection with the broker is still established for the readiness check. After that threshold, the check is considered as failed. |int |false |`3` +|_host_ _(amqp-host)_ |The broker hostname |string |false |`localhost` +|_initial-credits_ |The number of credits initially requested for incoming messages. This controls how many messages the receiver can buffer. |int |false |`1000` +|_link-name_ |The name of the link. If not set, the channel name is used. |string |false | +|_password_ _(amqp-password)_ |The password used to authenticate to the broker |string |false | +|_port_ _(amqp-port)_ |The broker port |int |false |`5672` +|_qos_ |Sets the local QOS config, values can be ++{++@code null}, ++{++@code AT++_++MOST++_++ONCE} or ++{++@code AT++_++LEAST++_++ONCE}. |string |false | +|_reconnect-attempts_ _(amqp-reconnect-attempts)_ |The number of reconnection attempts |int |false |`100` +|_reconnect-interval_ _(amqp-reconnect-interval)_ |The interval in second between two reconnection attempts |int |false |`10` +|_retry-on-fail-attempts_ |The number of tentative to retry on failure |int |false |`6` +|_retry-on-fail-interval_ |The interval (in seconds) between two sending attempts |int |false |`5` +|_selector_ |Sets a message selector. This attribute is used to define an `apache.org:selector-filter:string` filter on the source terminus, using SQL-based syntax to request the server filters which messages are delivered to the receiver (if supported by the server in question). Precise functionality supported and syntax needed can vary depending on the server. |string |false | +|_sni-server-name_ _(amqp-sni-server-name)_ |If set, explicitly override the hostname to use for the TLS SNI server name |string |false | +|_tracing-enabled_ |Whether tracing is enabled (default) or disabled |boolean |false |`true` +|_use-ssl_ _(amqp-use-ssl)_ |Whether the AMQP connection uses SSL/TLS |boolean |false |`false` +|_username_ _(amqp-username)_ |The username used to authenticate to the broker |string |false | +|_virtual-host_ _(amqp-virtual-host)_ |If set, configure the hostname value used for the connection AMQP Open frame and TLS SNI server name (if TLS is in use) |string |false | +|=== diff --git a/content/docs/smallrye-reactive-messaging/4/_generated/smallrye-amqp-outgoing.adoc b/content/docs/smallrye-reactive-messaging/4/_generated/smallrye-amqp-outgoing.adoc new file mode 100644 index 000000000..e38833d3d --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_generated/smallrye-amqp-outgoing.adoc @@ -0,0 +1,44 @@ +[cols="<,<,^,^,<",options="header",] +|=== +|Attribute (_alias_) |Description |Type |Mandatory |Default +|_address_ |The AMQP address. If not set, the channel name is used |string |false | +|_capabilities_ |A comma-separated list of capabilities proposed by the sender or receiver client. |string |false | +|_cbs.audience_ |The audience used to authenticate with the broker using the CBS exchange. If not set, the audience is derived from the address. |string |false |++``++ +|_cbs.enabled_ |Whether the CBS authorization is enabled. If enabled, the connector will use the claims-based security to authenticate with the broker. If disabled, the connector will not use the CBS exchange and will not authenticate with the broker. |boolean |false |`false` +|_cbs.exchange_ |The name of the CBS exchange used to authenticate with the broker. |string |false |`set-token` +|_cbs.scopes_ |The scopes used to authenticate with the broker using the CBS exchange. If not set, the scopes are derived from the address. |string |false |++``++ +|_cbs.token-manager_ |The name of the CbsTokenManager bean used to manage the CBS tokens. |string |false |`default-cbs-token-manager` +|_client-options-name_ _(amqp-client-options-name)_ |The name of the AMQP Client Option bean used to customize the AMQP client configuration |string |false | +|_cloud-events_ |Enables (default) or disables the Cloud Event support. If enabled on an _incoming_ channel, the connector analyzes the incoming records and try to create Cloud Event metadata. If enabled on an _outgoing_, the connector sends the outgoing messages as Cloud Event if the message includes Cloud Event Metadata. |boolean |false |`true` +|_cloud-events-data-content-type_ _(cloud-events-default-data-content-type)_ |Configure the default `datacontenttype` attribute of the outgoing Cloud Event. Requires `cloud-events` to be set to `true`. This value is used if the message does not configure the `datacontenttype` attribute itself |string |false | +|_cloud-events-data-schema_ _(cloud-events-default-data-schema)_ |Configure the default `dataschema` attribute of the outgoing Cloud Event. Requires `cloud-events` to be set to `true`. This value is used if the message does not configure the `dataschema` attribute itself |string |false | +|_cloud-events-insert-timestamp_ _(cloud-events-default-timestamp)_ |Whether or not the connector should insert automatically the `time` attribute into the outgoing Cloud Event. Requires `cloud-events` to be set to `true`. This value is used if the message does not configure the `time` attribute itself |boolean |false |`true` +|_cloud-events-mode_ |The Cloud Event mode (`structured` or `binary` (default)). Indicates how are written the cloud events in the outgoing record |string |false |`binary` +|_cloud-events-source_ _(cloud-events-default-source)_ |Configure the default `source` attribute of the outgoing Cloud Event. Requires `cloud-events` to be set to `true`. This value is used if the message does not configure the `source` attribute itself |string |false | +|_cloud-events-subject_ _(cloud-events-default-subject)_ |Configure the default `subject` attribute of the outgoing Cloud Event. Requires `cloud-events` to be set to `true`. This value is used if the message does not configure the `subject` attribute itself |string |false | +|_cloud-events-type_ _(cloud-events-default-type)_ |Configure the default `type` attribute of the outgoing Cloud Event. Requires `cloud-events` to be set to `true`. This value is used if the message does not configure the `type` attribute itself |string |false | +|_connect-timeout_ _(amqp-connect-timeout)_ |The connection timeout in milliseconds |int |false |`1000` +|_container-id_ |The AMQP container id. Channels with the same container id share the same AMQP connection. |string |false | +|_credit-retrieval-period_ |The period (in milliseconds) between two attempts to retrieve the credits granted by the broker. This time is used when the sender run out of credits. |int |false |`2000` +|_durable_ |Whether sent AMQP messages are marked durable |boolean |false |`false` +|_health-enabled_ |Whether health reporting is enabled (default) or disabled |boolean |false |`true` +|_health-timeout_ |The max number of seconds to wait to determine if the connection with the broker is still established for the readiness check. After that threshold, the check is considered as failed. |int |false |`3` +|_host_ _(amqp-host)_ |The broker hostname |string |false |`localhost` +|_lazy-client_ |Whether to create the connection and sender at startup or at first send request |boolean |false |`true` +|_link-name_ |The name of the link. If not set, the channel name is used. |string |false | +|_max-inflight-messages_ |The maximum number of messages to be written to the broker concurrently. The number of sent messages waiting to be acknowledged by the broker are limited by this value and credits granted by the broker. The default value `0` means only credits apply. |long |false |`0` +|_merge_ |Whether the connector should allow multiple upstreams |boolean |false |`false` +|_password_ _(amqp-password)_ |The password used to authenticate to the broker |string |false | +|_port_ _(amqp-port)_ |The broker port |int |false |`5672` +|_reconnect-attempts_ _(amqp-reconnect-attempts)_ |The number of reconnection attempts |int |false |`100` +|_reconnect-interval_ _(amqp-reconnect-interval)_ |The interval in second between two reconnection attempts |int |false |`10` +|_retry-on-fail-attempts_ |The number of tentative to retry on failure |int |false |`6` +|_retry-on-fail-interval_ |The interval (in seconds) between two sending attempts |int |false |`5` +|_sni-server-name_ _(amqp-sni-server-name)_ |If set, explicitly override the hostname to use for the TLS SNI server name |string |false | +|_tracing-enabled_ |Whether tracing is enabled (default) or disabled |boolean |false |`true` +|_ttl_ |The time-to-live of the send AMQP messages. 0 to disable the TTL |long |false |`0` +|_use-anonymous-sender_ |Whether or not the connector should use an anonymous sender. Default value is `true` if the broker supports it, `false` otherwise. If not supported, it is not possible to dynamically change the destination address. |boolean |false | +|_use-ssl_ _(amqp-use-ssl)_ |Whether the AMQP connection uses SSL/TLS |boolean |false |`false` +|_username_ _(amqp-username)_ |The username used to authenticate to the broker |string |false | +|_virtual-host_ _(amqp-virtual-host)_ |If set, configure the hostname value used for the connection AMQP Open frame and TLS SNI server name (if TLS is in use) |string |false | +|=== diff --git a/content/docs/smallrye-reactive-messaging/4/_generated/smallrye-camel-incoming.adoc b/content/docs/smallrye-reactive-messaging/4/_generated/smallrye-camel-incoming.adoc new file mode 100644 index 000000000..e14c417cf --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_generated/smallrye-camel-incoming.adoc @@ -0,0 +1,6 @@ +[cols="<,<,^,^,<",options="header",] +|=== +|Attribute (_alias_) |Description |Type |Mandatory |Default +|_endpoint-uri_ |The URI of the Camel endpoint (read from or written to) |string |true | +|_failure-strategy_ |Specify the failure strategy to apply when a message produced from a Camel exchange is nacked. Values can be `fail` (default) or `ignore` |string |false |`fail` +|=== diff --git a/content/docs/smallrye-reactive-messaging/4/_generated/smallrye-camel-outgoing.adoc b/content/docs/smallrye-reactive-messaging/4/_generated/smallrye-camel-outgoing.adoc new file mode 100644 index 000000000..ae1860580 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_generated/smallrye-camel-outgoing.adoc @@ -0,0 +1,6 @@ +[cols="<,<,^,^,<",options="header",] +|=== +|Attribute (_alias_) |Description |Type |Mandatory |Default +|_endpoint-uri_ |The URI of the Camel endpoint (read from or written to) |string |true | +|_merge_ |Whether the connector should allow multiple upstreams |boolean |false |`false` +|=== diff --git a/content/docs/smallrye-reactive-messaging/4/_generated/smallrye-jms-incoming.adoc b/content/docs/smallrye-reactive-messaging/4/_generated/smallrye-jms-incoming.adoc new file mode 100644 index 000000000..d93940a5d --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_generated/smallrye-jms-incoming.adoc @@ -0,0 +1,25 @@ +[cols="<,<,^,^,<",options="header",] +|=== +|Attribute (_alias_) |Description |Type |Mandatory |Default +|_broadcast_ |Whether or not the JMS message should be dispatched to multiple consumers |boolean |false |`false` +|_client-id_ |The client id |String |false | +|_connection-factory-name_ |The name of the JMS connection factory (`jakarta.jms.ConnectionFactory`) to be used. If not set, it uses any exposed JMS connection factory |String |false | +|_dead-letter-queue.destination_ |When the `failure-strategy` is set to `dead-letter-queue` indicates on which queue the message is sent. Defaults is `dead-letter-topic-$channel` |string |false | +|_dead-letter-queue.producer-client-id_ |When the `failure-strategy` is set to `dead-letter-queue` indicates what client id the generated producer should use. Defaults is `jms-dead-letter-topic-producer-$client-id` |string |false | +|_destination_ |The name of the JMS destination. If not set the name of the channel is used |String |false | +|_destination-type_ |The type of destination. It can be either `queue` or `topic` |string |false |`queue` +|_durable_ |Set to `true` to use a durable subscription |boolean |false |`false` +|_failure-strategy_ |Specify the failure strategy to apply when a message produced from a record is acknowledged negatively (nack). Values can be `fail` (default), `ignore`, or `dead-letter-queue` |string |false |`fail` +|_no-local_ |Enable or disable local delivery |boolean |false |`false` +|_password_ |The password to connect to to the JMS server |String |false | +|_retry_ |Whether to retry on terminal stream errors. |boolean |false |`true` +|_retry.initial-delay_ |The initial delay for the retry. |string |false |`PT1S` +|_retry.jitter_ |How much the delay jitters as a multiplier between 0 and 1. The formula is current delay ++*++ jitter. For example, with a current delay of 2H, a jitter of 0.5 will result in an actual delay somewhere between 1H and 3H. |double |false |`0.5` +|_retry.max-delay_ |The maximum delay |string |false |`PT10S` +|_retry.max-retries_ |Maximum number of retries for terminal stream errors. |int |false |`3` +|_reuse-jms-context_ |Whether to reuse JMS contexts by creating child contexts from a parent context. When enabled, child contexts will be created using JMSContext.createContext() instead of ConnectionFactory.createContext() |boolean |false |`false` +|_selector_ |The JMS selector |String |false | +|_session-mode_ |The session mode. Accepted values are AUTO++_++ACKNOWLEDGE, SESSION++_++TRANSACTED, CLIENT++_++ACKNOWLEDGE, DUPS++_++OK++_++ACKNOWLEDGE |String |false |`AUTO++_++ACKNOWLEDGE` +|_tracing-enabled_ |Whether tracing is enabled (default) or disabled |boolean |false |`true` +|_username_ |The username to connect to to the JMS server |String |false | +|=== diff --git a/content/docs/smallrye-reactive-messaging/4/_generated/smallrye-jms-outgoing.adoc b/content/docs/smallrye-reactive-messaging/4/_generated/smallrye-jms-outgoing.adoc new file mode 100644 index 000000000..335b694e3 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_generated/smallrye-jms-outgoing.adoc @@ -0,0 +1,28 @@ +[cols="<,<,^,^,<",options="header",] +|=== +|Attribute (_alias_) |Description |Type |Mandatory |Default +|_client-id_ |The client id |String |false | +|_connection-factory-name_ |The name of the JMS connection factory (`jakarta.jms.ConnectionFactory`) to be used. If not set, it uses any exposed JMS connection factory |String |false | +|_correlation-id_ |The JMS Message correlation id |string |false | +|_delivery-delay_ |The delivery delay |long |false | +|_delivery-mode_ |The delivery mode. Either `persistent` or `non++_++persistent` |string |false | +|_destination_ |The name of the JMS destination. If not set the name of the channel is used |String |false | +|_destination-type_ |The type of destination. It can be either `queue` or `topic` |string |false |`queue` +|_disable-message-id_ |Omit the message id in the outbound JMS message |boolean |false | +|_disable-message-timestamp_ |Omit the message timestamp in the outbound JMS message |boolean |false | +|_merge_ |Whether the connector should allow multiple upstreams |boolean |false |`false` +|_password_ |The password to connect to to the JMS server |String |false | +|_priority_ |The JMS Message priority |int |false | +|_reply-to_ |The reply to destination if any |string |false | +|_reply-to-destination-type_ |The type of destination for the response. It can be either `queue` or `topic` |string |false |`queue` +|_retry_ |Whether to retry on terminal stream errors. |boolean |false |`true` +|_retry.initial-delay_ |The initial delay for the retry. |string |false |`PT1S` +|_retry.jitter_ |How much the delay jitters as a multiplier between 0 and 1. The formula is current delay ++*++ jitter. For example, with a current delay of 2H, a jitter of 0.5 will result in an actual delay somewhere between 1H and 3H. |double |false |`0.5` +|_retry.max-delay_ |The maximum delay |string |false |`PT10S` +|_retry.max-retries_ |Maximum number of retries for terminal stream errors. |int |false |`3` +|_reuse-jms-context_ |Whether to reuse JMS contexts by creating child contexts from a parent context. When enabled, child contexts will be created using JMSContext.createContext() instead of ConnectionFactory.createContext() |boolean |false |`false` +|_session-mode_ |The session mode. Accepted values are AUTO++_++ACKNOWLEDGE, SESSION++_++TRANSACTED, CLIENT++_++ACKNOWLEDGE, DUPS++_++OK++_++ACKNOWLEDGE |String |false |`AUTO++_++ACKNOWLEDGE` +|_tracing-enabled_ |Whether tracing is enabled (default) or disabled |boolean |false |`true` +|_ttl_ |The JMS Message time-to-live |long |false | +|_username_ |The username to connect to to the JMS server |String |false | +|=== diff --git a/content/docs/smallrye-reactive-messaging/4/_generated/smallrye-kafka-incoming.adoc b/content/docs/smallrye-reactive-messaging/4/_generated/smallrye-kafka-incoming.adoc new file mode 100644 index 000000000..54dfa4117 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_generated/smallrye-kafka-incoming.adoc @@ -0,0 +1,65 @@ +[cols="<,<,^,^,<",options="header",] +|=== +|Attribute (_alias_) |Description |Type |Mandatory |Default +|_assign-seek_ |Assign partitions and optionally seek to offsets, instead of subscribing to topics. A comma-separating list of triplets in form of ++`++: | |:`to assign statically to the consumer and seek to the given offsets. Offset`0`seeks to beginning and offset`-1++`++ seeks to the end of the topic-partition. If the topic is omitted the configured topic will be used. If the offset is omitted partitions are assigned to the consumer but won't be seeked to offset. |string +|_auto.offset.reset_ |What to do when there is no initial offset in Kafka.Accepted values are earliest, latest and none |string |false |`latest` +|_batch_ |Whether the Kafka records are consumed in batch. The channel injection point must consume a compatible type, such as `List++<++Payload++>++` or `KafkaRecordBatch++<++Payload++>++`. |boolean |false |`false` +|_bootstrap.servers_ _(kafka.bootstrap.servers)_ |A comma-separated list of host:port to use for establishing the initial connection to the Kafka cluster. |string |false |`localhost:9092` +|_broadcast_ |Whether the Kafka records should be dispatched to multiple consumer |boolean |false |`false` +|_checkpoint.state-store_ |While using the `checkpoint` commit-strategy, the name set in `@Identifier` of a bean that implements `io.smallrye.reactive.messaging.kafka.StateStore.Factory` to specify the state store implementation. |string |false | +|_checkpoint.state-type_ |While using the `checkpoint` commit-strategy, the fully qualified type name of the state object to persist in the state store. When provided, it can be used by the state store implementation to help persisting the processing state object. |string |false | +|_checkpoint.unsynced-state-max-age.ms_ |While using the `checkpoint` commit-strategy, specify the max age in milliseconds that the processing state must be persisted before the connector is marked as unhealthy. Setting this attribute to 0 disables this monitoring. |int |false |`10000` +|_client-id-prefix_ |Prefix for Kafka client `client.id` attribute. If defined configured or generated `client.id` will be prefixed with the given value. |string |false | +|_cloud-events_ |Enables (default) or disables the Cloud Event support. If enabled on an _incoming_ channel, the connector analyzes the incoming records and try to create Cloud Event metadata. If enabled on an _outgoing_, the connector sends the outgoing messages as Cloud Event if the message includes Cloud Event Metadata. |boolean |false |`true` +|_commit-strategy_ |Specify the commit strategy to apply when a message produced from a record is acknowledged. Values can be `latest`, `ignore` or `throttled`. If `enable.auto.commit` is true then the default is `ignore` otherwise it is `throttled` |string |false | +|_consumer-rebalance-listener.name_ |The name set in `@Identifier` of a bean that implements `io.smallrye.reactive.messaging.kafka.KafkaConsumerRebalanceListener`. If set, this rebalance listener is applied to the consumer. |string |false | +|_dead-letter-queue.key.serializer_ |When the `failure-strategy` is set to `dead-letter-queue` indicates the key serializer to use. If not set the serializer associated to the key deserializer is used |string |false | +|_dead-letter-queue.producer-client-id_ |When the `failure-strategy` is set to `dead-letter-queue` indicates what client id the generated producer should use. Defaults is `kafka-dead-letter-topic-producer-$client-id` |string |false | +|_dead-letter-queue.topic_ |When the `failure-strategy` is set to `dead-letter-queue` indicates on which topic the record is sent. Defaults is `dead-letter-topic-$channel` |string |false | +|_dead-letter-queue.value.serializer_ |When the `failure-strategy` is set to `dead-letter-queue` indicates the value serializer to use. If not set the serializer associated to the value deserializer is used |string |false | +|_delayed-retry-topic.max-retries_ |When the `failure-strategy` is set to `delayed-retry-topic` indicates the maximum number of retries. If higher than the number of delayed retry topics, last topic is used. |int |false | +|_delayed-retry-topic.timeout_ |When the `failure-strategy` is set to `delayed-retry-topic` indicates the global timeout per record. |int |false |`120000` +|_delayed-retry-topic.topics_ |When the `failure-strategy` is set to `delayed-retry-topic` indicates topics to use. If not set the source channel name is used, with 10, 20 and 50 seconds delayed topics. |string |false | +|_enable.auto.commit_ |If enabled, consumer's offset will be periodically committed in the background by the underlying Kafka client, ignoring the actual processing outcome of the records. It is recommended to NOT enable this setting and let Reactive Messaging handles the commit. |boolean |false |`false` +|_fail-on-deserialization-failure_ |When no deserialization failure handler is set and a deserialization failure happens, report the failure and mark the application as unhealthy. If set to `false` and a deserialization failure happens, a `null` value is forwarded. |boolean |false |`true` +|_failure-strategy_ |Specify the failure strategy to apply when a message produced from a record is acknowledged negatively (nack). Values can be `fail` (default), `ignore`, or `dead-letter-queue` |string |false |`fail` +|_fetch.min.bytes_ |The minimum amount of data the server should return for a fetch request. The default setting of 1 byte means that fetch requests are answered as soon as a single byte of data is available or the fetch request times out waiting for data to arrive. |int |false |`1` +|_graceful-shutdown_ |Whether or not a graceful shutdown should be attempted when the application terminates. |boolean |false |`true` +|_group.id_ |A unique string that identifies the consumer group the application belongs to. If not set, a unique, generated id is used |string |false | +|_health-enabled_ |Whether health reporting is enabled (default) or disabled |boolean |false |`true` +|_health-readiness-enabled_ |Whether readiness health reporting is enabled (default) or disabled |boolean |false |`true` +|_health-readiness-timeout_ |_deprecated_ - During the readiness health check, the connector connects to the broker and retrieves the list of topics. This attribute specifies the maximum duration (in ms) for the retrieval. If exceeded, the channel is considered not-ready. Deprecated: Use 'health-topic-verification-timeout' instead. |long |false | +|_health-readiness-topic-verification_ |_deprecated_ - Whether the readiness check should verify that topics exist on the broker. Default to false. Enabling it requires an admin connection. Deprecated: Use 'health-topic-verification-enabled' instead. |boolean |false | +|_health-topic-verification-enabled_ |Whether the startup and readiness check should verify that topics exist on the broker. Default to false. Enabling it requires an admin client connection. |boolean |false |`false` +|_health-topic-verification-readiness-disabled_ |Whether the topic verification is disabled for the readiness health check. |boolean |false |`false` +|_health-topic-verification-startup-disabled_ |Whether the topic verification is disabled for the startup health check. |boolean |false |`false` +|_health-topic-verification-timeout_ |During the startup and readiness health check, the connector connects to the broker and retrieves the list of topics. This attribute specifies the maximum duration (in ms) for the retrieval. If exceeded, the channel is considered not-ready. |long |false |`2000` +|_kafka-configuration_ |Identifier of a CDI bean that provides the default Kafka consumer/producer configuration for this channel. The channel configuration can still override any attribute. The bean must have a type of Map++<++String, Object++>++ and must use the @io.smallrye.common.annotation.Identifier qualifier to set the identifier. |string |false | +|_key-deserialization-failure-handler_ |The name set in `@Identifier` of a bean that implements `io.smallrye.reactive.messaging.kafka.DeserializationFailureHandler`. If set, deserialization failure happening when deserializing keys are delegated to this handler which may retry or provide a fallback value. |string |false | +|_key.deserializer_ |The deserializer classname used to deserialize the record's key |string |false |`org.apache.kafka.common.serialization.StringDeserializer` +|_lazy-client_ |Whether Kafka client is created lazily or eagerly. |boolean |false |`false` +|_max-queue-size-factor_ |Multiplier factor to determine maximum number of records queued for processing, using `max.poll.records` ++*++ `max-queue-size-factor`. Defaults to 2. In `batch` mode `max.poll.records` is considered `1`. |int |false |`2` +|_ordered_ |Configures ordering guarantees for concurrent processing. Possible values: `key` for per-key sequential processing, or `partition` for per-partition sequential processing. Requires `@Blocking(ordered = false)` for concurrency. Works with any commit strategy. |string |false | +|_ordered.max-concurrency_ |Maximum number of groups (keys or partitions) that can be processed concurrently. Defaults to `max.poll.records`. |int |false | +|_partitions_ |_deprecated_ - The number of partitions to be consumed concurrently. The connector creates the specified amount of Kafka consumers. It should match the number of partition of the targeted topic. Deprecated: Use `concurrency` channel attribute instead. |int |false |`1` +|_pattern_ |Indicate that the `topic` property is a regular expression. Must be used with the `topic` property. Cannot be used with the `topics` property |boolean |false |`false` +|_pause-if-no-requests_ |Whether the polling must be paused when the application does not request items and resume when it does. This allows implementing back-pressure based on the application capacity. Note that polling is not stopped, but will not retrieve any records when paused. |boolean |false |`true` +|_poll-timeout_ |The polling timeout in milliseconds. When polling records, the poll will wait at most that duration before returning records. Default is 1000ms |int |false |`1000` +|_requests_ |_deprecated_ - When `partitions` is greater than 1, this attribute allows configuring how many records are requested by each consumers every time. Deprecated: Use `concurrency` channel attribute instead. |int |false |`128` +|_retry_ |Whether or not the connection to the broker is re-attempted in case of failure |boolean |false |`true` +|_retry-attempts_ |The maximum number of reconnection before failing. -1 means infinite retry |int |false |`-1` +|_retry-max-wait_ |The max delay (in seconds) between 2 reconnects |int |false |`30` +|_share-group_ |_deprecated_ - Deprecated, use 'share-group.enabled' instead |boolean |false |`false` +|_share-group.enabled_ |Whether to use Kafka Share Groups for consumption. When enabled, the consumer will use a ShareConsumer which provides cooperative record processing across multiple consumers without explicit partition assignment. |boolean |false |`false` +|_share-group.failure-acknowledgement-type_ |Default acknowledgement type to apply to the record, when the message is nacked. |string |false |`release` +|_share-group.failure-deserialization-acknowledgement-type_ |Default acknowledgement type to apply to the record, when the message is nacked because of failure on deserialization. |string |false |`reject` +|_share-group.unprocessed-record-max-age.ms_ |While using share groups, specify the max age in milliseconds that an unprocessed record can be before the connector reports a failure. Setting this attribute to 0 disables this monitoring. |int |false |`60000` +|_throttled.ordered_ |_deprecated_ - While using the `throttled` commit-strategy, configures ordering guarantees for concurrent processing. Possible values: `key` for per-key sequential processing within partitions, or `partition` for per-partition sequential processing. Requires `@Blocking(ordered = false)` for concurrency. Deprecated: Use `ordered` instead. |string |false | +|_throttled.ordered.max-concurrency_ |_deprecated_ - While using the `throttled` commit-strategy with ordered processing, specifies the maximum number of groups (keys or partitions) that can be processed concurrently. This should typically match your worker pool's `max-concurrency` setting. Defaults to `max.poll.records`. Deprecated: Use `ordered.max-concurrency` instead. |int |false | +|_throttled.unprocessed-record-max-age.ms_ |While using the `throttled` commit-strategy, specify the max age in milliseconds that an unprocessed message can be before the connector is marked as unhealthy. Setting this attribute to 0 disables this monitoring. |int |false |`60000` +|_topic_ |The consumed / populated Kafka topic. If neither this property nor the `topics` properties are set, the channel name is used |string |false | +|_topics_ |A comma-separating list of topics to be consumed. Cannot be used with the `topic` or `pattern` properties |string |false | +|_tracing-enabled_ |Whether tracing is enabled (default) or disabled |boolean |false |`true` +|_value-deserialization-failure-handler_ |The name set in `@Identifier` of a bean that implements `io.smallrye.reactive.messaging.kafka.DeserializationFailureHandler`. If set, deserialization failure happening when deserializing values are delegated to this handler which may retry or provide a fallback value. |string |false | +|_value.deserializer_ |The deserializer classname used to deserialize the record's value |string |true | +|=== diff --git a/content/docs/smallrye-reactive-messaging/4/_generated/smallrye-kafka-outgoing.adoc b/content/docs/smallrye-reactive-messaging/4/_generated/smallrye-kafka-outgoing.adoc new file mode 100644 index 000000000..85f3102a9 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_generated/smallrye-kafka-outgoing.adoc @@ -0,0 +1,46 @@ +[cols="<,<,^,^,<",options="header",] +|=== +|Attribute (_alias_) |Description |Type |Mandatory |Default +|_acks_ |The number of acknowledgments the producer requires the leader to have received before considering a request complete. This controls the durability of records that are sent. Accepted values are: 0, 1, all |string |false |`1` +|_bootstrap.servers_ _(kafka.bootstrap.servers)_ |A comma-separated list of host:port to use for establishing the initial connection to the Kafka cluster. |string |false |`localhost:9092` +|_buffer.memory_ |The total bytes of memory the producer can use to buffer records waiting to be sent to the server. |long |false |`33554432` +|_client-id-prefix_ |Prefix for Kafka client `client.id` attribute. If defined configured or generated `client.id` will be prefixed with the given value. |string |false | +|_close-timeout_ |The amount of milliseconds waiting for a graceful shutdown of the Kafka producer |int |false |`10000` +|_cloud-events_ |Enables (default) or disables the Cloud Event support. If enabled on an _incoming_ channel, the connector analyzes the incoming records and try to create Cloud Event metadata. If enabled on an _outgoing_, the connector sends the outgoing messages as Cloud Event if the message includes Cloud Event Metadata. |boolean |false |`true` +|_cloud-events-data-content-type_ _(cloud-events-default-data-content-type)_ |Configure the default `datacontenttype` attribute of the outgoing Cloud Event. Requires `cloud-events` to be set to `true`. This value is used if the message does not configure the `datacontenttype` attribute itself |string |false | +|_cloud-events-data-schema_ _(cloud-events-default-data-schema)_ |Configure the default `dataschema` attribute of the outgoing Cloud Event. Requires `cloud-events` to be set to `true`. This value is used if the message does not configure the `dataschema` attribute itself |string |false | +|_cloud-events-insert-timestamp_ _(cloud-events-default-timestamp)_ |Whether or not the connector should insert automatically the `time` attribute into the outgoing Cloud Event. Requires `cloud-events` to be set to `true`. This value is used if the message does not configure the `time` attribute itself |boolean |false |`true` +|_cloud-events-mode_ |The Cloud Event mode (`structured` or `binary` (default)). Indicates how are written the cloud events in the outgoing record |string |false |`binary` +|_cloud-events-source_ _(cloud-events-default-source)_ |Configure the default `source` attribute of the outgoing Cloud Event. Requires `cloud-events` to be set to `true`. This value is used if the message does not configure the `source` attribute itself |string |false | +|_cloud-events-subject_ _(cloud-events-default-subject)_ |Configure the default `subject` attribute of the outgoing Cloud Event. Requires `cloud-events` to be set to `true`. This value is used if the message does not configure the `subject` attribute itself |string |false | +|_cloud-events-type_ _(cloud-events-default-type)_ |Configure the default `type` attribute of the outgoing Cloud Event. Requires `cloud-events` to be set to `true`. This value is used if the message does not configure the `type` attribute itself |string |false | +|_health-enabled_ |Whether health reporting is enabled (default) or disabled |boolean |false |`true` +|_health-readiness-enabled_ |Whether readiness health reporting is enabled (default) or disabled |boolean |false |`true` +|_health-readiness-timeout_ |_deprecated_ - During the readiness health check, the connector connects to the broker and retrieves the list of topics. This attribute specifies the maximum duration (in ms) for the retrieval. If exceeded, the channel is considered not-ready. Deprecated: Use 'health-topic-verification-timeout' instead. |long |false | +|_health-readiness-topic-verification_ |_deprecated_ - Whether the readiness check should verify that topics exist on the broker. Default to false. Enabling it requires an admin connection. Deprecated: Use 'health-topic-verification-enabled' instead. |boolean |false | +|_health-topic-verification-enabled_ |Whether the startup and readiness check should verify that topics exist on the broker. Default to false. Enabling it requires an admin client connection. |boolean |false |`false` +|_health-topic-verification-readiness-disabled_ |Whether the topic verification is disabled for the readiness health check. |boolean |false |`false` +|_health-topic-verification-startup-disabled_ |Whether the topic verification is disabled for the startup health check. |boolean |false |`false` +|_health-topic-verification-timeout_ |During the startup and readiness health check, the connector connects to the broker and retrieves the list of topics. This attribute specifies the maximum duration (in ms) for the retrieval. If exceeded, the channel is considered not-ready. |long |false |`2000` +|_interceptor-bean_ |The name set in `@Identifier` of a bean that implements `org.apache.kafka.clients.producer.ProducerInterceptor`. If set, the identified bean will be used as the producer interceptor. |string |false | +|_kafka-configuration_ |Identifier of a CDI bean that provides the default Kafka consumer/producer configuration for this channel. The channel configuration can still override any attribute. The bean must have a type of Map++<++String, Object++>++ and must use the @io.smallrye.common.annotation.Identifier qualifier to set the identifier. |string |false | +|_key_ |A key to used when writing the record |string |false | +|_key-serialization-failure-handler_ |The name set in `@Identifier` of a bean that implements `io.smallrye.reactive.messaging.kafka.SerializationFailureHandler`. If set, serialization failure happening when serializing keys are delegated to this handler which may provide a fallback value. |string |false | +|_key.serializer_ |The serializer classname used to serialize the record's key |string |false |`org.apache.kafka.common.serialization.StringSerializer` +|_lazy-client_ |Whether Kafka client is created lazily or eagerly. |boolean |false |`false` +|_max-inflight-messages_ |The maximum number of messages to be written to Kafka concurrently. It limits the number of messages waiting to be written and acknowledged by the broker. You can set this attribute to `0` remove the limit |long |false |`1024` +|_merge_ |Whether the connector should allow multiple upstreams |boolean |false |`false` +|_partition_ |The target partition id. -1 to let the client determine the partition |int |false |`-1` +|_pooled-producer_ |_deprecated_ - Deprecated, use 'pooled-producer.enabled' instead |boolean |false |`false` +|_pooled-producer.enabled_ |Whether to use a pool of Kafka producers for concurrent transactions. Each transaction scope acquires a producer from the pool, enabling concurrent exactly-once processing. Requires transactional.id to be set. |boolean |false |`false` +|_pooled-producer.initial-pool-size_ |Number of Kafka producers to pre-create in the pool at startup. Respects the lazy-client setting. Defaults to 0 (lazy creation). |int |false |`0` +|_pooled-producer.max-pool-size_ |Maximum number of Kafka producers in the pool. When all producers are in use and the pool is exhausted, an exception is thrown. Defaults to 10. |int |false |`10` +|_propagate-headers_ |A comma-separating list of incoming record headers to be propagated to the outgoing record |string |false |++``++ +|_propagate-record-key_ |Propagate incoming record key to the outgoing record |boolean |false |`false` +|_retries_ |If set to a positive number, the connector will try to resend any record that was not delivered successfully (with a potentially transient error) until the number of retries is reached. If set to 0, retries are disabled. If not set, the connector tries to resend any record that failed to be delivered (because of a potentially transient error) during an amount of time configured by `delivery.timeout.ms`. |long |false |`2147483647` +|_topic_ |The consumed / populated Kafka topic. If neither this property nor the `topics` properties are set, the channel name is used |string |false | +|_tracing-enabled_ |Whether tracing is enabled (default) or disabled |boolean |false |`true` +|_value-serialization-failure-handler_ |The name set in `@Identifier` of a bean that implements `io.smallrye.reactive.messaging.kafka.SerializationFailureHandler`. If set, serialization failure happening when serializing values are delegated to this handler which may provide a fallback value. |string |false | +|_value.serializer_ |The serializer classname used to serialize the payload |string |true | +|_waitForWriteCompletion_ |Whether the client waits for Kafka to acknowledge the written record before acknowledging the message |boolean |false |`true` +|=== diff --git a/content/docs/smallrye-reactive-messaging/4/_generated/smallrye-mqtt-incoming.adoc b/content/docs/smallrye-reactive-messaging/4/_generated/smallrye-mqtt-incoming.adoc new file mode 100644 index 000000000..162579625 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_generated/smallrye-mqtt-incoming.adoc @@ -0,0 +1,38 @@ +[cols="<,<,^,^,<",options="header",] +|=== +|Attribute (_alias_) |Description |Type |Mandatory |Default +|_auto-clean-session_ |Set to start with a clean session (`true` by default) |boolean |false |`true` +|_auto-generated-client-id_ |Set if the MQTT client must generate clientId automatically |boolean |false |`true` +|_auto-keep-alive_ |Set if the MQTT client must handle `PINGREQ` automatically |boolean |false |`true` +|_broadcast_ |Whether or not the messages should be dispatched to multiple consumers |boolean |false |`false` +|_buffer-size_ |The size buffer of incoming messages waiting to be processed |int |false |`128` +|_client-id_ |Set the client identifier |string |false | +|_client-options-name_ _(mqtt-client-options-name)_ |The name of the MQTT Client Option bean (`io.smallrye.reactive.messaging.mqtt.session.MqttClientSessionOptions`) used to customize the MQTT client configuration |string |false | +|_connect-timeout-seconds_ |Set the connect timeout (in seconds) |int |false |`60` +|_failure-strategy_ |Specify the failure strategy to apply when a message produced from a MQTT message is nacked. Values can be `fail` (default), or `ignore` |string |false |`fail` +|_health-enabled_ |Whether health reporting is enabled (default) or disabled |boolean |false |`true` +|_host_ |Set the MQTT server host name/IP |string |true | +|_keep-alive-seconds_ |Set the keep alive timeout in seconds |int |false |`30` +|_max-inflight-queue_ |Set max count of unacknowledged messages |int |false |`10` +|_max-message-size_ |Set max MQTT message size in bytes |int |false |`8092` +|_password_ |Set the password to connect to the server |string |false | +|_port_ |Set the MQTT server port. Default to 8883 if ssl is enabled, or 1883 without ssl |int |false | +|_qos_ |Set the QoS level when subscribing to the topic or when sending a message |int |false |`0` +|_reconnect-interval-seconds_ |Set the reconnect interval in seconds |int |false |`1` +|_server-name_ |Set the SNI server name |string |false | +|_ssl_ |Set whether SSL/TLS is enabled |boolean |false |`false` +|_ssl.hostname-verification-algorithm_ |Set the hostname verifier algorithm for the TLS connection.Accepted values are `HTTPS`, `LDAPS`, and `NONE` (defaults). `NONE` disables the hostname verification. |string |false |`NONE` +|_ssl.keystore.location_ |Set the keystore location. In case of `pem` type this is the server ca cert path |string |false | +|_ssl.keystore.password_ |Set the keystore password. In case of `pem` type this is the key path |string |false | +|_ssl.keystore.type_ |Set the keystore type ++[++`pkcs12`, `jks`, `pem`++]++ |string |false |`pkcs12` +|_ssl.truststore.location_ |Set the truststore location. In case of `pem` type this is the client cert path |string |false | +|_ssl.truststore.password_ |Set the truststore password. In case of `pem` type this is not necessary |string |false | +|_ssl.truststore.type_ |Set the truststore type ++[++`pkcs12`, `jks`, `pem`++]++ |string |false |`pkcs12` +|_topic_ |Set the MQTT topic. If not set, the channel name is used |string |false | +|_trust-all_ |Set whether all server certificates should be trusted |boolean |false |`false` +|_unsubscribe-on-disconnection_ |This flag restore the old behavior to unsubscribe from the broken on disconnection |boolean |false |`false` +|_username_ |Set the username to connect to the server |string |false | +|_will-flag_ |Set if will information are provided on connection |boolean |false |`false` +|_will-qos_ |Set the QoS level for the will message |int |false |`0` +|_will-retain_ |Set if the will message must be retained |boolean |false |`false` +|=== diff --git a/content/docs/smallrye-reactive-messaging/4/_generated/smallrye-mqtt-outgoing.adoc b/content/docs/smallrye-reactive-messaging/4/_generated/smallrye-mqtt-outgoing.adoc new file mode 100644 index 000000000..b529a82de --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_generated/smallrye-mqtt-outgoing.adoc @@ -0,0 +1,37 @@ +[cols="<,<,^,^,<",options="header",] +|=== +|Attribute (_alias_) |Description |Type |Mandatory |Default +|_auto-clean-session_ |Set to start with a clean session (`true` by default) |boolean |false |`true` +|_auto-generated-client-id_ |Set if the MQTT client must generate clientId automatically |boolean |false |`true` +|_auto-keep-alive_ |Set if the MQTT client must handle `PINGREQ` automatically |boolean |false |`true` +|_client-id_ |Set the client identifier |string |false | +|_client-options-name_ _(mqtt-client-options-name)_ |The name of the MQTT Client Option bean (`io.smallrye.reactive.messaging.mqtt.session.MqttClientSessionOptions`) used to customize the MQTT client configuration |string |false | +|_connect-timeout-seconds_ |Set the connect timeout (in seconds) |int |false |`60` +|_health-enabled_ |Whether health reporting is enabled (default) or disabled |boolean |false |`true` +|_host_ |Set the MQTT server host name/IP |string |true | +|_keep-alive-seconds_ |Set the keep alive timeout in seconds |int |false |`30` +|_max-inflight-queue_ |Set max count of unacknowledged messages |int |false |`10` +|_max-message-size_ |Set max MQTT message size in bytes |int |false |`8092` +|_merge_ |Whether the connector should allow multiple upstreams |boolean |false |`false` +|_password_ |Set the password to connect to the server |string |false | +|_port_ |Set the MQTT server port. Default to 8883 if ssl is enabled, or 1883 without ssl |int |false | +|_qos_ |Set the QoS level when subscribing to the topic or when sending a message |int |false |`0` +|_reconnect-interval-seconds_ |Set the reconnect interval in seconds |int |false |`1` +|_retain_ |Whether the published message should be retained |boolean |false |`false` +|_server-name_ |Set the SNI server name |string |false | +|_ssl_ |Set whether SSL/TLS is enabled |boolean |false |`false` +|_ssl.hostname-verification-algorithm_ |Set the hostname verifier algorithm for the TLS connection.Accepted values are `HTTPS`, `LDAPS`, and `NONE` (defaults). `NONE` disables the hostname verification. |string |false |`NONE` +|_ssl.keystore.location_ |Set the keystore location. In case of `pem` type this is the server ca cert path |string |false | +|_ssl.keystore.password_ |Set the keystore password. In case of `pem` type this is the key path |string |false | +|_ssl.keystore.type_ |Set the keystore type ++[++`pkcs12`, `jks`, `pem`++]++ |string |false |`pkcs12` +|_ssl.truststore.location_ |Set the truststore location. In case of `pem` type this is the client cert path |string |false | +|_ssl.truststore.password_ |Set the truststore password. In case of `pem` type this is not necessary |string |false | +|_ssl.truststore.type_ |Set the truststore type ++[++`pkcs12`, `jks`, `pem`++]++ |string |false |`pkcs12` +|_topic_ |Set the MQTT topic. If not set, the channel name is used |string |false | +|_trust-all_ |Set whether all server certificates should be trusted |boolean |false |`false` +|_unsubscribe-on-disconnection_ |This flag restore the old behavior to unsubscribe from the broken on disconnection |boolean |false |`false` +|_username_ |Set the username to connect to the server |string |false | +|_will-flag_ |Set if will information are provided on connection |boolean |false |`false` +|_will-qos_ |Set the QoS level for the will message |int |false |`0` +|_will-retain_ |Set if the will message must be retained |boolean |false |`false` +|=== diff --git a/content/docs/smallrye-reactive-messaging/4/_generated/smallrye-pulsar-incoming.adoc b/content/docs/smallrye-reactive-messaging/4/_generated/smallrye-pulsar-incoming.adoc new file mode 100644 index 000000000..6d8f6f7e1 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_generated/smallrye-pulsar-incoming.adoc @@ -0,0 +1,21 @@ +[cols="<,<,^,^,<",options="header",] +|=== +|Attribute (_alias_) |Description |Type |Mandatory |Default +|_ack-strategy_ |Specify the commit strategy to apply when a message produced from a record is acknowledged. Values can be `ack`, `cumulative`. |string |false |`ack` +|_ackTimeout.redeliveryBackoff_ |Comma separated values for configuring ack timeout MultiplierRedeliveryBackoff, min delay, max delay, multiplier. |string |false | +|_batchReceive_ |Whether batch receive is used to consume messages |boolean |false |`false` +|_client-configuration_ |Identifier of a CDI bean that provides the default Pulsar client configuration for this channel. The channel configuration can still override any attribute. The bean must have a type of Map++<++String, Object++>++ and must use the @io.smallrye.common.annotation.Identifier qualifier to set the identifier. |string |false | +|_consumer-configuration_ |Identifier of a CDI bean that provides the default Pulsar consumer configuration for this channel. The channel configuration can still override any attribute. The bean must have a type of Map++<++String, Object++>++ and must use the @io.smallrye.common.annotation.Identifier qualifier to set the identifier. |string |false | +|_deadLetterPolicy.deadLetterTopic_ |Name of the dead letter topic where the failing messages will be sent |string |false | +|_deadLetterPolicy.initialSubscriptionName_ |Name of the initial subscription name of the dead letter topic |string |false | +|_deadLetterPolicy.maxRedeliverCount_ |Maximum number of times that a message will be redelivered before being sent to the dead letter topic |int |false | +|_deadLetterPolicy.retryLetterTopic_ |Name of the retry topic where the failing messages will be sent |string |false | +|_failure-strategy_ |Specify the failure strategy to apply when a message produced from a record is acknowledged negatively (nack). Values can be `nack` (default), `fail`, `ignore` or ++`++reconsume-later |string |false |`nack` +|_health-enabled_ |Whether health reporting is enabled (default) or disabled |boolean |false |`true` +|_negativeAck.redeliveryBackoff_ |Comma separated values for configuring negative ack MultiplierRedeliveryBackoff, min delay, max delay, multiplier. |string |false | +|_reconsumeLater.delay_ |Default delay for reconsume failure-strategy, in seconds |long |false |`3` +|_schema_ |The Pulsar schema type of this channel. When configured a schema is built with the given SchemaType and used for the channel. When absent, the schema is resolved searching for a CDI bean typed `Schema` qualified with `@Identifier` and the channel name. As a fallback AUTO++_++CONSUME or AUTO++_++PRODUCE are used. |string |false | +|_serviceUrl_ |The service URL for the Pulsar service |string |false |`pulsar://localhost:6650` +|_topic_ |The consumed / populated Pulsar topic. If not set, the channel name is used |string |false | +|_tracing-enabled_ |Whether tracing is enabled (default) or disabled |boolean |false |`true` +|=== diff --git a/content/docs/smallrye-reactive-messaging/4/_generated/smallrye-pulsar-outgoing.adoc b/content/docs/smallrye-reactive-messaging/4/_generated/smallrye-pulsar-outgoing.adoc new file mode 100644 index 000000000..9d44f7b1d --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_generated/smallrye-pulsar-outgoing.adoc @@ -0,0 +1,13 @@ +[cols="<,<,^,^,<",options="header",] +|=== +|Attribute (_alias_) |Description |Type |Mandatory |Default +|_client-configuration_ |Identifier of a CDI bean that provides the default Pulsar client configuration for this channel. The channel configuration can still override any attribute. The bean must have a type of Map++<++String, Object++>++ and must use the @io.smallrye.common.annotation.Identifier qualifier to set the identifier. |string |false | +|_health-enabled_ |Whether health reporting is enabled (default) or disabled |boolean |false |`true` +|_max-inflight-messages_ |The maximum size of a queue holding pending messages, i.e messages waiting to receive an acknowledgment from a broker. Defaults to 1000 messages |int |false | +|_producer-configuration_ |Identifier of a CDI bean that provides the default Pulsar producer configuration for this channel. The channel configuration can still override any attribute. The bean must have a type of Map++<++String, Object++>++ and must use the @io.smallrye.common.annotation.Identifier qualifier to set the identifier. |string |false | +|_schema_ |The Pulsar schema type of this channel. When configured a schema is built with the given SchemaType and used for the channel. When absent, the schema is resolved searching for a CDI bean typed `Schema` qualified with `@Identifier` and the channel name. As a fallback AUTO++_++CONSUME or AUTO++_++PRODUCE are used. |string |false | +|_serviceUrl_ |The service URL for the Pulsar service |string |false |`pulsar://localhost:6650` +|_topic_ |The consumed / populated Pulsar topic. If not set, the channel name is used |string |false | +|_tracing-enabled_ |Whether tracing is enabled (default) or disabled |boolean |false |`true` +|_waitForWriteCompletion_ |Whether the client waits for the broker to acknowledge the written record before acknowledging the message |boolean |false |`true` +|=== diff --git a/content/docs/smallrye-reactive-messaging/4/_generated/smallrye-rabbitmq-incoming.adoc b/content/docs/smallrye-reactive-messaging/4/_generated/smallrye-rabbitmq-incoming.adoc new file mode 100644 index 000000000..2c3d1016e --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_generated/smallrye-rabbitmq-incoming.adoc @@ -0,0 +1,79 @@ +[cols="<,<,^,^,<",options="header",] +|=== +|Attribute (_alias_) |Description |Type |Mandatory |Default +|_addresses_ _(rabbitmq-addresses)_ |The multiple addresses for cluster mode, when given overrides the host and port |string |false | +|_arguments_ |A comma-separated list of arguments ++[++key1:value1,key2:value2,...++]++ to bind the queue to the exchange. Relevant only if 'exchange.type' is headers |string |false | +|_auto-acknowledgement_ |Whether the received RabbitMQ messages must be acknowledged when received; if true then delivery constitutes acknowledgement |boolean |false |`false` +|_auto-bind-dlq_ |Whether to automatically declare the DLQ and bind it to the binder DLX |boolean |false |`false` +|_automatic-recovery-enabled_ |Whether automatic connection recovery is enabled |boolean |false |`false` +|_automatic-recovery-on-initial-connection_ |Whether automatic recovery on initial connections is enabled |boolean |false |`true` +|_broadcast_ |Whether the received RabbitMQ messages must be dispatched to multiple _subscribers_ |boolean |false |`false` +|_client-options-name_ _(rabbitmq-client-options-name)_ |The name of the RabbitMQ Client Option bean used to customize the RabbitMQ client configuration |string |false | +|_connection-timeout_ |The TCP connection timeout (ms); 0 is interpreted as no timeout |int |false |`60000` +|_consumer-arguments_ |A comma-separated list of arguments ++[++key1:value1,key2:value2,...++]++ for created consumer |string |false | +|_consumer-exclusive_ |The exclusive flag for created consumer |boolean |false | +|_consumer-tag_ |The consumer-tag option for created consumer, if not provided the consumer gets assigned a tag generated by the broker |string |false | +|_content-type-override_ |Override the content++_++type attribute of the incoming message, should be a valid MINE type |string |false | +|_credentials-provider-name_ _(rabbitmq-credentials-provider-name)_ |The name of the RabbitMQ Credentials Provider bean used to provide dynamic credentials to the RabbitMQ client |string |false | +|_dead-letter-dlx_ |If specified, a DLX to assign to the DLQ. Relevant only if auto-bind-dlq is true |string |false | +|_dead-letter-dlx-routing-key_ |If specified, a dead letter routing key to assign to the DLQ. Relevant only if auto-bind-dlq is true |string |false | +|_dead-letter-exchange_ |A DLX to assign to the queue. Relevant only if auto-bind-dlq is true |string |false |`DLX` +|_dead-letter-exchange-type_ |The type of the DLX to assign to the queue. Relevant only if auto-bind-dlq is true |string |false |`direct` +|_dead-letter-exchange.arguments_ |The identifier of the key-value Map exposed as bean used to provide arguments for dead-letter-exchange creation |string |false | +|_dead-letter-queue-mode_ |If automatically declare DLQ, we can choose different modes of DLQ ++[++lazy, default++]++ |string |false | +|_dead-letter-queue-name_ |The name of the DLQ; if not supplied will default to the queue name with '.dlq' appended |string |false | +|_dead-letter-queue-type_ |If automatically declare DLQ, we can choose different types of DLQ ++[++quorum, classic, stream++]++ |string |false | +|_dead-letter-queue.arguments_ |The identifier of the key-value Map exposed as bean used to provide arguments for dead-letter-queue creation |string |false | +|_dead-letter-routing-key_ |A dead letter routing key to assign to the queue; if not supplied will default to the queue name |string |false | +|_dead-letter-ttl_ |If specified, the time (ms) for which a message can remain in DLQ undelivered before it is dead. Relevant only if auto-bind-dlq is true |long |false | +|_dlx.declare_ |Whether to declare the dead letter exchange binding. Relevant only if auto-bind-dlq is true; set to false if these are expected to be set up independently |boolean |false |`false` +|_exchange.arguments_ |The identifier of the key-value Map exposed as bean used to provide arguments for exchange creation |string |false |`rabbitmq-exchange-arguments` +|_exchange.auto-delete_ |Whether the exchange should be deleted after use |boolean |false |`false` +|_exchange.declare_ |Whether to declare the exchange; set to false if the exchange is expected to be set up independently |boolean |false |`true` +|_exchange.durable_ |Whether the exchange is durable |boolean |false |`true` +|_exchange.name_ |The exchange that messages are published to or consumed from. If not set, the channel name is used. If set to "", the default exchange is used. |string |false | +|_exchange.type_ |The exchange type: direct, fanout, headers or topic (default) |string |false |`topic` +|_failure-strategy_ |The failure strategy to apply when a RabbitMQ message is nacked. Accepted values are `fail`, `accept`, `reject` (default), `requeue` or name of a bean |string |false |`reject` +|_handshake-timeout_ |The AMQP 0-9-1 protocol handshake timeout (ms) |int |false |`10000` +|_health-enabled_ |Whether health reporting is enabled (default) or disabled |boolean |false |`true` +|_health-lazy-subscription_ |Whether the liveness and readiness checks should report 'ok' when there is no subscription yet. This is useful when injecting the channel with `@Inject @Channel("...") Multi++<++...++>++ multi;` |boolean |false |`false` +|_health-readiness-enabled_ |Whether readiness health reporting is enabled (default) or disabled |boolean |false |`true` +|_host_ _(rabbitmq-host)_ |The broker hostname |string |false |`localhost` +|_include-properties_ |Whether to include properties when a broker message is passed on the event bus |boolean |false |`false` +|_keep-most-recent_ |Whether to discard old messages instead of recent ones |boolean |false |`false` +|_lazy-client_ |Whether the RabbitMQ client is created lazily or eagerly. |boolean |false |`true` +|_max-incoming-internal-queue-size_ |The maximum size of the incoming internal queue |int |false |`500000` +|_max-outstanding-messages_ |The maximum number of outstanding/unacknowledged messages being processed by the connector at a time; must be a positive number |int |false | +|_network-recovery-interval_ |How long (ms) will automatic recovery wait before attempting to reconnect |int |false |`5000` +|_password_ _(rabbitmq-password)_ |The password used to authenticate to the broker |string |false | +|_port_ _(rabbitmq-port)_ |The broker port |int |false |`5672` +|_queue.arguments_ |The identifier of the key-value Map exposed as bean used to provide arguments for queue creation |string |false |`rabbitmq-queue-arguments` +|_queue.auto-delete_ |Whether the queue should be deleted after use |boolean |false |`false` +|_queue.declare_ |Whether to declare the queue and binding; set to false if these are expected to be set up independently |boolean |false |`true` +|_queue.durable_ |Whether the queue is durable |boolean |false |`true` +|_queue.exclusive_ |Whether the queue is for exclusive use |boolean |false |`false` +|_queue.name_ |The queue from which messages are consumed. If not set, the channel name is used. |string |false | +|_queue.single-active-consumer_ |If set to true, only one consumer can actively consume messages |boolean |false | +|_queue.ttl_ |If specified, the time (ms) for which a message can remain in the queue undelivered before it is dead |long |false | +|_queue.x-delivery-limit_ |If queue.x-queue-type is quorum, when a message has been returned more times than the limit the message will be dropped or dead-lettered |long |false | +|_queue.x-max-priority_ |Define priority level queue consumer |int |false | +|_queue.x-queue-mode_ |If automatically declare queue, we can choose different modes of queue ++[++lazy, default++]++ |string |false | +|_queue.x-queue-type_ |If automatically declare queue, we can choose different types of queue ++[++quorum, classic, stream++]++ |string |false | +|_reconnect-attempts_ _(rabbitmq-reconnect-attempts)_ |The number of reconnection attempts |int |false |`100` +|_reconnect-interval_ _(rabbitmq-reconnect-interval)_ |The interval (in seconds) between two reconnection attempts |int |false |`10` +|_requested-channel-max_ |The initially requested maximum channel number |int |false |`2047` +|_requested-heartbeat_ |The initially requested heartbeat interval (seconds), zero for none |int |false |`60` +|_routing-keys_ |A comma-separated list of routing keys to bind the queue to the exchange. Relevant only if 'exchange.type' is topic or direct |string |false | +|_shared-connection-name_ |Optional identifier allowing multiple channels to share the same RabbitMQ connection when set to the same value |string |false | +|_ssl_ _(rabbitmq-ssl)_ |Whether or not the connection should use SSL |boolean |false |`false` +|_ssl.hostname-verification-algorithm_ |Set the hostname verifier algorithm for the TLS connection. Accepted values are `HTTPS`, and `NONE` (defaults). `NONE` disables the hostname verification. |string |false |`NONE` +|_tracing.attribute-headers_ |A comma-separated list of headers that should be recorded as span attributes. Relevant only if tracing.enabled=true |string |false |++``++ +|_tracing.enabled_ |Whether tracing is enabled (default) or disabled |boolean |false |`true` +|_trust-all_ _(rabbitmq-trust-all)_ |Whether to skip trust certificate verification |boolean |false |`false` +|_trust-store-password_ _(rabbitmq-trust-store-password)_ |The password of the JKS trust store |string |false | +|_trust-store-path_ _(rabbitmq-trust-store-path)_ |The path to a JKS trust store |string |false | +|_use-nio_ |Whether usage of NIO Sockets is enabled |boolean |false |`false` +|_user_ |The user name to use when connecting to the broker |string |false |`guest` +|_username_ _(rabbitmq-username)_ |The username used to authenticate to the broker |string |false | +|_virtual-host_ _(rabbitmq-virtual-host)_ |The virtual host to use when connecting to the broker |string |false |`/` +|=== diff --git a/content/docs/smallrye-reactive-messaging/4/_generated/smallrye-rabbitmq-outgoing.adoc b/content/docs/smallrye-reactive-messaging/4/_generated/smallrye-rabbitmq-outgoing.adoc new file mode 100644 index 000000000..95c6dcaae --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_generated/smallrye-rabbitmq-outgoing.adoc @@ -0,0 +1,48 @@ +[cols="<,<,^,^,<",options="header",] +|=== +|Attribute (_alias_) |Description |Type |Mandatory |Default +|_addresses_ _(rabbitmq-addresses)_ |The multiple addresses for cluster mode, when given overrides the host and port |string |false | +|_automatic-recovery-enabled_ |Whether automatic connection recovery is enabled |boolean |false |`false` +|_automatic-recovery-on-initial-connection_ |Whether automatic recovery on initial connections is enabled |boolean |false |`true` +|_client-options-name_ _(rabbitmq-client-options-name)_ |The name of the RabbitMQ Client Option bean used to customize the RabbitMQ client configuration |string |false | +|_connection-timeout_ |The TCP connection timeout (ms); 0 is interpreted as no timeout |int |false |`60000` +|_credentials-provider-name_ _(rabbitmq-credentials-provider-name)_ |The name of the RabbitMQ Credentials Provider bean used to provide dynamic credentials to the RabbitMQ client |string |false | +|_default-routing-key_ |The default routing key to use when sending messages to the exchange |string |false |++``++ +|_default-ttl_ |If specified, the time (ms) sent messages can remain in queues undelivered before they are dead |long |false | +|_exchange.arguments_ |The identifier of the key-value Map exposed as bean used to provide arguments for exchange creation |string |false |`rabbitmq-exchange-arguments` +|_exchange.auto-delete_ |Whether the exchange should be deleted after use |boolean |false |`false` +|_exchange.declare_ |Whether to declare the exchange; set to false if the exchange is expected to be set up independently |boolean |false |`true` +|_exchange.durable_ |Whether the exchange is durable |boolean |false |`true` +|_exchange.name_ |The exchange that messages are published to or consumed from. If not set, the channel name is used. If set to "", the default exchange is used. |string |false | +|_exchange.type_ |The exchange type: direct, fanout, headers or topic (default) |string |false |`topic` +|_handshake-timeout_ |The AMQP 0-9-1 protocol handshake timeout (ms) |int |false |`10000` +|_health-enabled_ |Whether health reporting is enabled (default) or disabled |boolean |false |`true` +|_health-readiness-enabled_ |Whether readiness health reporting is enabled (default) or disabled |boolean |false |`true` +|_host_ _(rabbitmq-host)_ |The broker hostname |string |false |`localhost` +|_include-properties_ |Whether to include properties when a broker message is passed on the event bus |boolean |false |`false` +|_lazy-client_ |Whether the RabbitMQ client is created lazily or eagerly. |boolean |false |`true` +|_max-inflight-messages_ |The maximum number of messages to be written to RabbitMQ concurrently; must be a positive number |long |false |`1024` +|_max-outgoing-internal-queue-size_ |The maximum size of the outgoing internal queue |int |false | +|_network-recovery-interval_ |How long (ms) will automatic recovery wait before attempting to reconnect |int |false |`5000` +|_password_ _(rabbitmq-password)_ |The password used to authenticate to the broker |string |false | +|_port_ _(rabbitmq-port)_ |The broker port |int |false |`5672` +|_publish-confirms_ |If set to true, published messages are acknowledged when the publish confirm is received from the broker |boolean |false |`false` +|_reconnect-attempts_ _(rabbitmq-reconnect-attempts)_ |The number of reconnection attempts |int |false |`100` +|_reconnect-interval_ _(rabbitmq-reconnect-interval)_ |The interval (in seconds) between two reconnection attempts |int |false |`10` +|_requested-channel-max_ |The initially requested maximum channel number |int |false |`2047` +|_requested-heartbeat_ |The initially requested heartbeat interval (seconds), zero for none |int |false |`60` +|_retry-on-fail-attempts_ |The number of tentative to retry on failure |int |false |`6` +|_retry-on-fail-interval_ |The interval (in seconds) between two sending attempts |int |false |`5` +|_shared-connection-name_ |Optional identifier allowing multiple channels to share the same RabbitMQ connection when set to the same value |string |false | +|_ssl_ _(rabbitmq-ssl)_ |Whether or not the connection should use SSL |boolean |false |`false` +|_ssl.hostname-verification-algorithm_ |Set the hostname verifier algorithm for the TLS connection. Accepted values are `HTTPS`, and `NONE` (defaults). `NONE` disables the hostname verification. |string |false |`NONE` +|_tracing.attribute-headers_ |A comma-separated list of headers that should be recorded as span attributes. Relevant only if tracing.enabled=true |string |false |++``++ +|_tracing.enabled_ |Whether tracing is enabled (default) or disabled |boolean |false |`true` +|_trust-all_ _(rabbitmq-trust-all)_ |Whether to skip trust certificate verification |boolean |false |`false` +|_trust-store-password_ _(rabbitmq-trust-store-password)_ |The password of the JKS trust store |string |false | +|_trust-store-path_ _(rabbitmq-trust-store-path)_ |The path to a JKS trust store |string |false | +|_use-nio_ |Whether usage of NIO Sockets is enabled |boolean |false |`false` +|_user_ |The user name to use when connecting to the broker |string |false |`guest` +|_username_ _(rabbitmq-username)_ |The username used to authenticate to the broker |string |false | +|_virtual-host_ _(rabbitmq-virtual-host)_ |The virtual host to use when connecting to the broker |string |false |`/` +|=== diff --git a/content/docs/smallrye-reactive-messaging/4/_generated/smallrye-sns-outgoing.adoc b/content/docs/smallrye-reactive-messaging/4/_generated/smallrye-sns-outgoing.adoc new file mode 100644 index 000000000..c25ed7a4f --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_generated/smallrye-sns-outgoing.adoc @@ -0,0 +1,17 @@ +[cols="<,<,^,^,<",options="header",] +|=== +|Attribute (_alias_) |Description |Type |Mandatory |Default +|_batch_ |When set, sends messages in batches of maximum 10 messages |boolean |false |`false` +|_batch-delay_ |In batch send mode, the maximum delay in milliseconds to wait for messages to be included in the batch. Defaults to 3000 ms |int |false |`3000` +|_batch-size_ |In batch send mode, the maximum number of messages to include in batch, currently SNS maximum is 10 messages |int |false |`10` +|_credentials-provider_ |The credential provider to be used in the client, defaults to AWS default provider chain |string |false | +|_email.subject_ |When set, sends messages with the specified subject |string |false | +|_endpoint-override_ |The endpoint override |string |false | +|_group.id_ |When set, sends messages with the specified group id |string |false | +|_health-enabled_ |Whether health reporting is enabled (default) or disabled |boolean |false |`true` +|_message-structure_ |Set to json if you want to send a different message for each protocol. |string |false | +|_region_ |The name of the SNS region, defaults to AWS region resolver |string |false | +|_sms.phoneNumber_ |When set, sends messages to the specified phone number. Not supported in combination with batching. |string |false | +|_topic_ |The name of the SNS topic, defaults to channel name if not provided |string |false | +|_topic.arn_ |The arn of the SNS topic |string |false | +|=== diff --git a/content/docs/smallrye-reactive-messaging/4/_generated/smallrye-sqs-incoming.adoc b/content/docs/smallrye-reactive-messaging/4/_generated/smallrye-sqs-incoming.adoc new file mode 100644 index 000000000..b461eea07 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_generated/smallrye-sqs-incoming.adoc @@ -0,0 +1,21 @@ +[cols="<,<,^,^,<",options="header",] +|=== +|Attribute (_alias_) |Description |Type |Mandatory |Default +|_ack-strategy_ |The identifier for the bean implementing ack strategy factory. Strategies: 'delete', 'ignore' |string |false |`delete` +|_ack.delete_ |_deprecated_ - Whether the acknowledgement deletes the message from the queue. Deprecated, use ack-strategy instead |boolean |false | +|_credentials-provider_ |The credential provider to be used in the client |string |false | +|_endpoint-override_ |The endpoint override |string |false | +|_failure-strategy_ |The identifier for the bean implementing failure strategy factory. Strategies: 'ignore', 'fail', 'visibility', 'delete' |string |false |`ignore` +|_health-enabled_ |Whether health reporting is enabled (default) or disabled |boolean |false |`true` +|_max-number-of-messages_ |The maximum number of messages to receive |int |false |`10` +|_queue_ |The name of the SQS queue, defaults to channel name if not provided |string |false | +|_queue.url_ |The url of the SQS queue |string |false | +|_receive.request.customizer_ |The identifier for the bean implementing a customizer to receive requests, defaults to channel name if not provided |string |false | +|_receive.request.message-attribute-names_ |The message attribute names to retrieve when receiving messages. |string |false | +|_receive.request.pause.resume_ |Whether the polling must be paused when the application does not request items and resume when it does. This allows implementing back-pressure based on the application capacity. Note that polling is not stopped, but will not retrieve any records when paused. |boolean |false |`true` +|_receive.request.retries_ |If set to a positive number, the connector will try to retry the request that was not delivered successfully (with a potentially transient error) until the number of retries is reached. If set to 0, retries are disabled. |long |false |`2147483647` +|_region_ |The name of the SQS region |string |false | +|_tracing-enabled_ |Whether tracing is enabled (default) or disabled |boolean |false |`true` +|_visibility-timeout_ |The duration in seconds that the received messages are hidden from subsequent retrieve requests after being retrieved by a receive request |int |false | +|_wait-time-seconds_ |The maximum amount of time in seconds to wait for messages to be received |int |false |`1` +|=== diff --git a/content/docs/smallrye-reactive-messaging/4/_generated/smallrye-sqs-outgoing.adoc b/content/docs/smallrye-reactive-messaging/4/_generated/smallrye-sqs-outgoing.adoc new file mode 100644 index 000000000..28e23dc2c --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_generated/smallrye-sqs-outgoing.adoc @@ -0,0 +1,15 @@ +[cols="<,<,^,^,<",options="header",] +|=== +|Attribute (_alias_) |Description |Type |Mandatory |Default +|_batch_ |When set, sends messages in batches of maximum 10 messages |boolean |false |`false` +|_batch-delay_ |In batch send mode, the maximum delay in milliseconds to wait for messages to be included in the batch |int |false |`3000` +|_batch-size_ |In batch send mode, the maximum number of messages to include in batch, currently SQS maximum is 10 messages |int |false |`10` +|_credentials-provider_ |The credential provider to be used in the client |string |false | +|_endpoint-override_ |The endpoint override |string |false | +|_group.id_ |When set, sends messages with the specified group id |string |false | +|_health-enabled_ |Whether health reporting is enabled (default) or disabled |boolean |false |`true` +|_queue_ |The name of the SQS queue, defaults to channel name if not provided |string |false | +|_queue.url_ |The url of the SQS queue |string |false | +|_region_ |The name of the SQS region |string |false | +|_tracing-enabled_ |Whether tracing is enabled (default) or disabled |boolean |false |`true` +|=== diff --git a/content/docs/smallrye-reactive-messaging/4/_nav.yml b/content/docs/smallrye-reactive-messaging/4/_nav.yml new file mode 100644 index 000000000..e30c6022e --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_nav.yml @@ -0,0 +1,177 @@ +title: SmallRye Reactive Messaging +nav: +- title: Home + page: index +- title: Core + items: + - title: Getting Started + page: getting-started + - title: Concepts + items: + - title: Introduction + page: concepts/concepts + - title: Development Model + page: concepts/model + - title: Emitters and Channel + page: concepts/emitter + - title: Connectors + page: concepts/connectors + - title: Contributing Connectors + page: concepts/contributing-connectors + - title: Acknowledgement + page: concepts/acknowledgement + - title: Blocking Processing + page: concepts/blocking + - title: Method Signatures + page: concepts/signatures + - title: Skipping Messages + page: concepts/skipping + - title: Message Converters + page: concepts/converters + - title: Keyed Streams + page: concepts/keyed-multi + - title: Channel Decorators and Interceptors + page: concepts/decorators + - title: Broadcast + page: concepts/broadcast + - title: Merge channels + page: concepts/merge + - title: Incoming Channel Concurrency + page: concepts/incoming-concurrency + - title: '@Incomings' + page: concepts/incomings + - title: '@Outgoings' + page: concepts/outgoings + - title: Testing + page: concepts/testing + - title: Logging + page: concepts/logging + - title: Observability API + page: concepts/observability + - title: Advanced Configuration + page: concepts/advanced-config + - title: Message Context + page: concepts/message-context + - title: Metadata Injection + page: concepts/incoming-metadata-injection + - title: Generic Payloads + page: concepts/generic-payloads + - title: Pausable Channels + page: concepts/pausable-channels +- title: Kafka + items: + - page: kafka/kafka + - title: Receiving records + page: kafka/receiving-kafka-records + - title: Writing records + page: kafka/writing-kafka-records + - title: Health Checks + page: kafka/health + - title: Using Avro + page: kafka/avro-configuration + - title: Using Protobuf + page: kafka/protobuf-configuration + - title: Rebalance Listeners + page: kafka/consumer-rebalance-listener + - title: Kerberos authentication + page: kafka/kerberos + - title: Accessing the client + page: kafka/client-service + - title: Customizing Default Kafka Configuration + page: kafka/default-configuration + - title: Test Companion for Kafka + page: kafka/test-companion + - title: Kafka Transactions and Exactly-Once Processing + page: kafka/transactions + - title: Kafka Request/Reply + page: kafka/request-reply +- title: AMQP 1.0 + items: + - page: amqp/amqp + - title: Receiving messages + page: amqp/receiving-amqp-messages + - title: Sending messages + page: amqp/sending-amqp-messages + - title: AMQP Request/Reply + page: amqp/request-reply + - title: CBS Authentication + page: amqp/cbs-authentication + - title: Health Checks + page: amqp/health + - title: Client Customization + page: amqp/client-customization + - title: Using RabbitMQ + page: amqp/rabbitmq +- title: RabbitMQ + items: + - page: rabbitmq/rabbitmq + - title: Receiving messages + page: rabbitmq/receiving-messages-from-rabbitmq + - title: Sending messages + page: rabbitmq/sending-messages-to-rabbitmq + - title: Health Checks + page: rabbitmq/rabbitmq-health + - title: Client Customization + page: rabbitmq/rabbitmq-client-customization + - title: Connecting to managed instances + page: rabbitmq/rabbitmq-cloud + - title: RabbitMQ Request/Reply + page: rabbitmq/request-reply +- title: Pulsar + items: + - page: pulsar/pulsar + - title: Receiving messages + page: pulsar/receiving-pulsar-messages + - title: Sending messages + page: pulsar/sending-messages-to-pulsar + - title: Configuring the Schema + page: pulsar/schema-configuration + - title: Configuring the Pulsar client + page: pulsar/client-configuration + - title: Health Checks + page: pulsar/health + - title: Accessing the client + page: pulsar/client-service + - title: Pulsar Transactions and Exactly-Once Processing + page: pulsar/transactions +- title: Apache Camel + items: + - page: camel/camel + - title: Receiving messages from Camel + page: camel/receiving-messages-from-camel + - title: Sending messages to Camel + page: camel/sending-messages-to-camel + - title: Implementing Camel processor + page: camel/camel-processor + - title: Using existing Camel routes + page: camel/using-existing-routes +- title: JMS + items: + - page: jms/jms + - title: Receiving JMS messages + page: jms/receiving-jms-messages + - title: Sending JMS messages + page: jms/sending-jms-messages + - title: Advanced configuration + page: jms/advanced-jms +- title: MQTT + items: + - page: mqtt/mqtt + - title: Receiving MQTT messages + page: mqtt/receiving-mqtt-messages + - title: Sending MQTT messages + page: mqtt/sending-messages-to-mqtt + - title: Customizing the MQTT client + page: mqtt/client-customization +- title: AWS SQS + items: + - page: sqs/sqs + - title: Receiving AWS SQS messages + page: sqs/receiving-aws-sqs-messages + - title: Sending AWS SQS messages + page: sqs/sending-aws-sqs-messages +- title: AWS SNS + items: + - page: sns/sns + - title: Sending AWS SNS messages + page: sns/sending-aws-sns-messages diff --git a/content/docs/smallrye-reactive-messaging/4/_partials/pulsar/config/smallrye-pulsar-client.adoc b/content/docs/smallrye-reactive-messaging/4/_partials/pulsar/config/smallrye-pulsar-client.adoc new file mode 100644 index 000000000..513f4c895 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_partials/pulsar/config/smallrye-pulsar-client.adoc @@ -0,0 +1,58 @@ +[width="100%",cols="<13%,<42%,^17%,^17%,<11%",options="header",] +|=== +|Attribute |Description |Type |Config file |Default +|_serviceUrl_ |Pulsar cluster HTTP URL to connect to a broker. |String |true | +|_serviceUrlProvider_ |The implementation class of ServiceUrlProvider used to generate ServiceUrl. |ServiceUrlProvider |false | +|_authentication_ |Authentication settings of the client. |Authentication |false | +|_authPluginClassName_ |Class name of authentication plugin of the client. |String |true | +|_authParams_ |Authentication parameter of the client. |String |true | +|_authParamMap_ |Authentication map of the client. |Map |true | +|_operationTimeoutMs_ |Client operation timeout (in milliseconds). |long |true |30000 +|_lookupTimeoutMs_ |Client lookup timeout (in milliseconds). |long |true |-1 +|_statsIntervalSeconds_ |Interval to print client stats (in seconds). |long |true |60 +|_numIoThreads_ |Number of IO threads. |int |true |10 +|_numListenerThreads_ |Number of consumer listener threads. |int |true |10 +|_connectionsPerBroker_ |Number of connections established between the client and each Broker. A value of 0 means to disable connection pooling. |int |true |1 +|_connectionMaxIdleSeconds_ |Release the connection if it is not used for more than ++[++connectionMaxIdleSeconds++]++ seconds. If ++[++connectionMaxIdleSeconds++]++ ++<++ 0, disabled the feature that auto release the idle connections |int |true |180 +|_useTcpNoDelay_ |Whether to use TCP NoDelay option. |boolean |true |true +|_useTls_ |Whether to use TLS. |boolean |true |false +|_tlsKeyFilePath_ |Path to the TLS key file. |String |true | +|_tlsCertificateFilePath_ |Path to the TLS certificate file. |String |true | +|_tlsTrustCertsFilePath_ |Path to the trusted TLS certificate file. |String |true | +|_tlsAllowInsecureConnection_ |Whether the client accepts untrusted TLS certificates from the broker. |boolean |true |false +|_tlsHostnameVerificationEnable_ |Whether the hostname is validated when the client creates a TLS connection with brokers. |boolean |true |false +|_concurrentLookupRequest_ |The number of concurrent lookup requests that can be sent on each broker connection. Setting a maximum prevents overloading a broker. |int |true |5000 +|_maxLookupRequest_ |Maximum number of lookup requests allowed on each broker connection to prevent overloading a broker. |int |true |50000 +|_maxLookupRedirects_ |Maximum times of redirected lookup requests. |int |true |20 +|_maxNumberOfRejectedRequestPerConnection_ |Maximum number of rejected requests of a broker in a certain time frame (60 seconds) after the current connection is closed and the client creating a new connection to connect to a different broker. |int |true |50 +|_keepAliveIntervalSeconds_ |Seconds of keeping alive interval for each client broker connection. |int |true |30 +|_connectionTimeoutMs_ |Duration of waiting for a connection to a broker to be established.If the duration passes without a response from a broker, the connection attempt is dropped. |int |true |10000 +|_requestTimeoutMs_ |Maximum duration for completing a request. |int |true |60000 +|_readTimeoutMs_ |Maximum read time of a request. |int |true |60000 +|_autoCertRefreshSeconds_ |Seconds of auto refreshing certificate. |int |true |300 +|_initialBackoffIntervalNanos_ |Initial backoff interval (in nanosecond). |long |true |100000000 +|_maxBackoffIntervalNanos_ |Max backoff interval (in nanosecond). |long |true |60000000000 +|_enableBusyWait_ |Whether to enable BusyWait for EpollEventLoopGroup. |boolean |true |false +|_listenerName_ |Listener name for lookup. Clients can use listenerName to choose one of the listeners as the service URL to create a connection to the broker as long as the network is accessible."advertisedListeners" must enabled in broker side. |String |true | +|_useKeyStoreTls_ |Set TLS using KeyStore way. |boolean |true |false +|_sslProvider_ |The TLS provider used by an internal client to authenticate with other Pulsar brokers. |String |true | +|_tlsKeyStoreType_ |TLS KeyStore type configuration. |String |true |JKS +|_tlsKeyStorePath_ |Path of TLS KeyStore. |String |true | +|_tlsKeyStorePassword_ |Password of TLS KeyStore. |String |true | +|_tlsTrustStoreType_ |TLS TrustStore type configuration. You need to set this configuration when client authentication is required. |String |true |JKS +|_tlsTrustStorePath_ |Path of TLS TrustStore. |String |true | +|_tlsTrustStorePassword_ |Password of TLS TrustStore. |String |true | +|_tlsCiphers_ |Set of TLS Ciphers. |Set |true |++[]++ +|_tlsProtocols_ |Protocols of TLS. |Set |true |++[]++ +|_memoryLimitBytes_ |Limit of client memory usage (in byte). The 64M default can guarantee a high producer throughput. |long |true |67108864 +|_proxyServiceUrl_ |URL of proxy service. proxyServiceUrl and proxyProtocol must be mutually inclusive. |String |true | +|_proxyProtocol_ |Protocol of proxy service. proxyServiceUrl and proxyProtocol must be mutually inclusive. |ProxyProtocol |true | +|_enableTransaction_ |Whether to enable transaction. |boolean |true |false +|_clock_ | |Clock |false | +|_dnsLookupBindAddress_ |The Pulsar client dns lookup bind address, default behavior is bind on 0.0.0.0 |String |true | +|_dnsLookupBindPort_ |The Pulsar client dns lookup bind port, takes effect when dnsLookupBindAddress is configured, default value is 0. |int |true |0 +|_socks5ProxyAddress_ |Address of SOCKS5 proxy. |InetSocketAddress |true | +|_socks5ProxyUsername_ |User name of SOCKS5 proxy. |String |true | +|_socks5ProxyPassword_ |Password of SOCKS5 proxy. |String |true | +|_description_ |The extra description of the client version. The length cannot exceed 64. |String |true | +|=== diff --git a/content/docs/smallrye-reactive-messaging/4/_partials/pulsar/config/smallrye-pulsar-consumer.adoc b/content/docs/smallrye-reactive-messaging/4/_partials/pulsar/config/smallrye-pulsar-consumer.adoc new file mode 100644 index 000000000..1f4b499aa --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_partials/pulsar/config/smallrye-pulsar-consumer.adoc @@ -0,0 +1,137 @@ +[width="100%",cols="<13%,<42%,^17%,^17%,<11%",options="header",] +|=== +|Attribute |Description |Type |Config file |Default +|_topicNames_ |Topic name |Set |true |++[]++ +|_topicsPattern_ |Topic pattern |Pattern |true | +|_subscriptionName_ |Subscription name |String |true | +|_subscriptionType_ |Subscription type. + +Four subscription types are available: + +++*++ Exclusive + +++*++ Failover + +++*++ Shared + +++*++ Key++_++Shared |SubscriptionType |true |Exclusive +|_subscriptionProperties_ | |Map |true | +|_subscriptionMode_ | |SubscriptionMode |true |Durable +|_messageListener_ | |MessageListener |false | +|_consumerEventListener_ | |ConsumerEventListener |false | +|_negativeAckRedeliveryBackoff_ |Interface for custom message is negativeAcked policy. You can specify `RedeliveryBackoff` for a consumer. |RedeliveryBackoff |false | +|_ackTimeoutRedeliveryBackoff_ |Interface for custom message is ackTimeout policy. You can specify `RedeliveryBackoff` for a consumer. |RedeliveryBackoff |false | +|_receiverQueueSize_ |Size of a consumer's receiver queue. + + + +For example, the number of messages accumulated by a consumer before an application calls `Receive`. + + + +A value higher than the default value increases consumer throughput, though at the expense of more memory utilization. |int |true |1000 +|_acknowledgementsGroupTimeMicros_ |Group a consumer acknowledgment for a specified time. + + + +By default, a consumer uses 100ms grouping time to send out acknowledgments to a broker. + + + +Setting a group time of 0 sends out acknowledgments immediately. + + + +A longer ack group time is more efficient at the expense of a slight increase in message re-deliveries after a failure. |long |true |100000 +|_maxAcknowledgmentGroupSize_ |Group a consumer acknowledgment for the number of messages. |int |true |1000 +|_negativeAckRedeliveryDelayMicros_ |Delay to wait before redelivering messages that failed to be processed. + + + +When an application uses `Consumer#negativeAcknowledge(Message)`, failed messages are redelivered after a fixed timeout. |long |true |60000000 +|_maxTotalReceiverQueueSizeAcrossPartitions_ |The max total receiver queue size across partitions. + + + +This setting reduces the receiver queue size for individual partitions if the total receiver queue size exceeds this value. |int |true |50000 +|_consumerName_ |Consumer name |String |true | +|_ackTimeoutMillis_ |Timeout of unacked messages |long |true |0 +|_tickDurationMillis_ |Granularity of the ack-timeout redelivery. + + + +Using an higher `tickDurationMillis` reduces the memory overhead to track messages when setting ack-timeout to a bigger value (for example, 1 hour). |long |true |1000 +|_priorityLevel_ |Priority level for a consumer to which a broker gives more priority while dispatching messages in Shared subscription type. + + + +The broker follows descending priorities. For example, 0=max-priority, 1, 2,... + + + +In Shared subscription type, the broker *first dispatches messages to the max priority level consumers if they have permits*. Otherwise, the broker considers next priority level consumers. + + + +**Example 1** + +If a subscription has consumerA with `priorityLevel` 0 and consumerB with `priorityLevel` 1, then the broker *only dispatches messages to consumerA until it runs out permits* and then starts dispatching messages to consumerB. + + + +**Example 2** + +Consumer Priority, Level, Permits + +C1, 0, 2 + +C2, 0, 1 + +C3, 0, 1 + +C4, 1, 2 + +C5, 1, 1 + + + +Order in which a broker dispatches messages to consumers is: C1, C2, C3, C1, C4, C5, C4. |int |true |0 +|_maxPendingChunkedMessage_ |The maximum size of a queue holding pending chunked messages. When the threshold is reached, the consumer drops pending messages to optimize memory utilization. |int |true |10 +|_autoAckOldestChunkedMessageOnQueueFull_ |Whether to automatically acknowledge pending chunked messages when the threshold of `maxPendingChunkedMessage` is reached. If set to `false`, these messages will be redelivered by their broker. |boolean |true |false +|_expireTimeOfIncompleteChunkedMessageMillis_ |The time interval to expire incomplete chunks if a consumer fails to receive all the chunks in the specified time period. The default value is 1 minute. |long |true |60000 +|_cryptoKeyReader_ | |CryptoKeyReader |false | +|_messageCrypto_ | |MessageCrypto |false | +|_cryptoFailureAction_ |Consumer should take action when it receives a message that can not be decrypted. + +++*++ *FAIL*: this is the default option to fail messages until crypto succeeds. + +++*++ *DISCARD*:silently acknowledge and not deliver message to an application. + +++*++ *CONSUME*: deliver encrypted messages to applications. It is the application's responsibility to decrypt the message. + + + +The decompression of message fails. + + + +If messages contain batch messages, a client is not be able to retrieve individual messages in batch. + + + +Delivered encrypted message contains `EncryptionContext` which contains encryption and compression information in it using which application can decrypt consumed message payload. |ConsumerCryptoFailureAction |true |FAIL +|_properties_ |A name or value property of this consumer. + + + +`properties` is application defined metadata attached to a consumer. + + + +When getting a topic stats, associate this metadata with the consumer stats for easier identification. |SortedMap |true |++{++} +|_readCompacted_ |If enabling `readCompacted`, a consumer reads messages from a compacted topic rather than reading a full message backlog of a topic. + + + +A consumer only sees the latest value for each key in the compacted topic, up until reaching the point in the topic message when compacting backlog. Beyond that point, send messages as normal. + + + +Only enabling `readCompacted` on subscriptions to persistent topics, which have a single active consumer (like failure or exclusive subscriptions). + + + +Attempting to enable it on subscriptions to non-persistent topics or on shared subscriptions leads to a subscription call throwing a `PulsarClientException`. |boolean |true |false +|_subscriptionInitialPosition_ |Initial position at which to set cursor when subscribing to a topic at first time. |SubscriptionInitialPosition |true |Latest +|_patternAutoDiscoveryPeriod_ |Topic auto discovery period when using a pattern for topic's consumer. + + + +The default and minimum value is 1 minute. |int |true |60 +|_regexSubscriptionMode_ |When subscribing to a topic using a regular expression, you can pick a certain type of topics. + + + +++*++ *PersistentOnly*: only subscribe to persistent topics. + +++*++ *NonPersistentOnly*: only subscribe to non-persistent topics. + +++*++ *AllTopics*: subscribe to both persistent and non-persistent topics. |RegexSubscriptionMode |true |PersistentOnly +|_deadLetterPolicy_ |Dead letter policy for consumers. + + + +By default, some messages are probably redelivered many times, even to the extent that it never stops. + + + +By using the dead letter mechanism, messages have the max redelivery count. *When exceeding the maximum number of redeliveries, messages are sent to the Dead Letter Topic and acknowledged automatically*. + + + +You can enable the dead letter mechanism by setting `deadLetterPolicy`. + + + +**Example** + + + +client.newConsumer() + +.deadLetterPolicy(DeadLetterPolicy.builder().maxRedeliverCount(10).build()) + +.subscribe(); + + + +Default dead letter topic name is `++{++TopicName}-++{++Subscription}-DLQ`. + + + +To set a custom dead letter topic name: + + + +client.newConsumer() + +.deadLetterPolicy(DeadLetterPolicy.builder().maxRedeliverCount(10) + +.deadLetterTopic("your-topic-name").build()) + +.subscribe(); + + + +When specifying the dead letter policy while not specifying `ackTimeoutMillis`, you can set the ack timeout to 30000 millisecond. |DeadLetterPolicy |true | +|_retryEnable_ | |boolean |true |false +|_batchReceivePolicy_ | |BatchReceivePolicy |false | +|_autoUpdatePartitions_ |If `autoUpdatePartitions` is enabled, a consumer subscribes to partition increasement automatically. + + + +**Note**: this is only for partitioned consumers. |boolean |true |true +|_autoUpdatePartitionsIntervalSeconds_ | |long |true |60 +|_replicateSubscriptionState_ |If `replicateSubscriptionState` is enabled, a subscription state is replicated to geo-replicated clusters. |boolean |true |false +|_resetIncludeHead_ | |boolean |true |false +|_keySharedPolicy_ | |KeySharedPolicy |false | +|_batchIndexAckEnabled_ | |boolean |true |false +|_ackReceiptEnabled_ | |boolean |true |false +|_poolMessages_ | |boolean |true |false +|_payloadProcessor_ | |MessagePayloadProcessor |false | +|_startPaused_ | |boolean |true |false +|_autoScaledReceiverQueueSizeEnabled_ | |boolean |true |false +|_topicConfigurations_ | |List |true |++[]++ +|=== diff --git a/content/docs/smallrye-reactive-messaging/4/_partials/pulsar/config/smallrye-pulsar-producer.adoc b/content/docs/smallrye-reactive-messaging/4/_partials/pulsar/config/smallrye-pulsar-producer.adoc new file mode 100644 index 000000000..e8755e86b --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/_partials/pulsar/config/smallrye-pulsar-producer.adoc @@ -0,0 +1,60 @@ +[width="100%",cols="<13%,<42%,^17%,^17%,<11%",options="header",] +|=== +|Attribute |Description |Type |Config file |Default +|_topicName_ |Topic name |String |true | +|_producerName_ |Producer name |String |true | +|_sendTimeoutMs_ |Message send timeout in ms. + +If a message is not acknowledged by a server before the `sendTimeout` expires, an error occurs. |long |true |30000 +|_blockIfQueueFull_ |If it is set to `true`, when the outgoing message queue is full, the `Send` and `SendAsync` methods of producer block, rather than failing and throwing errors. + +If it is set to `false`, when the outgoing message queue is full, the `Send` and `SendAsync` methods of producer fail and `ProducerQueueIsFullError` exceptions occur. + + + +The `MaxPendingMessages` parameter determines the size of the outgoing message queue. |boolean |true |false +|_maxPendingMessages_ |The maximum size of a queue holding pending messages. + + + +For example, a message waiting to receive an acknowledgment from a https://pulsar.apache.org/docs/reference-terminology#broker[broker]. + + + +By default, when the queue is full, all calls to the `Send` and `SendAsync` methods fail *unless* you set `BlockIfQueueFull` to `true`. |int |true |0 +|_maxPendingMessagesAcrossPartitions_ |The maximum number of pending messages across partitions. + + + +Use the setting to lower the max pending messages for each partition (`#setMaxPendingMessages(int)`) if the total number exceeds the configured value. |int |true |0 +|_messageRoutingMode_ |Message routing logic for producers on https://pulsar.apache.org/docs/concepts-architecture-overview#partitioned-topics[partitioned topics]. + +Apply the logic only when setting no key on messages. + +Available options are as follows: + +++*++ `pulsar.RoundRobinDistribution`: round robin + +++*++ `pulsar.UseSinglePartition`: publish all messages to a single partition + +++*++ `pulsar.CustomPartition`: a custom partitioning scheme |MessageRoutingMode |true | +|_hashingScheme_ |Hashing function determining the partition where you publish a particular message (partitioned topics only). + +Available options are as follows: + +++*++ `pulsar.JavastringHash`: the equivalent of `string.hashCode()` in Java + +++*++ `pulsar.Murmur3++_++32Hash`: applies the https://en.wikipedia.org/wiki/MurmurHash[Murmur3] hashing function + +++*++ `pulsar.BoostHash`: applies the hashing function from C{plus}{plus}'shttps://www.boost.org/doc/libs/1_62_0/doc/html/hash.html[Boost] library |HashingScheme |true |JavaStringHash +|_cryptoFailureAction_ |Producer should take action when encryption fails. + +++*++ *FAIL*: if encryption fails, unencrypted messages fail to send. + +++*++ *SEND*: if encryption fails, unencrypted messages are sent. |ProducerCryptoFailureAction |true |FAIL +|_customMessageRouter_ | |MessageRouter |false | +|_batchingMaxPublishDelayMicros_ |Batching time period of sending messages. |long |true |1000 +|_batchingPartitionSwitchFrequencyByPublishDelay_ | |int |true |10 +|_batchingMaxMessages_ |The maximum number of messages permitted in a batch. |int |true |1000 +|_batchingMaxBytes_ | |int |true |131072 +|_batchingEnabled_ |Enable batching of messages. |boolean |true |true +|_batcherBuilder_ | |BatcherBuilder |false | +|_chunkingEnabled_ |Enable chunking of messages. |boolean |true |false +|_chunkMaxMessageSize_ | |int |true |-1 +|_cryptoKeyReader_ | |CryptoKeyReader |false | +|_messageCrypto_ | |MessageCrypto |false | +|_encryptionKeys_ | |Set |true |++[]++ +|_compressionType_ |Message data compression type used by a producer. + +Available options: + +++*++ https://github.com/lz4/lz4[LZ4] + +++*++ https://zlib.net/[ZLIB] + +++*++ https://facebook.github.io/zstd/[ZSTD] + +++*++ https://google.github.io/snappy/[SNAPPY] |CompressionType |true |NONE +|_initialSequenceId_ | |Long |true | +|_autoUpdatePartitions_ | |boolean |true |true +|_autoUpdatePartitionsIntervalSeconds_ | |long |true |60 +|_multiSchema_ | |boolean |true |true +|_accessMode_ | |ProducerAccessMode |true |Shared +|_lazyStartPartitionedProducers_ | |boolean |true |false +|_properties_ | |SortedMap |true |++{++} +|_initialSubscriptionName_ |Use this configuration to automatically create an initial subscription when creating a topic. If this field is not set, the initial subscription is not created. |String |true | +|=== diff --git a/content/docs/smallrye-reactive-messaging/4/amqp/amqp.adoc b/content/docs/smallrye-reactive-messaging/4/amqp/amqp.adoc new file mode 100644 index 000000000..d4326ffeb --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/amqp/amqp.adoc @@ -0,0 +1,51 @@ += AMQP 1.0 Connector +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The AMQP Connector adds support for AMQP 1.0 to Reactive Messaging. + +Advanced Message Queuing Protocol 1.0 (AMQP 1.0) is an open standard for +passing business messages between applications or organizations. + +With this connector, your application can: + +* receive messages from an AMQP Broker or Router. +* send `Message` to an AMQP _address_ +* implement xref:amqp/request-reply.adoc[request-reply] messaging patterns +* authenticate using xref:amqp/cbs-authentication.adoc[Claims-Based Security (CBS)] tokens + +The AMQP connector is based on the https://vertx.io/docs/vertx-amqp-client/java/[Vert.x AMQP Client]. + +Using the AMQP connector + +To use the AMQP Connector, add the following dependency to your project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.reactive + smallrye-reactive-messaging-amqp + {project-version} + +---- + +The connector name is: `smallrye-amqp`. + +So, to indicate that a channel is managed by this connector you need: + +[source,properties] +---- +# Inbound +mp.messaging.incoming.[channel-name].connector=smallrye-amqp + +# Outbound +mp.messaging.outgoing.[channel-name].connector=smallrye-amqp +---- + +[IMPORTANT] +.RabbitMQ +==== + +To use RabbitMQ, refer to xref:#amqp-rabbitmq[Using RabbitMQ]. + +==== diff --git a/content/docs/smallrye-reactive-messaging/4/amqp/cbs-authentication.adoc b/content/docs/smallrye-reactive-messaging/4/amqp/cbs-authentication.adoc new file mode 100644 index 000000000..5b5fd6132 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/amqp/cbs-authentication.adoc @@ -0,0 +1,186 @@ += Claims-Based Security (CBS) Authentication +:version-root: ../ +include::{version-root}_attributes.adoc[] + +[WARNING] +.Experimental +==== + +CBS authentication support is an experimental feature. + +==== + +The AMQP connector supports https://docs.oasis-open.org/amqp/amqp-cbs/v1.0/amqp-cbs-v1.0.html[Claims-Based Security (CBS)] for token-based authentication with AMQP 1.0 brokers. +CBS allows the client to authenticate by sending a security token (such as a JWT or SAS token) to the broker over the AMQP connection, rather than relying solely on SASL mechanisms. +This is commonly used with brokers like *Azure Service Bus*. + +== Enabling CBS + +To enable CBS authentication on a channel, set the `cbs.enabled` attribute: + +[source,properties] +---- +mp.messaging.incoming.my-channel.connector=smallrye-amqp +mp.messaging.incoming.my-channel.host=my-broker.servicebus.windows.net +mp.messaging.incoming.my-channel.port=5671 +mp.messaging.incoming.my-channel.use-ssl=true +mp.messaging.incoming.my-channel.cbs.enabled=true +---- + +CBS can be enabled on both incoming and outgoing channels independently. + +== Providing Tokens + +When CBS is enabled, the connector requires a CDI bean implementing +link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-amqp/{javadoc-version}/io/smallrye/reactive/messaging/amqp/cbs/CbsTokenProvider.html[`CbsTokenProvider`]. +This bean is called during initial connection and on each token refresh cycle. + +[source,java] +---- +include::{examples-dir}/amqp/customization/MyCbsTokenProvider.java[tags=provider,indent=0] +---- + +The `getToken` method receives the channel configuration, allowing you to adapt token generation per channel (e.g., using different audiences or scopes). +It returns a `Uni++<++CbsToken++>++` to support non-blocking token retrieval from external identity providers. + +=== The CbsToken Interface + +A `CbsToken` provides: + +[width="100%",cols="16%,84%",options="header",] +|=== +|Method |Description +|`token()` |The token string value (JWT, SAS token, etc.) +|`type()` |The token type (e.g., `"jwt"` or `"servicebus.windows.net:sastoken"`) +|`expiresAt()` |When the token expires +|`refreshAt()` |When the token should be refreshed (defaults to `expiresAt()` if not set) +|=== + +The `DefaultCbsToken` record provides a convenient implementation: + +[source,java] +---- +// With explicit refresh time (refresh before expiration) +new DefaultCbsToken(token, "jwt", expiresAt, refreshAt); + +// Refresh at expiration +new DefaultCbsToken(token, "jwt", expiresAt); + +// Using validity duration in seconds +new DefaultCbsToken(token, "jwt", 3600); +---- + +== Token Refresh + +The connector automatically refreshes tokens before they expire. +After a successful authorization, a timer is scheduled based on the `refreshAt()` value of the token. + +* If `refreshAt()` is set earlier than `expiresAt()`, the token is refreshed proactively. +* If the refresh fails after retries, the connection is closed and re-established. + +Setting `refreshAt()` earlier than `expiresAt()` is recommended to avoid authentication gaps during refresh: + +[source,java] +---- +new DefaultCbsToken( + token, "jwt", + OffsetDateTime.now().plusHours(1), // expires in 1 hour + OffsetDateTime.now().plusMinutes(45)); // refresh after 45 minutes +---- + +== CBS Exchange Protocols + +The connector supports two CBS exchange protocols, configured with `cbs.exchange`: + +=== `set-token` (default) + +The `set-token` protocol sends the token as an AMQP message to the CBS node with: + +* Subject: `set-token` +* Application property `token-type`: the token type +* Body: the token string + +The broker acknowledges the message to confirm the token was accepted. + +[source,properties] +---- +mp.messaging.incoming.my-channel.cbs.exchange=set-token +---- + +=== `put-token` + +The `put-token` protocol uses a request-reply exchange, compatible with *Azure Service Bus*. +It sends the token as a request and validates the response status code. + +[source,properties] +---- +mp.messaging.incoming.my-channel.cbs.exchange=put-token +mp.messaging.incoming.my-channel.cbs.audience=my-namespace.servicebus.windows.net/my-queue +---- + +When using `put-token`, the `cbs.audience` attribute sets the token audience. +If not configured, the audience is derived from the broker host and target address in the format `amqp://HOST/ADDRESS`. + +== Connection Sharing with CBS + +When multiple channels share the same connection using `container-id`, CBS authentication is performed once for the shared connection: + +[source,properties] +---- +mp.messaging.incoming.my-channel-in.connector=smallrye-amqp +mp.messaging.incoming.my-channel-in.container-id=shared +mp.messaging.incoming.my-channel-in.cbs.enabled=true + +mp.messaging.outgoing.my-channel-out.connector=smallrye-amqp +mp.messaging.outgoing.my-channel-out.container-id=shared +mp.messaging.outgoing.my-channel-out.cbs.enabled=true +---- + +Both channels share a single connection and a single CBS token authorization. + +== Azure Service Bus Example + +To connect to Azure Service Bus using CBS with SAS tokens: + +[source,java] +---- +include::{examples-dir}/amqp/customization/MySasTokenProvider.java[tags=sas-provider,indent=0] +---- + +[source,properties] +---- +mp.messaging.incoming.my-channel.connector=smallrye-amqp +mp.messaging.incoming.my-channel.host=my-namespace.servicebus.windows.net +mp.messaging.incoming.my-channel.port=5671 +mp.messaging.incoming.my-channel.use-ssl=true +mp.messaging.incoming.my-channel.cbs.enabled=true +mp.messaging.incoming.my-channel.cbs.exchange=put-token +mp.messaging.incoming.my-channel.cbs.audience=my-namespace.servicebus.windows.net/my-queue +---- + +== Retry Configuration + +If token authorization fails, the connector retries with exponential backoff. +The retry behavior is controlled by the standard connector retry attributes: + +[source,properties] +---- +mp.messaging.incoming.my-channel.retry-on-fail-attempts=6 # max retries (default: 6) +mp.messaging.incoming.my-channel.retry-on-fail-interval=5 # retry delay in seconds (default: 5) +---- + +If all retries are exhausted, the connection is closed and re-established using the connector's reconnection mechanism. + +== Configuration Reference + +[width="100%",cols="17%,7%,24%,52%",options="header",] +|=== +|Attribute |Type |Default |Description +|`cbs.enabled` |boolean |`false` |Enable CBS authorization +|`cbs.exchange` |string |`set-token` |CBS exchange protocol: `set-token` or `put-token` +|`cbs.audience` |string | |Token audience (auto-derived from address if not set) +|`cbs.scopes` |string | |Token scopes for authorization +|`cbs.token-manager` |string |`default-cbs-token-manager` |CDI bean identifier of a custom `CbsTokenManager.Factory` +|=== + +All attributes are available for both incoming and outgoing channels. diff --git a/content/docs/smallrye-reactive-messaging/4/amqp/client-customization.adoc b/content/docs/smallrye-reactive-messaging/4/amqp/client-customization.adoc new file mode 100644 index 000000000..bc214acb8 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/amqp/client-customization.adoc @@ -0,0 +1,50 @@ += Customizing the underlying AMQP client +:version-root: ../ +include::{version-root}_attributes.adoc[] + +You can customize the underlying AMQP Client configuration by +_producing_ an instance of +https://vertx.io/docs/apidocs/io/vertx/amqp/AmqpClientOptions.html[`AmqpClientOptions`]: + +[source,java] +---- +include::{examples-dir}/amqp/customization/ClientProducers.java[tags=named,indent=0] +---- + +This instance is retrieved and used to configure the client used by the +connector. You need to indicate the name of the client using the +`client-options-name` attribute: + +[source,properties] +---- +mp.messaging.incoming.prices.client-options-name=my-named-options +---- + +If you experience frequent disconnections from the broker, the AmqpClientOptions can also be used to set a heartbeat if you need to keep the AMQP connection permanently. +Some brokers might terminate the AMQP connection after a certain idle timeout. +You can provide a heartbeat value which will be used by the Vert.x proton client to advertise the idle timeout when opening transport to a remote peer. + +[source,java] +---- +@Produces +@Identifier("my-named-options") +public AmqpClientOptions getNamedOptions() { + // set a heartbeat of 30s (in milliseconds) + return new AmqpClientOptions() + .setHeartbeat(30000); +} +---- + +== Client capabilities + +Both incoming and outgoing AMQP channels can be configured with a list +of capabilities to declare during sender and receiver connections with +the AMQP broker. Note that supported capability names are broker +specific. + +[source,properties] +---- +mp.messaging.incoming.sink.capabilities=temporary-topic +... +mp.messaging.outgoing.source.capabilities=shared +---- diff --git a/content/docs/smallrye-reactive-messaging/4/amqp/health.adoc b/content/docs/smallrye-reactive-messaging/4/amqp/health.adoc new file mode 100644 index 000000000..79ec6eb9f --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/amqp/health.adoc @@ -0,0 +1,34 @@ += Health reporting +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The AMQP connector reports the startup, liveness, and readiness of each +inbound (Receiving messages) and outbound (sending messages) channel +managed by the connector: + +* Startup :: For both inbound and outbound, the startup probe reports _OK_ when the +connection with the broker is established, and the AMQP senders and +receivers are opened (the links are attached to the broker). +* Liveness :: For both inbound and outbound, the liveness check verifies that the +connection is established. The check still returns _OK_ if the +connection got cut, but we are attempting a reconnection. +* Readiness :: For the inbound, it checks that the connection is established and the +receiver is opened. Unlike the liveness check, this probe reports _KO_ +until the connection is re-established. For the outbound, it checks that +the connection is established and the sender is opened. Unlike the +liveness check, this probe reports _KO_ until the connection is +re-established. + +[NOTE] +==== + +To disable health reporting, set the `health-enabled` attribute for the +channel to `false`. + +==== + +Note that a message processing failures _nacks_ the message, which is +then handled by the failure-strategy. It is the responsibility of the +`failure-strategy` to report the failure and influence the outcome of +the checks. The `fail` failure strategy reports the failure, and so the +check will report the fault. diff --git a/content/docs/smallrye-reactive-messaging/4/amqp/rabbitmq.adoc b/content/docs/smallrye-reactive-messaging/4/amqp/rabbitmq.adoc new file mode 100644 index 000000000..06631ded9 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/amqp/rabbitmq.adoc @@ -0,0 +1,61 @@ += Using RabbitMQ +:version-root: ../ +include::{version-root}_attributes.adoc[] + +RabbitMQ 4.0{plus} provides native AMQP 1.0 support, making it fully compatible with this connector. +For older versions (3.x), RabbitMQ implements AMQP 0.9.1 and requires the +https://github.com/rabbitmq/rabbitmq-amqp1.0/blob/v3.8.x/README.md[AMQP 1.0 plugin] to be enabled. + +== Address format + +RabbitMQ 4.0{plus} uses the v2 address format with `/queues/` and `/exchanges/` prefixes. +When using this connector with RabbitMQ, prefix your addresses accordingly: + +[source,properties] +---- +mp.messaging.incoming.prices.connector=smallrye-amqp +mp.messaging.incoming.prices.address=/queues/prices + +mp.messaging.outgoing.generated-price.connector=smallrye-amqp +mp.messaging.outgoing.generated-price.address=/queues/prices +mp.messaging.outgoing.generated-price.use-anonymous-sender=false +---- + +== Recommendations + +RabbitMQ support for AMQP 1.0 may differ from other AMQP 1.0 supporting brokers. +We recommend the following configurations. + +To receive messages from RabbitMQ: + +* Set `durable` to `false` + +[source,properties] +---- +mp.messaging.incoming.prices.connector=smallrye-amqp +mp.messaging.incoming.prices.durable=false +---- + +To send messages to RabbitMQ: + +* set the destination `address` (anonymous senders are not supported) +* set `use-anonymous-sender` to `false` + +[source,properties] +---- +mp.messaging.outgoing.generated-price.connector=smallrye-amqp +mp.messaging.outgoing.generated-price.address=prices +mp.messaging.outgoing.generated-price.use-anonymous-sender=false +---- + +It’s not possible to change the destination dynamically (using message +metadata) when using RabbitMQ. The connector automatically detects that +the broker does not support anonymous sender (See +http://docs.oasis-open.org/amqp/anonterm/v1.0/anonterm-v1.0.html). + +== Request/Reply with RabbitMQ + +This connector supports the request-reply pattern with RabbitMQ. +See xref:amqp/request-reply.adoc#using-with-rabbitmq[AMQP Request/Reply] for details. + +Alternatively, you can use the xref:rabbitmq/rabbitmq.adoc[RabbitMQ connector]. diff --git a/content/docs/smallrye-reactive-messaging/4/amqp/receiving-amqp-messages.adoc b/content/docs/smallrye-reactive-messaging/4/amqp/receiving-amqp-messages.adoc new file mode 100644 index 000000000..751056c73 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/amqp/receiving-amqp-messages.adoc @@ -0,0 +1,257 @@ += Receiving messages from AMQP +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The AMQP connector lets you retrieve messages from an https://www.amqp.org/product/architecture[AMQP broker or +router]. The AMQP connector +retrieves _AMQP Messages_ and maps each of them into Reactive Messaging +`Messages`. + +== Example + +Let’s imagine you have an AMQP broker (such as https://activemq.apache.org/components/artemis/[Apache ActiveMQ +Artemis]) running, and +accessible using the `amqp:5672` address (by default it would use +`localhost:5672`). Configure your application to receive AMQP Messages +on the `prices` channel as follows: + +[source,properties] +---- +amqp-host=amqp # <1> +amqp-port=5672 # <2> +amqp-username=my-username # <3> +amqp-password=my-password # <4> + +mp.messaging.incoming.prices.connector=smallrye-amqp # <5> +---- + +[arabic] +. Configures the broker/router host name. You can do it per channel +(using the `host` attribute) or globally using `amqp-host` +. Configures the broker/router port. You can do it per channel (using +the `port` attribute) or globally using `amqp-port`. The default is +`5672`. +. Configures the broker/router username if required. You can do it per +channel (using the `username` attribute) or globally using +`amqp-username`. +. Configures the broker/router password if required. You can do it per +channel (using the `password` attribute) or globally using +`amqp-password`. +. Instructs the `prices` channel to be managed by the AMQP connector + +[NOTE] +==== + +You don’t need to set the AMQP _address_. By default, it uses the +channel name (`prices`). You can configure the `address` attribute to +override it. + +==== + +Then, your application receives `Message++<++Double++>++`. You can consume the +payload directly: + +[source,java] +---- +include::{examples-dir}/amqp/inbound/AmqpPriceConsumer.java[indent=0] +---- + +Or, you can retrieve the `Message++<++Double++>++`: + +[source,java] +---- +include::{examples-dir}/amqp/inbound/AmqpPriceMessageConsumer.java[indent=0] +---- + +== Deserialization + +The connector converts incoming AMQP Messages into Reactive Messaging +`Message++<++T++>++` instances. `T` depends on the _body_ of the received AMQP +Message. + +The http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-types-v1.0-os.html[AMQP Type +System] +defines the supported types. + +[width="100%",cols="66%,34%",options="header",] +|=== +|AMQP Body Type |`++<++T++>++` +|AMQP Value containing a http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-types-v1.0-os.html#section-primitive-type-definitions[AMQP Primitive Type] |the corresponding Java type +|AMQP Value using the `Binary` type |`byte++[]++` +|AMQP Sequence |`List` +|AMQP Data (with binary content) and the `content-type` is set to `application/json` |https://vertx.io/docs/apidocs/io/vertx/core/json/JsonObject.html[`JsonObject`] +|AMQP Data with a different `content-type` |`byte++[]++` +|=== + +If you send objects with this AMQP connector (outbound connector), it +gets encoded as JSON and sent as binary. The `content-type` is set to +`application/json`. You can receive this payload using (Vert.x) JSON +Objects, and then map it to the object class you want: + +[source,java] +---- +@ApplicationScoped +public static class Generator { + + @Outgoing("to-amqp") + public Multi prices() { // <1> + AtomicInteger count = new AtomicInteger(); + return Multi.createFrom().ticks().every(Duration.ofMillis(1000)) + .map(l -> new Price().setPrice(count.incrementAndGet())) + .onOverflow().drop(); + } + +} + +@ApplicationScoped +public static class Consumer { + + List prices = new CopyOnWriteArrayList<>(); + + @Incoming("from-amqp") + public void consume(JsonObject p) { // <2> + Price price = p.mapTo(Price.class); // <3> + prices.add(price); + } + + public List list() { + return prices; + } +} +---- + +[arabic] +. The `Price` instances are automatically encoded to JSON by the +connector +. You can receive it using a `JsonObject` +. Then, you can reconstruct the instance using the `mapTo` method + +== Inbound Metadata + +Messages coming from AMQP contains an instance of link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-amqp/{javadoc-version}/io/smallrye/reactive/messaging/amqp/IncomingAmqpMetadata.html[`IncomingAmqpMetadata`] + +[source,java] +---- +include::{examples-dir}/amqp/inbound/AmqpMetadataExample.java[tags=code,indent=0] +---- + +== Acknowledgement + +When a Reactive Messaging `Message` associated with an AMQP Message is +acknowledged, it informs the broker that the message has been +_accepted_. + +== Failure Management + +If a message produced from an AMQP message is _nacked_, a failure +strategy is applied. The AMQP connector supports six strategies: + +* `fail` - fail the application; no more AMQP messages will be +processed (default). The AMQP message is marked as rejected. +* `accept` - this strategy marks the AMQP message as _accepted_. The +processing continues ignoring the failure. Refer to the http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-messaging-v1.0-os.html#type-accepted[accepted +delivery state +documentation]. +* `release` - this strategy marks the AMQP message as _released_. The +processing continues with the next message. The broker can redeliver +the message. Refer to the http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-messaging-v1.0-os.html#type-released[released delivery state +documentation]. +* `reject` - this strategy marks the AMQP message as rejected. The +processing continues with the next message. Refer to the http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-messaging-v1.0-os.html#type-rejected[rejected +delivery state +documentation]. +* `modified-failed` - this strategy marks the AMQP message as +_modified_ and indicates that it failed (with the `delivery-failed` +attribute). The processing continues with the next message, but the +broker may attempt to redeliver the message. Refer to the http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-messaging-v1.0-os.html#type-modified[modified +delivery state +documentation] +* `modified-failed-undeliverable-here` - this strategy marks the AMQP +message as _modified_ and indicates that it failed (with the +`delivery-failed` attribute). It also indicates that the application +cannot process the message, meaning that the broker will not attempt +to redeliver the message to this node. The processing continues with +the next message. Refer to the http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-messaging-v1.0-os.html#type-modified[modified delivery state +documentation] + +== Configuration Reference + +include::{generated-dir}/smallrye-amqp-incoming.adoc[indent=0] + +You can also pass any property supported by the https://vertx.io/docs/vertx-amqp-client/java/[Vert.x AMQP +client] as attribute. + +To use an existing _address_ or _queue_, you need to configure the +`address`, `container-id` and, optionally, the `link-name` attributes. +For example, if you have an Apache Artemis broker configured with: + +[source,xml] +---- + + +
people
+ true + artemis +
+
+---- + +You need the following configuration: + +[source,properties] +---- +mp.messaging.incoming.people.connector=smallrye-amqp +mp.messaging.incoming.people.durable=true +mp.messaging.incoming.people.address=people +mp.messaging.incoming.people.container-id=people +---- + +You may need to configure the `link-name` attribute, if the queue name +is not the channel name: + +[source,properties] +---- +mp.messaging.incoming.people-in.connector=smallrye-amqp +mp.messaging.incoming.people-in.durable=true +mp.messaging.incoming.people-in.address=people +mp.messaging.incoming.people-in.container-id=people +mp.messaging.incoming.people-in.link-name=people +---- + +== Receiving Cloud Events + +The AMQP connector supports https://cloudevents.io/[Cloud Events]. +When the connector detects a _structured_ or _binary_ Cloud Events, it +adds a link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/io/smallrye/reactive/messaging/ce/IncomingCloudEventMetadata.html[`IncomingCloudEventMetadata`] +into the metadata of the `Message`. `IncomingCloudEventMetadata` +contains accessors to the mandatory and optional Cloud Event attributes. + +If the connector cannot extract the Cloud Event metadata, it sends the +Message without the metadata. + +=== Binary Cloud Events + +For `binary` Cloud Events, *all* mandatory Cloud Event attributes must +be set in the AMQP application properties, prefixed by `cloudEvents:` +(as mandated by the https://github.com/cloudevents/spec/blob/v1.0.1/amqp-protocol-binding.md[protocol +binding]). +The connector considers headers starting with the `cloudEvents:` prefix +but not listed in the specification as extensions. You can access them +using the `getExtension` method from `IncomingCloudEventMetadata`. + +The `datacontenttype` attribute is mapped to the `content-type` header +of the record. + +=== Structured Cloud Events + +For `structured` Cloud Events, the event is encoded in the record’s +value. Only JSON is supported, so your event must be encoded as JSON in +the record’s value. + +Structured Cloud Event must set the `content-type` header of the record +to `application/cloudevents{plus}json; charset=UTF-8`. The message body must +be a valid JSON object containing at least all the mandatory Cloud +Events attributes. + +If the record is a structured Cloud Event, the created Message’s payload +is the Cloud Event `data`. diff --git a/content/docs/smallrye-reactive-messaging/4/amqp/request-reply.adoc b/content/docs/smallrye-reactive-messaging/4/amqp/request-reply.adoc new file mode 100644 index 000000000..4b7bbaad1 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/amqp/request-reply.adoc @@ -0,0 +1,202 @@ += AMQP Request/Reply +:version-root: ../ +include::{version-root}_attributes.adoc[] + +[WARNING] +.Experimental +==== + +AMQP Request Reply Emitter is an experimental feature. + +==== + +The AMQP https://www.enterpriseintegrationpatterns.com/patterns/messaging/RequestReply.html[Request-Reply] pattern allows you to publish a message to an AMQP address and then await for a reply message that responds to the initial request. + +The `AmqpRequestReply` emitter implements the requestor (or the client) of the request-reply pattern for AMQP 1.0 outbound channels: + +[source,java] +---- +include::{examples-dir}/amqp/outbound/AmqpRequestReplyEmitter.java[indent=0] +---- + +The `request` method publishes the request message to the configured target address of the outgoing channel, +and listens on a reply address (by default, the channel name with `-reply` suffix) for a reply message. +When the reply is received the returned `Uni` is completed with the message payload. + +The request send operation generates a correlation id and sets the AMQP `message-id` property, +which it expects to be sent back in the reply message's `correlation-id` property. + +The replier (or the server) can be implemented using a Reactive Messaging processor: + +[source,java] +---- +include::{examples-dir}/amqp/outbound/AmqpReplier.java[indent=0] +---- + +When you need more control over the reply message, such as setting the reply-to address and correlation id explicitly, you can use the `Message` type: + +[source,java] +---- +include::{examples-dir}/amqp/outbound/AmqpReplierWithMetadata.java[indent=0] +---- + +Given the following configuration example: + +[source,properties] +---- +mp.messaging.outgoing.my-request.connector=smallrye-amqp +mp.messaging.outgoing.my-request.address=requests +mp.messaging.outgoing.my-request.reply.address=my-request-reply + +mp.messaging.incoming.request.connector=smallrye-amqp +mp.messaging.incoming.request.address=requests + +mp.messaging.outgoing.reply.connector=smallrye-amqp +mp.messaging.outgoing.reply.address=my-request-reply +mp.messaging.outgoing.reply.use-anonymous-sender=false +---- + +== Requesting with `Message` types + +Like the core Emitter's `send` methods, `request` method also can receive a `Message` type and return a message: + +[source,java] +---- +include::{examples-dir}/amqp/outbound/AmqpRequestReplyMessageEmitter.java[indent=0] +---- + +[NOTE] +==== + +The ingested reply type of the `AmqpRequestReply` is discovered at runtime, +in order to configure a `MessageConverter` to be applied on the incoming message before returning the `Uni` result. + +==== + +== Requesting multiple replies + +You can use the `requestMulti` method to expect any number of replies represented by the `Multi` return type. + +For example this can be used to aggregate multiple replies to a single request. + +[source,java] +---- +include::{examples-dir}/amqp/outbound/AmqpRequestReplyMultiEmitter.java[indent=0] +---- + +Like the other `request` you can also request `Message` types. + +[NOTE] +==== + +The channel attribute `reply.timeout` will be applied between each message, if reached the returned `Multi` will +fail. + +==== + +== Pending replies and reply timeout + +By default, the `Uni` returned from the `request` method is configured to fail with timeout exception if no reply is received after 5 seconds. +This timeout is configurable with the channel attribute `reply.timeout` (in milliseconds). + +[source,properties] +---- +mp.messaging.outgoing.my-request.reply.timeout=10000 +---- + +A snapshot of the list of pending replies is available through the `AmqpRequestReply#getPendingReplies` method. + +== Scaling Request/Reply + +If multiple requestor instances are configured on the same outgoing address, and the same reply address, +each requestor instance will receive replies of all instances. If an observed correlation id doesn't match +the id of any pending replies, the reply is simply discarded. +With the additional network traffic this allows scaling requestors, (and repliers) dynamically. + +== Correlation Ids + +The AMQP Request/Reply allows configuring the correlation id mechanism completely through a `CorrelationIdHandler` implementation. +The default handler is based on randomly generated UUID strings, set as the AMQP `message-id` on the request message. +The reply message is expected to carry the same value in the `correlation-id` property. + +The correlation id handler implementation can be configured using the `reply.correlation-id.handler` attribute. +The default configuration is `uuid`, which uses randomly generated UUID strings as correlation ids. +An alternative `bytes` implementation can be used to generate random binary correlation ids. +The `bytes` id handler generates 12 random bytes, +but the length can be configured with the `smallrye.amqp.request-reply.correlation-id.bytes.length` attribute. + +Custom handlers can be implemented by proposing a CDI-managed bean with `@Identifier` qualifier. + +== Reply Error Handling + +If the reply server produces an error, it can propagate the error back to the requestor, failing the returned `Uni`. + +If configured using the `reply.failure.handler` channel attribute, +the `ReplyFailureHandler` implementations are discovered through CDI, matching the `@Identifier` qualifier. + +A sample reply error handler can lookup application properties and return the error to be thrown by the reply: + +[source,java] +---- +include::{examples-dir}/amqp/outbound/MyAmqpReplyFailureHandler.java[indent=0] +---- + +`null` return value indicates that no error has been found in the reply message, and it can be delivered to the application. + +== Connection Sharing + +Multiple AMQP channels can share the same underlying connection when configured with the same `container-id`. +This reduces resource consumption and is particularly useful for request-reply patterns +where the sender and reply receiver can share a single connection. + +[source,properties] +---- +mp.messaging.outgoing.my-request.connector=smallrye-amqp +mp.messaging.outgoing.my-request.address=requests +mp.messaging.outgoing.my-request.container-id=my-connection +mp.messaging.outgoing.my-request.reply.address=replies + +mp.messaging.incoming.other-channel.connector=smallrye-amqp +mp.messaging.incoming.other-channel.address=other +mp.messaging.incoming.other-channel.container-id=my-connection +---- + +Both channels above will share the same AMQP connection because they use the same `container-id`. + +[NOTE] +==== + +Connection sharing requires all channels with the same `container-id` to have compatible connection settings (host, port, credentials, etc.). +If the settings differ, the connector will detect the conflict and raise an error. + +==== + +== Using with RabbitMQ + +RabbitMQ 4.0{plus} with native AMQP 1.0 support is compatible with the request-reply pattern. +When using RabbitMQ, remember to use `/queues/` prefixed addresses (v2 address format) +and set `use-anonymous-sender=false` as anonymous senders are not supported. + +[source,properties] +---- +mp.messaging.outgoing.my-request.connector=smallrye-amqp +mp.messaging.outgoing.my-request.address=/queues/requests +mp.messaging.outgoing.my-request.reply.address=/queues/replies +mp.messaging.outgoing.my-request.use-anonymous-sender=false + +mp.messaging.incoming.request.connector=smallrye-amqp +mp.messaging.incoming.request.address=/queues/requests + +mp.messaging.outgoing.reply.connector=smallrye-amqp +mp.messaging.outgoing.reply.address=/queues/replies +mp.messaging.outgoing.reply.use-anonymous-sender=false +---- + +See xref:amqp/rabbitmq.adoc[Using RabbitMQ] for more details on RabbitMQ-specific configuration. + +== Limitations + +The following features are not yet supported: + +* *Direct reply-to* (`amq.rabbitmq.reply-to`): RabbitMQ's direct reply-to mechanism requires link pairing, which is not yet implemented. +* *Link pairing*: Pairing sender and receiver links on the same connection for request-reply routing is not yet available. diff --git a/content/docs/smallrye-reactive-messaging/4/amqp/sending-amqp-messages.adoc b/content/docs/smallrye-reactive-messaging/4/amqp/sending-amqp-messages.adoc new file mode 100644 index 000000000..f89a2a0b6 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/amqp/sending-amqp-messages.adoc @@ -0,0 +1,254 @@ += Sending messages to AMQP +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The AMQP connector can write Reactive Messaging `Messages` as AMQP +Messages. + +== Example + +Let’s imagine you have an AMQP broker (such as https://activemq.apache.org/components/artemis/[Apache ActiveMQ +Artemis]) running, and +accessible using the `amqp:5672` address (by default it would use +`localhost:5672`). Configure your application to send the messages from +the `prices` channel as AMQP Message as follows: + +[source,properties] +---- +amqp-host=amqp # <1> +amqp-port=5672 # <2> +amqp-username=my-username # <3> +amqp-password=my-password # <4> + +mp.messaging.outgoing.prices.connector=smallrye-amqp # <5> +---- + +[arabic] +. Configures the broker/router host name. You can do it per channel +(using the `host` attribute) or globally using `amqp-host` +. Configures the broker/router port. You can do it per channel (using +the `port` attribute) or globally using `amqp-port`. The default is +`5672`. +. Configures the broker/router username if required. You can do it per +channel (using the `username` attribute) or globally using +`amqp-username`. +. Configures the broker/router password if required. You can do it per +channel (using the `password` attribute) or globally using +`amqp-password`. +. Instructs the `prices` channel to be managed by the AMQP connector + +[NOTE] +==== + +You don’t need to set the _address_. By default, it uses the channel +name (`prices`). You can configure the `address` attribute to override +it. + +==== + +Then, your application must send `Message++<++Double++>++` to the `prices` +channel. It can use `double` payloads as in the following snippet: + +[source,java] +---- +include::{examples-dir}/amqp/outbound/AmqpPriceProducer.java[indent=0] +---- + +Or, you can send `Message++<++Double++>++`: + +[source,java] +---- +include::{examples-dir}/amqp/outbound/AmqpPriceMessageProducer.java[indent=0] +---- + +== Serialization + +When receiving a `Message++<++T++>++`, the connector convert the message into an +AMQP Message. The payload is converted to the AMQP Message _body_. + +[width="100%",cols="50%,50%",options="header",] +|=== +|`T` |AMQP Message Body +|primitive types or `String` |AMQP Value with the payload +|`Instant` or `UUID` |AMQP Value using the corresponding AMQP Type +|https://vertx.io/docs/apidocs/io/vertx/core/json/JsonObject.html[`JsonObject`] or https://vertx.io/docs/apidocs/io/vertx/core/json/JsonArray.html[`JsonArray`] |AMQP Data using a binary content. The `content-type` is set to `application/json` +|`io.vertx.mutiny.core.buffer.Buffer` |AMQP Data using a binary content. No `content-type` set +|Any other class |The payload is converted to JSON (using a Json Mapper). The result is wrapped into AMQP Data using a *binary* content. The `content-type` is set to `application/json` +|=== + +If the message payload cannot be serialized to JSON, the message is +_nacked_. + +== Outbound Metadata + +When sending `Messages`, you can add an instance of +link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-amqp/{javadoc-version}/io/smallrye/reactive/messaging/amqp/OutgoingAmqpMetadata.html[`OutgoingAmqpMetadata`] +to influence how the message is going to be sent to AMQP. For example, you +can configure the subjects, properties: + +[source,java] +---- +include::{examples-dir}/amqp/outbound/AmqpOutboundMetadataExample.java[tags=code,indent=0] +---- + +== Dynamic address names + +Sometimes it is desirable to select the destination of a message +dynamically. In this case, you should not configure the address inside +your application configuration file, but instead, use the outbound +metadata to set the address. + +For example, you can send to a dynamic address based on the incoming +message: + +[source,java] +---- +include::{examples-dir}/amqp/outbound/AmqpOutboundDynamicAddressExample.java[tags=code,indent=0] +---- + +[NOTE] +==== + +To be able to set the address per message, the connector is using an +_anonymous sender_. + +==== + +== Acknowledgement + +By default, the Reactive Messaging `Message` is acknowledged when the +broker acknowledged the message. When using routers, this +acknowledgement may not be enabled. In this case, configure the +`auto-acknowledgement` attribute to acknowledge the message as soon as +it has been sent to the router. + +If an AMQP message is rejected/released/modified by the broker (or +cannot be sent successfully), the message is nacked. + +== Back Pressure and Credits + +The back-pressure is handled by the `max-inflight-messages` attribute and AMQP _credits_. +The outbound connector requests messages minimum between `max-inflight-messages` and credits allowed by the broker. +When the amount of credits reaches 0, it waits (in a non-blocking fashion) until the broker grants more credits to the AMQP sender. + +When `max-inflight-messages` is set to 0, only AMQP credits apply to limit the requests. + +Note that if an AMQP message send fails, it is retried until `retry-on-fail-attempts` is reached. +If the client reconnects to the broker during the retry, failing messages are sent again but the message order is not preserved. + +To preserve the message order in this case you can set `max-inflight-messages` to + +== Configuration Reference + +include::{generated-dir}/smallrye-amqp-outgoing.adoc[indent=0] + +You can also pass any property supported by the https://vertx.io/docs/vertx-amqp-client/java/[Vert.x AMQP +client] as attribute. + +== Using existing destinations + +To use an existing _address_ or _queue_, you need to configure the +`address`, `container-id` and, optionally, the `link-name` attributes. +For example, if you have an Apache Artemis broker configured with: + +[source,xml] +---- + + +
people
+ true + artemis +
+
+---- + +You need the following configuration: + +[source,properties] +---- +mp.messaging.outgoing.people.connector=smallrye-amqp +mp.messaging.outgoing.people.durable=true +mp.messaging.outgoing.people.address=people +mp.messaging.outgoing.people.container-id=people +---- + +You may need to configure the `link-name` attribute, if the queue name +is not the channel name: + +[source,properties] +---- +mp.messaging.outgoing.people-out.connector=smallrye-amqp +mp.messaging.outgoing.people-out.durable=true +mp.messaging.outgoing.people-out.address=people +mp.messaging.outgoing.people-out.container-id=people +mp.messaging.outgoing.people-out.link-name=people +---- + +To use a `MULTICAST` queue, you need to provide the _FQQN_ +(Fully-qualified queue name) instead of just the name of the queue: + +[source,properties] +---- +mp.messaging.outgoing.people-out.connector=smallrye-amqp +mp.messaging.outgoing.people-out.durable=true +mp.messaging.outgoing.people-out.address=foo +mp.messaging.outgoing.people-out.container-id=foo + +mp.messaging.incoming.people-out.connector=smallrye-amqp +mp.messaging.incoming.people-out.durable=true +mp.messaging.incoming.people-out.address=foo::bar # Note the syntax: address-name::queue-name +mp.messaging.incoming.people-out.container-id=bar +mp.messaging.incoming.people-out.link-name=people +---- + +More details about the AMQP Address model can be found on https://activemq.apache.org/components/artemis/documentation/2.0.0/address-model.html[the Artemis +documentation]. + +== Sending Cloud Events + +The AMQP connector supports https://cloudevents.io/[Cloud Events]. The +connector sends the outbound record as Cloud Events if: + +* the message metadata contains an +`io.smallrye.reactive.messaging.ce.OutgoingCloudEventMetadata` +instance, +* the channel configuration defines the `cloud-events-type` and +`cloud-events-source` attributes. + +You can create +`io.smallrye.reactive.messaging.ce.OutgoingCloudEventMetadata` instances +using: + +[source,java] +---- +include::{examples-dir}/amqp/outbound/AmqpCloudEventProcessor.java[indent=0] +---- + +If the metadata does not contain an id, the connector generates one +(random UUID). The `type` and `source` can be configured per message or +at the channel level using the `cloud-events-type` and +`cloud-events-source` attributes. Other attributes are also +configurable. + +The metadata can be contributed by multiple methods, however, you must +always retrieve the already existing metadata to avoid overriding the +values: + +[source,java] +---- +include::{examples-dir}/amqp/outbound/AmqpCloudEventMultipleProcessors.java[indent=0] +---- + +By default, the connector sends the Cloud Events using the _binary_ +format. You can write _structured_ Cloud Events by setting the +`cloud-events-mode` to `structured`. Only JSON is supported, so the +created records had its `content-type` header set to +`application/cloudevents{plus}json; charset=UTF-8` + +[NOTE] +==== + +you can disable the Cloud Event support by setting the `cloud-events` +attribute to `false` + +==== diff --git a/content/docs/smallrye-reactive-messaging/4/camel/camel-processor.adoc b/content/docs/smallrye-reactive-messaging/4/camel/camel-processor.adoc new file mode 100644 index 000000000..20327120f --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/camel/camel-processor.adoc @@ -0,0 +1,37 @@ += The processor pattern using Camel +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Using the processor pattern, you can consume on a channel using a Camel +component, and produce on a channel using another Camel component. In +that case, the headers present in the incoming metadata will be +forwarded in the outgoing metadata. + +== Example + +Let’s imagine you want to read messages from a Nats subject, process it +and produce a message on a Kafka topic. + +[source,properties] +---- +mp.messaging.incoming.mynatssubject.connector=smallrye-camel # <1> +mp.messaging.incoming.mynatssubject.endpoint-uri=nats:mynatssubject # <2> +mp.messaging.outgoing.mykafkatopic.connector=smallrye-camel # <3> +mp.messaging.outgoing.mykafkatopic.endpoint-uri=kafka:mykafkatopic# <4> + +camel.component.nats.servers=127.0.0.1:5555 # <5> +camel.component.kafka.brokers=127.0.0.1:9092 # <6> +---- + +[arabic] +. Sets the connector for the `mynatssubject` channel +. Configures the `endpoint-uri` for nats subject +. Sets the connector for the `mykafkatopic` channel +. Configures the `endpoint-uri` for the kafka topic +. Sets the URL of the nats server to use +. Sets the URL of a kafka broker to use + +[source,java] +---- +include::{examples-dir}/camel/processor/CamelProcessor.java[indent=0] +---- diff --git a/content/docs/smallrye-reactive-messaging/4/camel/camel.adoc b/content/docs/smallrye-reactive-messaging/4/camel/camel.adoc new file mode 100644 index 000000000..00eb2828e --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/camel/camel.adoc @@ -0,0 +1,75 @@ += Apache Camel Connector +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The Camel connector adds support for Apache Camel to Reactive Messaging. + +https://camel.apache.org/[Camel] is an open source integration +framework let you integrate various systems consuming or producing data. +Camel implements the Enterprise Integration Patterns and provides +several hundred of components used to access databases, message queues, +APIs or basically https://camel.apache.org/components/latest/[anything under the +sun]. + +[IMPORTANT] +==== + +Smallrye Reactive Messaging 4.x moved from `javax` to `jakarta` APIs therefore only supports Camel 4 releases. +Similarly, this connector drops the Java 11 support as this is the case for Camel 4. + +==== + +== Introduction + +Camel is not a messaging broker. But, it allows your Reactive Messaging +application to retrieve data from almost anything and send data to +almost anything. + +So if you want to send Reactive Messaging `Message` to Telegram or +retrieve data from Salesforce or SAP, this is the connector you need. + +One of the Camel cornerstone is the `endpoint` and its `uri` encoding +the connection to an external system. For example, +`file:orders/?delete=true&charset=utf-8` instructs Camel to read the +files from the `orders` directory. URI format and parameters are listed +on the component documentation, such as the https://camel.apache.org/components/latest/file-component.html[File +component]. + +== Using the camel connector + +To you the camel Connector, add the following dependency to your +project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.reactive + smallrye-reactive-messaging-camel + {project-version} + +---- + +You will also need the dependency of the Camel component you are using. +For example, if you want to process files, you would need to add the +Camel File Component artifact: + +[source,xml,subs="attributes+"] +---- + + org.apache.camel + camel-file + {camel-version} + +---- + +The connector name is: `smallrye-camel`. + +So, to indicate that a channel is managed by this connector you need: + +.... +# Inbound +mp.messaging.incoming.[channel-name].connector=smallrye-camel + +# Outbound +mp.messaging.outgoing.[channel-name].connector=smallrye-camel +.... diff --git a/content/docs/smallrye-reactive-messaging/4/camel/receiving-messages-from-camel.adoc b/content/docs/smallrye-reactive-messaging/4/camel/receiving-messages-from-camel.adoc new file mode 100644 index 000000000..65338aedc --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/camel/receiving-messages-from-camel.adoc @@ -0,0 +1,89 @@ += Retrieving data using Camel +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Camel provides many components. To keep this documentation focused on +the integration with Camel, we use the https://camel.apache.org/components/latest/file-component.html[File +component]. +This component let use read files from a directory. So the connector +configured with this component creates a `Message` for each file located +in the directory. As soon as a file is dropped in the directory, a new +`Message` is created. + +== Example + +Let’s imagine you want to read the files from the `orders` directory and +send them to the `files` channel. Configuring the Camel connector to +gets the _file_ from this directory only requires 2 properties: + +[source,properties] +---- +mp.messaging.incoming.files.connector=smallrye-camel # <1> +mp.messaging.incoming.files.endpoint-uri=file:orders/?delete=true&charset=utf-8 # <2> +---- + +[arabic] +. Sets the connector for the `files` channel +. Configures the `endpoint-uri` + +Then, your application receives `Message++<++GenericFile++<++File++>>++`. + +[NOTE] +==== + +The Camel File component produces +`org.apache.camel.component.file.GenericFile` instances. You can +retrieve the actual `File` using `getFile()`. + +==== + +You can consume the payload directly: + +[source,java] +---- +include::{examples-dir}/camel/inbound/CamelFileConsumer.java[indent=0] +---- + +You can also retrieve the `Message++<++GenericFile++<++File++>>++`: + +[source,java] +---- +include::{examples-dir}/camel/inbound/CamelFileMessageConsumer.java[indent=0] +---- + +== Deserialization + +Each Camel component is producing specific objects. As we have seen, the +File component produces `GenericFile`. + +Refer to the component documentation to check which type is produced. + +== Inbound Metadata + +Messages coming from Camel contains an instance of +link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-camel/{javadoc-version}/io/smallrye/reactive/messaging/camel/IncomingExchangeMetadata.html[`IncomingExchangeMetadata`] +in the metadata. + +[source,java] +---- +include::{examples-dir}/camel/inbound/IncomingCamelMetadataExample.java[indent=0] +---- + +This object lets you retrieve the Camel `Exchange`. + +== Failure Management + +If a message produced from a Camel exchange is _nacked_, a failure +strategy is applied. The Camel connector supports 3 strategies: + +* `fail` - fail the application, no more MQTT messages will be +processed. (default) The offset of the record that has not been +processed correctly is not committed. +* `ignore` - the failure is logged, but the processing continue. + +In both cases, the `exchange` is marked as rollback only and the nack +reason is attached to the exchange. + +== Configuration Reference + +include::{generated-dir}/smallrye-camel-incoming.adoc[indent=0] diff --git a/content/docs/smallrye-reactive-messaging/4/camel/sending-messages-to-camel.adoc b/content/docs/smallrye-reactive-messaging/4/camel/sending-messages-to-camel.adoc new file mode 100644 index 000000000..8bf614150 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/camel/sending-messages-to-camel.adoc @@ -0,0 +1,76 @@ += Sending data with Camel +:version-root: ../ +include::{version-root}_attributes.adoc[] + +You can use the Camel connector to send data to almost any type of +system. + +To keep this document focused on the Camel connector, we use the Camel +File component. However, the connector can be used with any Camel +component. + +== Example + +Let’s imagine you want to write generated prices into files. Configure +your application to write the messages from the `prices` channel into a +files as follows: + +[source,properties] +---- +mp.messaging.outgoing.prices.connector=smallrye-camel # <1> +mp.messaging.outgoing.prices.endpoint-uri=file:prices/?fileName=${date:now:yyyyMMddssSS}.txt&charset=utf-8 # <2> +---- + +[arabic] +. Sets the connector for the `prices` channel +. Configure the `endpoint-uri` to write into files in the `prices` +directory + +[IMPORTANT] +==== + +Depending on your implementation of MicroProfile Reactive Messaging, the +`$` may need to be escaped as follows: `$$++{++...}` + +==== + +Then, your application must send `Message++<++String++>++` to the `prices` +channel. It can use `String` payloads as in the following snippet: + +[source,java] +---- +include::{examples-dir}/camel/outbound/CamelPriceProducer.java[indent=0] +---- + +Or, you can send `Message++<++Double++>++`: + +[source,java] +---- +include::{examples-dir}/camel/outbound/CamelPriceMessageProducer.java[indent=0] +---- + +== Serialization + +The serialization is handled by the Camel component. Refer to the Camel +documentation to check which payload type is supported by the component. + +== Outbound Metadata + +When sending `Messages`, you can add an instance +of link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-camel/{javadoc-version}/io/smallrye/reactive/messaging/camel/OutgoingExchangeMetadata.html[`OutgoingExchangeMetadata`] +to the message metadata. You can then influence how the outbound Camel +`Exchange` is created, for example by adding properties: + +[source,java] +---- +include::{examples-dir}/camel/outbound/CamelOutboundMetadataExample.java[tags=code,indent=0] +---- + +== Acknowledgement + +The incoming messages are acknowledged when the underlying Camel +exchange completes. If the exchange fails, the message is nacked. + +== Configuration Reference + +include::{generated-dir}/smallrye-camel-outgoing.adoc[indent=0] diff --git a/content/docs/smallrye-reactive-messaging/4/camel/using-existing-routes.adoc b/content/docs/smallrye-reactive-messaging/4/camel/using-existing-routes.adoc new file mode 100644 index 000000000..ed8b2cfd7 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/camel/using-existing-routes.adoc @@ -0,0 +1,67 @@ += Using the Camel API +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The Camel connector is based on the https://camel.apache.org/components/latest/reactive-streams-component.html[Reactive Streams +support] +from Camel. If you have an application already using the Camel API +(routes, `from`...), you can integrate it with Reactive Messaging. + +== Getting the CamelReactiveStreamsService + +Once you add the Camel connector to your application, you can retrieve +the +`org.apache.camel.component.reactive.streams.api.CamelReactiveStreamsService` +object: + +[source,java] +---- +include::{examples-dir}/camel/api/CamelApi.java[tags=reactive,indent=0] +---- + +This `CamelReactiveStreamsService` lets you create `Publisher` and +`Subscriber` instances from existing routes. + +== Using Camel Route with @Outgoing + +If you have an existing Camel route, you can transform it as a +`Publisher` using the `CamelReactiveStreamsService`. Then, you can +return this `Publisher` from a method annotated with `@Outgoing`: + +[source,java] +---- +include::{examples-dir}/camel/api/CamelApi.java[tags=source,indent=0] +---- + +You can also expose a `RouteBuilder` bean, making sure to use the `Singleton` scope, +as `RouteBuilder` is no longer proxyable: + +[source,java] +---- +include::{examples-dir}/camel/api/CamelApi.java[tags=source-route-builder,indent=0] +---- + +Alternatively you can use the `LambdaRouteBuilder`: + +[source,java] +---- +include::{examples-dir}/camel/api/CamelApi.java[tags=source-lambda-route-builder,indent=0] +---- + +Using Camel Route with @Incoming + +If you have an existing Camel route, you can transform it as a +`Subscriber` using the `CamelReactiveStreamsService`. Then, you can +return this `Subscriber` from a method annotated with `@Incoming`: + +[source,java] +---- +include::{examples-dir}/camel/api/CamelApi.java[tags=sink,indent=0] +---- + +You can also use a producer: + +[source,java] +---- +include::{examples-dir}/camel/api/CamelApi.java[tags=producer,indent=0] +---- diff --git a/content/docs/smallrye-reactive-messaging/4/concepts/acknowledgement.adoc b/content/docs/smallrye-reactive-messaging/4/concepts/acknowledgement.adoc new file mode 100644 index 000000000..782ec0e81 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/concepts/acknowledgement.adoc @@ -0,0 +1,183 @@ += Acknowledgement +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Acknowledgment is an essential concept in messaging. A message is +acknowledged when its processing or reception has been successful. It +allows the broker to move to the next message. + +How acknowledgment is used, and the exact behavior in terms of retry and +resilience depends on the broker. For example, for Kafka, it would +commit the offset. For AMQP, it would inform the broker that the message +has been _accepted_. + +Reactive Messaging supports acknowledgement. The default acknowledgement +depends on the method signature. Also, the acknowledgement policy can be +configured using the `@Acknowledgement` annotation. + +== Chain of acknowledgment + +If we reuse this example: + +[source,java] +---- +include::{examples-dir}/beans/Chain.java[tags=chain,indent=0] +---- + +The framework automatically acknowledges the message received from the +`sink` channel when the `consume` method returns. As a consequence, the +message received by the `process` method is acknowledged, and so on. In +other words, it creates a chain of acknowledgement - from the outbound +channel to the inbound channel. + +When using connectors to receive and consume messages, the outbound +connector acknowledges the messages when they are dispatched +successfully to the broker. The acknowledgment chain would, as a result, +acknowledges the inbound connector, which would be able to send an +acknowledgment to the broker. + +This chain of acknowledgment is automatically implemented when +processing payloads. + +== Acknowledgment when using Messages + +When using `Messages`, the user controls the acknowledgment, and so the +chain is not formed automatically. It gives you more flexibility about +when and how the incoming messages are acknowledged. + +If you create a `Message` using the `with` method, is copy the +acknowledgment function from the incoming message: + +[source,java] +---- +include::{examples-dir}/ack/MessageAckExamples.java[tags=with-payload,indent=0] +---- + +To have more control over the acknowledgment, you can create a brand new +`Message` and pass the acknowledgment function: + +[source,java] +---- +include::{examples-dir}/ack/MessageAckExamples.java[tags=message-creation,indent=0] +---- + +However, you may need to create the acknowledgment chain, to acknowledge +the incoming message: + +[source,java] +---- +include::{examples-dir}/ack/MessageAckExamples.java[tags=process,indent=0] +---- + +To trigger the acknowledgment of the incoming message, use the `ack()` +method. It returns a `CompletionStage`, receiving `null` as value when +the acknowledgment has completed. + +== Acknowledgment when using streams + +When transforming streams of `Message`, the acknowledgment is delegated +to the user. It means that it’s up to the user to acknowledge the +incoming messages: + +[source,java] +---- +include::{examples-dir}/ack/StreamAckExamples.java[tags=message,indent=0] +---- + +In the previous example, we only generate a single message per incoming +message so that we can use the `with` method. It becomes more +sophisticated when grouping incoming messages or when each incoming +message produces multiple messages. + +In the case of a stream of payloads, the default strategy acknowledges +the incoming messages before being processed by the method (regardless +of the outcome). + +[source,java] +---- +include::{examples-dir}/ack/StreamAckExamples.java[tags=payload,indent=0] +---- + +For method receiving a single payload and producing a stream of payloads, it defaults to pre-processing acknowledgement. +However, in this case, post-processing is supported. +It waits for all the produced messages to be acknowledged before acknowledging the received one. +If one of the produced message is nacked, the received one is nacked immediately. + +[source,java] +---- +include::{examples-dir}/ack/StreamAckExamples.java[tags=payload-to-multi,indent=0] +---- + +== Controlling acknowledgement + +The link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/org/eclipse/microprofile/reactive/messaging/Acknowledgment.html[`Acknowledgment`] +annotation lets you customize the default strategy presented in the +previous sections. The `@Acknowledgement` annotation takes a _strategy_ +as parameter. Reactive Messaging proposed 4 strategies: + +* `POST++_++PROCESSING` - the acknowledgement of the incoming message is +executed once the produced message is acknowledged. +* `PRE++_++PROCESSING` - the acknowledgement of the incoming message is +executed before the message is processed by the method. +* `MANUAL` - the acknowledgement is done by the user. +* `NONE` - No acknowledgment is performed, neither manually or +automatically. + +It is recommended to use `POST++_++PROCESSING` as it guarantees that the +full processing has completed before acknowledging the incoming message. +However, sometimes it’s not possible, and this strategy is not available +if you manipulate streams of `Messages` or payloads. + +The `PRE++_++PROCESSING` strategy can be useful to acknowledge a message +early in the process: + +[source,java] +---- +include::{examples-dir}/ack/PreAckExamples.java[tags=pre,indent=0] +---- + +It cuts the acknowledgment chain, meaning that the rest of the +processing is not linked to the incoming message anymore. This strategy +is the default strategy when manipulating streams of payloads. + +Refer to the xref:concepts/signatures.adoc[signature list] to determine +which strategies are available for a specific method signature and +what’s the default strategy. + +== Negative acknowledgement + +Messages can also be _nacked_, which indicates that the message was not +processed correctly. The `Message.nack` method indicates failing +processing (and supply the reason), and, as for successful +acknowledgment, the _nack_ is propagated through the chain of messages. + +If the message has been produced by a connector, this connector +implements specific behavior when receiving a _nack_. It can fail +(default), or ignore the failing, or implement a dead-letter queue +mechanism. Refer to the connector documentation for further details +about the available strategies. + +If the message is sent by an emitter using the `send(P)` method, the +returned `CompletionStage` is completed _exceptionally_ with the _nack_ +reason. + +[source,java] +---- +include::{examples-dir}/ack/NackOnEmitter.java[tags=emitter,indent=0] +---- + +Negative acknowledgment can be manual or automatic. If your method +handles instances of `Message` and the acknowledgment strategy is +`MANUAL`, you can nack a message explicitly. You must indicate the +reason (an exception) when calling the `nack` method. As for successful +acknowledgment, the `nack` returns a `CompletionStage` completed when +the `nack` has been processed. + +If your method uses the `POST++_++PROCESSING` acknowledgment strategy, and +the method fails (either by throwing an exception or by producing a +failure), the message is automatically nacked with the caught exception: + +[source,java] +---- +include::{examples-dir}/ack/AutoNack.java[tags=auto-nack,indent=0] +---- diff --git a/content/docs/smallrye-reactive-messaging/4/concepts/advanced-config.adoc b/content/docs/smallrye-reactive-messaging/4/concepts/advanced-config.adoc new file mode 100644 index 000000000..89aa2cd08 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/concepts/advanced-config.adoc @@ -0,0 +1,43 @@ += Advanced configuration +:version-root: ../ +include::{version-root}_attributes.adoc[] + +== Strict Binding Mode + +By default, SmallRye Reactive Messaging does not enforce whether all +_mediators_ are connected. It just prints a warning message. The strict +mode fails the deployment if some "incomings" are not bound to +"outgoings". To enable this mode, you can pass the +`-Dsmallrye-messaging-strict-binding=true` via the command line, or you +can set the `smallrye-messaging-strict-binding` attribute to `true` in +the configuration: + +[source,text] +---- +smallrye-messaging-strict-binding=true +---- + +== Disabling channels + +You can disable a channel in the configuration by setting the `enabled` +attribute to `false`: + +[source,text] +---- +mp.messaging.outgoing.dummy-sink.connector=dummy +mp.messaging.outgoing.dummy-sink.enabled=false # Disable this channel +---- + +SmallRye Reactive Messaging does not register disabled channels, so make +sure the rest of the application does not rely on them. + +== Publisher metrics + +SmallRye Reactive Messaging integrates MicroProfile Metrics and +Micrometer for registering counter metrics (named +`mp.messaging.message.count`) of published messages per channel. + +Both MicroProfile and Micrometer publisher metrics are enabled by +default if found on the classpath. They can be disabled with +`smallrye.messaging.metrics.mp.enabled` and +`smallrye.messaging.metrics.micrometer.enabled` properties respectively. diff --git a/content/docs/smallrye-reactive-messaging/4/concepts/blocking.adoc b/content/docs/smallrye-reactive-messaging/4/concepts/blocking.adoc new file mode 100644 index 000000000..078af3ac4 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/concepts/blocking.adoc @@ -0,0 +1,105 @@ += @Blocking +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The `io.smallrye.reactive.messaging.annotations.Blocking` annotation can +be used on a method annotated with `@Incoming`, or `@Outgoing` to +indicate that the method should be executed on a worker pool: + +[source,java] +---- +@Outgoing("Y") +@Incoming("X") +@Blocking +public String process(String s) { + return s.toUpperCase(); +} +---- + +If method execution does not need to be ordered, it can be indicated on +the `@Blocking` annotation: + +[source,java] +---- +@Outgoing("Y") +@Incoming("X") +@Blocking(ordered = false) +public String process(String s) { + return s.toUpperCase(); +} +---- + +When unordered, the invocation can happen concurrently. + +By default, use of `@Blocking` results in the method being executed in +the Vert.x worker pool. If it’s desired to execute methods on a custom +worker pool, with specific concurrency needs, it can be defined on +`@Blocking`: + +[source,java] +---- +@Outgoing("Y") +@Incoming("X") +@Blocking("my-custom-pool") +public String process(String s) { + return s.toUpperCase(); +} +---- + +Specifying the concurrency for the above worker pool requires the +following configuration property to be defined: + +.... +smallrye.messaging.worker.my-custom-pool.max-concurrency=3 +.... + +== Custom worker pool shutdown timeout + +When using a custom worker pool, it is possible to define a shutdown timeout in order to wait for the scheduled tasks to complete before shutting down the pool. +This can be done by setting the following configuration property: + +[source,properties] +---- +smallrye.messaging.worker.my-custom-pool.max-concurrency=3 +smallrye.messaging.worker.my-custom-pool.shutdown-timeout=10000 +smallrye.messaging.worker.my-custom-pool.shutdown-check-interval=2000 +---- + +The `shutdown-timeout` property defines the maximum time to wait in milliseconds for the scheduled tasks to complete before shutting down the pool, defaults to 60 seconds. +The `shutdown-check-interval` property defines the interval in milliseconds at which the pool will check if the tasks have completed, defaults to 5 seconds. + +== Supported signatures + +`@Blocking` does not support every signature. The following table lists +the supported ones. + +[width="100%",cols="7%,36%,57%",options="header",] +|=== +|Shape |Signature |Comment +|Publisher |`@Outgoing("in") @Blocking O generator()` |Invokes the generator from a worker thread. If `ordered` is set to `false`, the generator can be called concurrently. +|Publisher |`@Outgoing("in") @Blocking Message++<++O++>++ generator()` |Invokes the generator from a worker thread. If `ordered` is set to `false`, the generator can be called concurrently. +|Processor |`@Incoming("in") @Outgoing("bar") @Blocking O process(I in)` |Invokes the method on a worker thread. If `ordered` is set to `false`, the method can be called concurrently. +|Processor |`@Incoming("in") @Outgoing("bar") @Blocking Message++<++O++>++ process(I in)` |Invokes the method on a worker thread. If `ordered` is set to `false`, the method can be called concurrently. +|Subscriber |`@Incoming("in") @Blocking void consume(I in)` |Invokes the method on a worker thread. If `ordered` is set to `false`, the method can be called concurrently. +|Subscriber |`@Incoming("in") @Blocking Uni++<++Void++>++ consume(I in)` |Invokes the method on a worker thread. If `ordered` is set to `false`, the method can be called concurrently. +|Subscriber |`@Incoming("in") @Blocking Uni++<++Void++>++ consume(Message++<++I++>++ msg)` |Invokes the method on a worker thread. If `ordered` is set to `false`, the method can be called concurrently. +|Subscriber |`@Incoming("in") @Blocking CompletionStage++<++Void++>++ consume(I in)` |Invokes the method on a worker thread. If `ordered` is set to `false`, the method can be called concurrently. +|Subscriber |`@Incoming("in") @Blocking CompletionStage++<++Void++>++ consume(Message++<++I++>++ msg)` |Invokes the method on a worker thread. If `ordered` is set to `false`, the method can be called concurrently. +|=== + +When a method can be called concurrently, the max concurrency depends on +the number of threads from the worker thread pool. + +== Using io.smallrye.common.annotation.Blocking + +`io.smallrye.common.annotation.Blocking` is another annotation with the +same semantic. `io.smallrye.common.annotation.Blocking` is used by +multiple SmallRye projects and Quarkus. + +SmallRye Reactive Messaging also supports +`io.smallrye.common.annotation.Blocking`. However, +`io.smallrye.common.annotation.Blocking` does not allow configuring the +ordering (it defaults to `ordered=true`). + +When both annotations are used, +`io.smallrye.reactive.messaging.annotations.Blocking` is preferred. diff --git a/content/docs/smallrye-reactive-messaging/4/concepts/broadcast.adoc b/content/docs/smallrye-reactive-messaging/4/concepts/broadcast.adoc new file mode 100644 index 000000000..a10dc2c86 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/concepts/broadcast.adoc @@ -0,0 +1,49 @@ += Broadcast +:version-root: ../ +include::{version-root}_attributes.adoc[] + +[WARNING] +.Experimental +==== + +`@Broadcast` is an experimental feature. + +==== + +By default, messages transiting in a channel are only dispatched to a +single consumer. Having multiple consumers is considered as an error, +and is reported at deployment time. + +The link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/io/smallrye/reactive/messaging/annotations/Broadcast.html[`Broadcast`] +annotation changes this behavior and indicates that messages transiting +in the channel are dispatched to all the consumers. `@Broadcast` must be +used with the `@Outgoing` annotation: + +[source,java] +---- +include::{examples-dir}/broadcast/BroadcastExamples.java[tags=chain,indent=0] +---- + +In the previous example, both consumers get the messages. + +You can also control the number of consumers to wait before starting to +dispatch the messages. This allows waiting for the complete graph to be +woven: + +[source,java] +---- +include::{examples-dir}/broadcast/BroadcastWithCountExamples.java[tags=chain,indent=0] +---- + +[NOTE] +==== + +Inbound connectors also support a `broadcast` attribute that allows +broadcasting the messages to multiple downstream subscribers. + +==== + +Use with Emitter + +For details on how to use `@Broadcast` with `Emitter` see the +xref:concepts/emitter.adoc#emitter-and-broadcast[documentation]. diff --git a/content/docs/smallrye-reactive-messaging/4/concepts/client-customizers.adoc b/content/docs/smallrye-reactive-messaging/4/concepts/client-customizers.adoc new file mode 100644 index 000000000..2ca59c240 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/concepts/client-customizers.adoc @@ -0,0 +1,28 @@ += Client Customizers +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Client customizers allow to customize the client instance created by the connector. +Only connectors which create their own client instance support customizers. + +To define a client customizer, you need to provide a CDI bean that implements the `ClientCustomizer++<++T++>++` interface, +parameterized with the config type: + +[source,java] +---- +include::{examples-dir}/customizers/MyClientCustomizer.java[indent=0] +---- + +Connectors which support client customizers will discover all beans and call the `customize` method, +with the _channel name_, the _channel configuration_ and the configuration that'll be used to create the client instance. +If the customizer returns `null` it'll be skipped. + +If you have multiple customizers, customizers can override the `getPriority` method to define the order in which they are called. + +Currently, the following core connectors support client customizers: + +* Kafka: `ClientCustomizer++<++Map++<++String, Object++>>++` +* RabbitMQ: `ClientCustomizer++<++RabbitMQOptions++>++` +* AMQP 1.0: `ClientCustomizer++<++AmqpClientOptions++>++` +* MQTT: `ClientCustomizer++<++MqttClientSessionOptions++>++` +* Pulsar: `ClientCustomizer++<++ClientBuilder++>++`, `ClientCustomizer++<++ConsumerBuilder++<++?++>>++`, `ClientCustomizer++<++ProducerBuilder++<++?++>>++` diff --git a/content/docs/smallrye-reactive-messaging/4/concepts/concepts.adoc b/content/docs/smallrye-reactive-messaging/4/concepts/concepts.adoc new file mode 100644 index 000000000..8b9079d3e --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/concepts/concepts.adoc @@ -0,0 +1,97 @@ += Concepts +:version-root: ../ +include::{version-root}_attributes.adoc[] + +When dealing with event-driven or data streaming applications, there are +a few concepts and vocabulary to introduce. + +== Messages, Payload, Metadata + +A `Message` is an _envelope_ around a `payload`. Your application is +going to receive, process, and send `Messages`. + +Your application’s logic can generate these `Messages` or receive them +from a message broker. They can also be consumed by your application or +sent to a message broker. + +.An application can receive a message, process it and send a resulting message +image::images/messages.png[An application can receive a message, process it and send a resulting message] + +In Reactive Messaging, `Message` are represented by the link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/io/eclipse/microprofile/reactive/messaging/Message.html[`Message`] interface. +Each `Message++<++T++>++` contains a _payload_ of type `++<++T++>++`. This payload can be retrieved using `message.getPayload()`: + +[source,java] +---- +include::{examples-dir}/messages/MessageExamples.java[tags=message,indent=0] +---- + +As you can see in the previous snippet, `Messages` can also have +_metadata_. _Metadata_ is a way to extend messages with additional data. +It can be metadata related to the message broker +(like link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/io/smallrye/reactive/messaging/kafka/api/KafkaMessageMetadata.html[`KafkaMessageMetadata`]), or contain operational data (such as tracing metadata), or business-related data. + +[NOTE] +==== + +When retrieving metadata, you get an `Optional` as it may not be present. + +==== + +[TIP] +==== + +Metadata is also used to influence the outbound dispatching (how the message will be sent to the broker). + +==== + +== Channels and Streams + +Inside your application, `Messages` transit on _channel_. A _channel_ is +a virtual destination identified by a name. + +.The application is a set of channels +image::images/channels.png[The application is a set of channels] + +SmallRye Reactive Messaging connects the component to the channel they read and to the channel they populate. +The resulting structure is a stream: Messages flow between components through channels. + +[NOTE] +.What about Reactive Streams? +==== + +You may wonder why _Reactive Messaging_ has _Reactive_ in the name. The +Messaging part is kind of obvious. The Reactive part comes from the +streams that are created by binding components. These streams are +https://www.reactive-streams.org/[Reactive Streams]. They follow the +subscription and request protocol and implement back-pressure. It also +means that xref:#connectors[Connectors] are intended to use non-blocking IO +to interact with the various message brokers. + +==== + +== Connectors + +Your application is interacting with messaging brokers or event backbone using _connectors_. +A _connector_ is a piece of code that connects to a broker and: + +[arabic] +. subscribe/poll/receive messages from the broker and propagate them to the application +. send/write/dispatch messages provided by the application to the broker + +Connectors are configured to map incoming messages to a specific +_channel_ (consumed by the application) and collect outgoing messages +sent to a specific channel. These collected messages are sent to the +external broker. + +.Connectors manages the communication between the application and the brokers +image::images/connectors.png[Connectors manages the communication between the application and the brokers] + +Each connector is dedicated to a specific technology. +For example, a Kafka Connector only deals with Kafka. + +You don’t necessarily need a connector. +When your application does not use connectors, everything happens _in-memory_, and the streams are created by chaining methods altogether. +Each chain is still a reactive stream and enforces the back-pressure protocol. +When you don’t use connectors, you need to make sure the chain is complete, meaning it starts with a message source, and it ends with a sink. +In other words, you need to generate messages from within the application (using a method with only `@Outgoing`, or an `Emitter`) and consume the messages +from within the application (using a method with only `@Incoming` or using an unmanaged stream). diff --git a/content/docs/smallrye-reactive-messaging/4/concepts/connectors.adoc b/content/docs/smallrye-reactive-messaging/4/concepts/connectors.adoc new file mode 100644 index 000000000..5a10eafca --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/concepts/connectors.adoc @@ -0,0 +1,199 @@ += Connectors +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Reactive Messaging can handle messages generated from within the +application but also interact with remote _brokers_. Reactive Messaging +Connectors interacts with these remote _brokers_ to retrieve messages +and send messages using various protocols and technology. + +Each connector handles to a specific technology. For example, a Kafka +Connector is responsible for interacting with Kafka, while an MQTT +Connector is responsible for MQTT interactions. + +== Connector name + +Each connector has a name. This name is referenced by the application to +indicate that this connector manages a specific channel. + +For example, the SmallRye Kafka Connector is named: `smallrye-kafka`. + +== Inbound and Outbound connectors + +Connector can: + +[arabic] +. retrieve messages from a remote broker (inbound) +. send messages to a remote broker (outbound) + +A connector can, of course, implement both directions. + +Inbound connectors are responsible for: + +[arabic] +. Getting messages from the remote broker, +. Creating a Reactive Messaging `Message` associated with the +retrieved message. +. Potentially associating technical metadata with the message. It +includes unmarshalling the payload. +. Associating an acknowledgment callback to acknowledge the incoming +message when the Reactive Messaging message is +processed/acknowledged. + +[IMPORTANT] +.Reactive matters +==== + +The first step should follow the reactive streams principle: uses +non-blocking technology, respects downstream requests. + +==== + +Outbound connectors are responsible for: + +[arabic] +. Receiving Reactive Messaging `Message` and transform it into a +structure understood by the remote broker. It includes marshaling +the payload. +. If the `Message` contains outbound metadata (metadata set during the +processing to influence the outbound structure and routing), taking +them into account. +. Sending the message to the remote broker. +. Acknowledging the Reactive Messaging `Message` when the broker has +accepted/acknowledged the message. + +== Configuring connectors + +Applications need to configure the connector used by expressing which +channel is managed by which connector. Non-mapped channels are local / +in-memory. + +To configure connectors, you need to have an implementation of +MicroProfile Config. If you don’t have one, add an implementation of +MicroProfile Config in your _classpath_, such as: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.config + smallrye-config + {smallrye-config-version} + +---- + +Then edit the application configuration, generally +`src/main/resources/META-INF/microprofile-config.properties`. + +The application configures the connector with a set of properties +structured as follows: + +.... +mp.messaging.[incoming|outgoing].[channel-name].[attribute]=[value] +.... + +For example: + +.... +mp.messaging.incoming.dummy-incoming-channel.connector=dummy +mp.messaging.incoming.dummy-incoming-channel.attribute=value +mp.messaging.outgoing.dummy-outgoing-channel.connector=dummy +mp.messaging.outgoing.dummy-outgoing-channel.attribute=value +.... + +You configure each channel (both incoming and outgoing) individually. + +The `++[++incoming++|++outgoing++]++` segment indicates the direction. + +* an `incoming` channel consumes data from a message broker or +something producing data. It’s an inbound interaction. It relates to +methods annotated with an `@Incoming` using the same channel name. +* an `outgoing` consumes data from the application and forwards it to +a message broker or something consuming data. It’s an outbound +interaction. It relates to methods annotated with an `@Outgoing` +using the same channel name. + +The `++[++channel-name++]++` is the name of the channel. If the channel name +contains a `.` (dot), you would need to use `"` (double-quote) around +it. For example, to configure the `dummy.incoming.channel` channel, you +would need: + +.... +mp.messaging.incoming."dummy.incoming.channel".connector=dummy +mp.messaging.incoming."dummy.incoming.channel".attribute=value +.... + +The `++[++attribute++]++=++[++value++]++` sets a specific connector attribute to the +given value. Attributes depend on the used connector. So, refer to the +connector documentation to check the supported attributes. + +The `connector` attribute must be set for each mapped channel and +indicates the name of the connector responsible for the channel. + +Here is an example of a channel using an MQTT connector, consuming data +from a MQTT broker, and a channel using a Kafka connector (writing data +to Kafka): + +[source,properties] +---- +# Configure the incoming health channel +mp.messaging.incoming.health.topic=neo +mp.messaging.incoming.health.connector=smallrye-mqtt +mp.messaging.incoming.health.host=localhost +mp.messaging.incoming.health.broadcast=true + +# Configure outgoing data channel +mp.messaging.outgoing.data.connector=smallrye-kafka +mp.messaging.outgoing.data.bootstrap.servers=localhost:9092 +mp.messaging.outgoing.data.key.serializer=org.apache.kafka.common.serialization.StringSerializer +mp.messaging.outgoing.data.value.serializer=io.vertx.kafka.client.serialization.JsonObjectSerializer +mp.messaging.outgoing.data.acks=1 +---- + +[IMPORTANT] +==== + +To use a connector, you need to add it to your _CLASSPATH_. Generally, +adding the dependency to your project is enough. Then, you need to know +the connector’s name and set the `connector` attribute for each channel +managed by this connector. + +==== + +[TIP] +.Connector names +==== + +Connector managed by the SmallRye Reactive Messaging project are typically +prefixed with `smallrye-`. For example, the SmallRye Kafka Connector is +named `smallrye-kafka`. When using Smallrye connectors, you can omit the +prefix and just use `kafka` as the connector name: + +[source,properties] +---- +mp.messaging.incoming.my-channel.connector=kafka +mp.messaging.connector.kafka.bootstrap.servers=localhost:9092 +---- + +==== + +== Connector attribute table + +In the connector documentation, you will find a table listing the +attribute supported by the connector. Be aware that attributes for +inbound and outbound interactions may be different. + +These tables contain the following entries: + +[arabic] +. The name of the attribute, and potentially an _alias_. The name of +the attribute is used with the +`mp.messaging.++[++incoming++|++outgoing++]++.++[++channel-name++]++.++[++attribute++]++=++[++value++]++` +syntax (the `attribute` segment). The _alias_ (if set) is the name +of a global MicroProfile Config property that avoids having to +configure the attribute for each managed channel. For example, to +set the location of your Kafka broker globally, you can use the +`kafka.bootstrap.servers` alias. +. The description of the attribute, including the type. +. Whether that attribute is mandatory. If so, it fails the +deployment if not set +. The default value, if any. diff --git a/content/docs/smallrye-reactive-messaging/4/concepts/contributing-connectors.adoc b/content/docs/smallrye-reactive-messaging/4/concepts/contributing-connectors.adoc new file mode 100644 index 000000000..2af522fc2 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/concepts/contributing-connectors.adoc @@ -0,0 +1,457 @@ += Connector Contribution Guide +:version-root: ../ +include::{version-root}_attributes.adoc[] + +A connector implementation is a CDI-managed bean, typically an `@ApplicationScoped` bean, +which is identified by the `@Connector` identifier. +In order to provide inbound and outbound channels, the connector implements two interfaces `InboundConnector` and `OutboundConnector` respectively. +In addition to that, the connector bean is annotated with `@ConnectorAttribute`, which describes attributes to configure channels. + +[NOTE] +.Maven Archetype +==== + +Smallrye Reactive Messaging provides a Maven archetype to bootstrap a new connector. +You can generate a new connector project with the code described in this guide using: + +[source,shell,subs="attributes+"] +---- +mvn -N archetype:generate \ +-DarchetypeGroupId=io.smallrye.reactive \ +-DarchetypeArtifactId=smallrye-reactive-messaging-connector-archetype \ +-DarchetypeVersion={project-version} \ +-DgroupId=io.smallrye.reactive \ +-Dpackage=io.smallrye.reactive.messaging.my \ +-Dversion={project-version} \ +-DartifactId=smallrye-reactive-messaging-my \ +-DconnectorName=my +---- + +==== + +The following is an example of a connector skeleton : + +[source,java] +---- +include::{examples-dir}/connectors/MyConnector.java[indent=0] +---- + +Note that the `getPublisher` and `getSubscriber` methods receive MicroProfile Config `Config` instance and wrap it with +`MyConnectorIncomingConfiguration` and `MyConnectorOutgoingConfiguration` objects. + +These custom channel configuration types ease getting channel configuration, including the optional or default values. +They are generated by the `smallrye-connector-attribute-processor` annotation processor, and can be configured in project pom like the following: + +[source,xml] +---- + + + ${project.groupId} + smallrye-reactive-messaging-provider + ${project.version} + + + io.smallrye.reactive + smallrye-connector-attribute-processor + ${project.version} + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + ${project.build.directory}/generated-sources/ + + + io.smallrye.reactive.messaging.connector.ConnectorAttributeProcessor + + + org.jboss.logging.processor.apt.LoggingToolsProcessor + + + + + + +---- + +The `smallrye-reactive-messaging-provider` is the minimum required dependency for a connector implementation. +You'll also note that the `LoggingToolsProcessor` annotation processor is also configured. +This enables generating internationalized log statements and exceptions. +Typically, you would create following interfaces in `i18n` sub-package: `++[++Connector++]++Exceptions`, `++[++Connector++]++Logging` and `++[++Connector++]++Messages`. +More information can be found in https://jboss-logging.github.io/jboss-logging-tools/[JBoss Logging Tools documentation]. + +== Implementing Inbound Channels + +The `InboundConnector` implementation returns, for a given channel configuration, a reactive stream of `Message`s. +The returned reactive stream is an instance of `Flow.Publisher` and typically can be implemented using Mutiny `Multi` type. + +[NOTE] +.`IncomingConnectorFactory` +==== + +The inbound channels can also implement the `IncomingConnectorFactory` from the MicroProfile Reactive Messaging specification. +However, the `PublisherBuilder` type can be more challenging to work with and +Smallrye Reactive Messaging converts the provided stream to Mutiny types to do the wiring anyway. + +==== + +The returned `Flow.Publisher` would allow controlling the flow of ingestion using backpressure. +It would be preferable to use pull-based APIs of the underlying messaging library to receive messages from the message broker. +You can refer to the https://smallrye.io/smallrye-mutiny/latest/guides/polling/[`Mutiny` "How to use polling?" guide] +to construct a `Multi` using Mutiny APIs, or implement the `Flow.Subscription` from scratch and wrap it in an `AbstractMulti`. + +Here is an example channel implementation which constructs the reactive stream using the polling API: + +[source,java] +---- +include::{examples-dir}/connectors/MyIncomingChannel.java[indent=0] +---- + +=== Connector Threading and Vert.x + +Whether the external API call is blocking or non-blocking, managing the thread on which the message processing will be dispatched can be challenging. +Smallrye Reactive Messaging depends on https://vertx.io/[Eclipse Vert.x] to consistently dispatch messages in event-loop or worker threads, +propagating the message context along different processing stages. +You can read more on xref:concepts/message-context.adoc[Message Context] and https://vertx.io/blog/an-introduction-to-the-vert-x-context-object/[Vert.x Context]. + +Some connectors already use Vert.x clients, such as RabbitMQ, AMQP 1.0 or MQTT. +Other connectors such as Kafka or Pulsar directly use the client library of the messaging technology, +therefore they create a Vert.x Context per channel to dispatch messages on that context. +Connectors can access the `Vertx` instance by injecting the `ExecutionHolder` bean. +https://smallrye.io/smallrye-mutiny/latest/guides/emit-on-vs-run-subscription-on/[Mutiny operators `runSubscribtionOn` and `emitOn`] can be used to switch threads the events are dispatched on. + +=== Custom Message and Metadata implementations + +Reactive messaging link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/org/eclipse/microprofile/reactive/messaging/Message.html[`Message`] +type is a thin wrapper around a payload and some metadata, which lets implementing acknowledgment and negative acknowledgment of that message. + +Messages received from the underlying library very often return with a custom type, +wrapping the payload and some properties such as key, timestamp, schema information, or any other metadata. + +[TIP] +==== + +The client may propose different strategies for consuming messages individually or in batches. +If a batch consuming is available the incoming channel may receive wrap and dispatch message as a batch or individually. + +==== + +While it is possible to use `Message.of` builder methods to wrap the incoming message type, +a custom type implementing `Message` interface helps to deal with different aspects we'll cover later, +such as deserialization, message acknowledgment or tracing. + +An example message implementation would be like the following: + +[source,java] +---- +include::{examples-dir}/connectors/MyMessage.java[indent=0] +---- + +Note that `MyMessage` implements the `ContextAwareMessage`. +In the constructor `captureContextMetadata` helper method is used to capture the Vert.x context which created the object and capturing it into the `LocalContextMetadata`. +This metadata allows running xref:concepts/message-context.adoc[each message in its own Vert.x context, supporting context propagation]. + +The `MyMessage` type implements the accessors for the `metadata` and `payload` from the `Message` interface. +If the messaging technology doesn't have a built-in unmarshalling mechanism, the message can deserialize the raw payload to a primitive or a complex object. + +[WARNING] +==== + +The custom message implementation is usually not the type consumed by the application injecting channels. +Applications usually inject in the payload, the raw consumed type (in the above example the `ConsumedMessage`), +or some other type provided by the xref:concepts/converters.adoc[`MessageConverter`s]. +Handling of `Message` types by the application is restricted only to advanced use cases, because handling of message acknowledgment is manual +Even then the message may be intercepted before and changed, conserving the `metadata`, `ack` and `nack` handlers but not the original type created by the connector. + +==== + +The `MyIncomingMetadata` gives access to the underlying consumed message attributes, and applications can inject this object for accessing message details: + +[source,java] +---- +include::{examples-dir}/connectors/MyIncomingMetadata.java[indent=0] +---- + +Also note that `ack` and `nack` method implementations are delegated to handler objects. +This allows configuring different strategies at channel level. + +=== Acknowledgment strategies + +The xref:concepts/acknowledgement.adoc[acknowledgement] is the way for message consumers to inform the broker that the message has been successfully received and processed. +Depending on the messaging technology the broker then can decide to remove the message from the queue, flag as consumed or purge it completely. +In Reactive Messaging there are different policies to trigger the acknowledgement but the canonical one is to acknowledge a message when the processing (potentially asynchronous) has completed (`POST++_++PROCESSING`). + +The Reactive Messaging defines `Message#ack` method as non-blocking asynchronous, returning a `CompletionStage++<++Void++>++`, +because potentially the acknowledgement action sends a network call to the broker. + +The following example simply calls the client `ack` method using the Mutiny `Uni` and switch the emitter to the Message context. +Returning back to the message context is essential for chaining asynchronous actions without losing the context and for keeping the consistency on message consumption flow. + +[source,java] +---- +include::{examples-dir}/connectors/MyAckHandler.java[indent=0] +---- + +While this ack handler strategy acknowledges each message to the broker, +the messaging technology can allow employing different strategies for acknowledging messages. +For example an ack strategy can track processed messages and acknowledge them altogether or call a different client side endpoint to acknowledge the message batch. + +=== Failure handling strategies + +The failure handling, or the xref:concepts/acknowledgement.adoc#negative-acknowledgement[negative acknowledgment] allows indicating that a message was not processed correctly. +Similar to the acknowledgment the Reactive Messaging defines `Message#nack(Throwable reason, Metadata metadata)` method as non-blocking asynchronous, returning a `CompletionStage++<++Void++>++`. + +[source,java] +---- +include::{examples-dir}/connectors/MyFailureHandler.java[indent=0] +---- + +Different failure handling strategies can, for example, +- Ignore the failure, log and call the `ack` instead +- Send the message to a dead letter queue and call the `ack` +- Employ a different strategy depending on the `Metadata` associated with the `nack` method call. + +=== Message Converters + +The connector can propose default xref:concepts/converters.adoc[`MessageConverter`] implementations for converting the payload to a custom type. +As an example the following converter extracts the `CustomMessage` and puts it in the payload: + +[source,java] +---- +include::{examples-dir}/connectors/MyMessageConverter.java[indent=0] +---- + +== Implementing Outbound Channels + +The `OutboundConnector` implementation returns, for a given channel configuration, a `Flow.Subscriber` of messages. +This is typically implemented by a custom `Flow.Processor` and using the `MultiUtils.via` helper methods to apply message transformations. + +[NOTE] +.`OutgoingConnectorFactory` +==== + +The outbound channels can also implement the `OutgoingConnectorFactory` from the MicroProfile Reactive Messaging specification. +However, it is usually more friendly to work with the `MultiUtils.via` methods to construct and transform outgoing messages. + +==== + +Here is an example outgoing channel implementation: + +[source,java] +---- +include::{examples-dir}/connectors/MyOutgoingChannel.java[indent=0] +---- + +The `MultiUtils.via` helper method allows using the `Multi` chaining methods and in the same time provides a `Flow.Subscriber`. +However, this implementation allows sending messages one at a time: +one only after the previous outgoing message send is completed. + +Some messaging technologies provide publish receipt, +a message back from the broker to the sender that asynchronously acknowledges the sent operation. +In this case the connector can only be sure of the send operation when it receives the publish receipt of that message. +Some technologies may provide blocking sending calls, +in that case the connector needs to delegate the sending call to a worker thread. + +Depending on whether the client supports multiple in-flight outgoing messages, you can also use a `SenderProcessor`, +which allows receiving configuration for the maximum number of in-flight messages and whether it waits for completion (publish receipt from the broker): + +[source,java] +---- +include::{examples-dir}/connectors/MyOutgoingChannelWithPartials.java[tags=sender-processor,indent=0] +---- + +Other more advanced scenarios can be implemented to retry the transmission in case of a retryable failure, +or batch multiple outgoing messages to a single send operation. + +=== Outgoing `Message` Builder + +In order to convey all attributes of the outgoing message to the client library +connectors provide outgoing `Message` implementation and a corresponding outgoing message metadata. +These allow the application developer to build the message attributes that will be sent to the broker. + +[source,java] +---- +include::{examples-dir}/connectors/MyOutgoingMessage.java[indent=0] +---- + +[source,java] +---- +include::{examples-dir}/connectors/MyOutgoingMetadata.java[indent=0] +---- + +The outgoing channel implementation then will construct the client library object that represents the outbound message, `SendMessage` in this example: + +[source,java] +---- +include::{examples-dir}/connectors/MyOutgoingChannelWithPartials.java[tags=send-message,indent=0] +---- + +It is a best practice to also allow the application to return a payload of the client outbound library object (`SendMessage`). + +=== Outgoing message acknowledgement + +Because the Reactive Messaging xref:concepts/acknowledgement.adoc#chain-of-acknowledgment[chains acknowledgements] from incoming message until the outgoing message, +it is crucial for the outgoing channel to correctly ack and nack the message. + +== Smallrye Health Integration + +Smallrye Reactive Messaging allows connectors to integrate with Smallrye Health to contribute channel state to the health reports. +Connectors need to implement the `HealthReporter` interface and implement some or all of the `getReadiness`, `getLiveness` and `getStartup` methods: + +[source,java] +---- +include::{examples-dir}/connectors/MyConnectorWithPartials.java[tags=health-report,indent=0] +---- + +Implementing health reports per channel depends on what information is available to the connector. +For more information on different health check probes you can check out https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/[Configure Liveness, Readiness and Startup Probes] + +You may want to add a connector attribute to enable/disable the health reporting per channel: + +[source,java] +---- +import io.smallrye.reactive.messaging.annotations.ConnectorAttribute; + +@ConnectorAttribute(name = "health-enabled", type = "boolean", direction = ConnectorAttribute.Direction.INCOMING_AND_OUTGOING, description = "Whether health reporting is enabled (default) or disabled", defaultValue = "true") +---- + +== OpenTelemetry Tracing Integration + +Smallrye Reactive Messaging allows connectors to easily integrate with OpenTelemetry tracing. +It propagates the tracing context from inbound messages and to outbound messages. +The `smallrye-reactive-messaging-otel` module provides necessary dependencies to the OpenTelemetry artifacts and also provides `TracingUtils` helper class for setting up the tracing. + +[source,xml] +---- + + io.smallrye.reactive.messaging + smallrye-reactive-messaging-otel + +---- + +For integrating tracing you'd need to create a couple of classes: + +* Holder class for tracing information +* Implementation of `io.opentelemetry.context.propagation.TextMapGetter`, which retrieves for a given key the value of a tracing attributed from the holder object +* Implementation of `io.opentelemetry.context.propagation.TextMapSetter`, which sets on the holder object the key and value of tracing attributes +* Implementations of `io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor` and `io.opentelemetry.instrumentation.api.instrumenter.messaging.MessagingAttributesGetter` + +Then you'd need to configure instrumenters per incoming and outgoing channel: + +[source,java] +---- +include::{examples-dir}/connectors/tracing/MyOpenTelemetryInstrumenter.java[tags=create-instrumenter,indent=0] +---- + +Finally, you'd need to configure instrumenters per incoming and outgoing channels and wire the call to instrumenter using `TracingUtils`. + +For an incoming channel, you'd need to call the instrumenter on an inbound message: + +[source,java] +---- +include::{examples-dir}/connectors/MyIncomingChannelWithPartials.java[tags=incoming-tracing,indent=0] +---- + +For an outgoing channel, you'd need to call the instrumenter on constructing the outbound message: + +[source,java] +---- +include::{examples-dir}/connectors/MyOutgoingChannelWithPartials.java[tags=outgoing-tracing,indent=0] +---- + +You may want to add a connector attribute to enable/disable the tracing per channel: + +[source,java] +---- +import io.smallrye.reactive.messaging.annotations.ConnectorAttribute; + +@ConnectorAttribute(name = "tracing-enabled", type = "boolean", direction = ConnectorAttribute.Direction.INCOMING_AND_OUTGOING, description = "Whether tracing is enabled (default) or disabled", defaultValue = "true") +---- + +== Testing the connector + +While unit tests are highly encouraged for validating ad-hoc logic in connector code, +by nature connector tests are mostly integration tests validating the correct configuration and functioning of channels. +Most of the time tests need to run against a broker instance. +This instance can be mocked or embedded in the test JVM, +or provisioned in a container runtime using https://testcontainers.org[Testcontainers]. +The testcontainers approach is encouraged as it'll provide a testing environment closest to reality. + +It may take too much time and resources to start a broker per test method or per test class, +so may want to share the same broker instance between all test classes. +In that case you can checkout how to write a https://junit.org/junit5/docs/current/user-guide/#extensions[JUnit 5 Extension] +and start only one container instance in the beginning of tests and stop it at the end of all the tests. + +There are essentially two ways of creating the connector behavior to test against: + +[arabic] +. Instantiating channels directly by passing the custom configuration. +With this you can get the Reactive stream directly from the channel implementation and send/receive messages. +You can use https://smallrye.io/smallrye-mutiny/2.5.1/guides/testing/[AssertSubscriber from Mutiny] to regulate demand and write assertions. +. CDI-based tests which write configuration and instantiate application beans. +You can use https://weld.cdi-spec.org/[Weld, the reference implementation of CDI specification] with configured set of beans and extensions: + +[source,java] +---- +include::{examples-dir}/connectors/test/WeldTestBase.java[indent=0] +---- + +You would need following test dependencies for enabling Weld in tests: + +[source,xml] +---- + + io.smallrye.reactive + test-common + test + + + org.jboss.weld.se + weld-se-shaded + ${weld.version} + test + + + org.jboss.weld + weld-core-impl + ${weld.version} + test + + + org.awaitility + awaitility + test + +---- + +Your test classes can therefore extend the `WeldTestBase` and provide configuration and application beans: + +[source,java] +---- +include::{examples-dir}/connectors/test/MyConnectorTest.java[indent=0] +---- + +[TIP] +.Awaitility +==== + +Because connector tests are usually asynchronous, https://github.com/awaitility/awaitility[awaitility] +provides a DSL to await on expressed assertions. + +==== + +=== Common tests for validating the connector + +* Message consumption through incoming channels +* Message producing through outgoing channels +* Ack and failure handler strategies test +* Message Context propagation test `LocalPropagationTest` +* `HealthCheckTest` +* `MessageConverterTest` +* `TracingPropagationTest` +* Configuration test +* Authentication test +* Tests for diff --git a/content/docs/smallrye-reactive-messaging/4/concepts/converters.adoc b/content/docs/smallrye-reactive-messaging/4/concepts/converters.adoc new file mode 100644 index 000000000..ff3f6148e --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/concepts/converters.adoc @@ -0,0 +1,43 @@ += Message Converters +:version-root: ../ +include::{version-root}_attributes.adoc[] + +SmallRye Reactive Messaging supports _message converters_, allowing to +transform an incoming message into a version accepted by the method. If +the incoming messages or payload does not match the invoked method’s +expectation, SmallRye Reactive Messaging looks for a suitable converter. +If found, it converts the incoming message with this converter. + +Converters can have multiple purposes, but the main use case is about +transforming the message’s payload: + +[source,java] +---- +include::{examples-dir}/converters/MyConverter.java[tags=code,indent=0] +---- + +To provide a converter, implement a bean exposing the `MessageConverter` +interface. The `canConvert` method is called during the lookup and +verifies if it can handle the conversion. The `target` type is the +expected payload type. If the converter returns `true` to `canConvert`, +SmallRye Reactive Messaging calls the `convert` method to proceed to the +conversion. + +The previous converter can be used in application like the following, to +convert `Message++<++String++>++` to `Message++<++Person++>++`: + +[source,java] +---- +include::{examples-dir}/converters/ConverterExample.java[tags=code,indent=0] +---- + +Converters work for all supported method signatures. However, the +signature must be well-formed to allow the extraction of the expected +payload type. Wildcards and raw types do not support conversion. If the +expected payload type cannot be extracted, or no converter fits, the +message is passed as received. + +If multiple suitable converters are present, implementations should +override the `getPriority` method returning the priority. The default +priority is `100`. The converter lookup invokes converters with higher +priority (from the least value to the greatest) first. diff --git a/content/docs/smallrye-reactive-messaging/4/concepts/decorators.adoc b/content/docs/smallrye-reactive-messaging/4/concepts/decorators.adoc new file mode 100644 index 000000000..0cdb8b7a9 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/concepts/decorators.adoc @@ -0,0 +1,96 @@ += Channel Decorators +:version-root: ../ +include::{version-root}_attributes.adoc[] + +SmallRye Reactive Messaging supports decorating reactive streams +of incoming and outgoing channels for implementing cross-cutting +concerns such as monitoring, tracing or message interception. + +Two symmetrical APIs are proposed for decorating publisher and subscriber channels, +link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/io/smallrye/reactive/messaging/PublisherDecorator.html[`PublisherDecorator`] +and link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/io/smallrye/reactive/messaging/SubscriberDecorator.html[`SubscriberDecorator`] respectively. + +[IMPORTANT] +==== + +`@Incoming` channels and channels bound to an outbound connector are both `Subscriber`s. +Conversely `@Outgoing` channels and channels bound to an inbound connector are `Publisher`s. + +==== + +For example, to provide a decorator which counts consumed messages from incoming connector, +implement a bean exposing the interface `PublisherDecorator`: + +[source,java] +---- +include::{examples-dir}/decorators/ConsumedMessageDecorator.java[tags=code,indent=0] +---- + +Decorators' `decorate` method is called only once per channel at application deployment when graph wiring is taking place. +Decorators are very powerful because they receive the stream of messages (Mutiny `Multi++<++Message++<++?++>>++`) +and potentially return a new stream of messages. + +Note that if a decorator is available it will be called on every channel. +The `decorate` method receives the channel name and whether the channel is a connector or not as parameters. +Decorators are called ordered from highest to lowest priority (from the least value to the greatest), +obtained using the `jakarta.enterprise.inject.spi.Prioritized#getPriority` method. + +[NOTE] +==== + +The `SubscriberDecorator` receive a list of channel names because `@Incoming` annotation is repeatable +and consuming methods can be linked to multiple channels. + +==== + +== Intercepting Incoming and Outgoing Messages + +Decorators (`PublisherDecorator` and `SubscriberDecorator`) can be used to intercept and alter messages, both on incoming and outgoing channels. + +Smallrye Reactive Messaging allows defining intercepting incoming and outgoing messages for a specific channel, using +`IncomingInterceptor` and `OutgoingInterceptor` respectively. + +Only one interceptor is allowed to be bound for interception per outgoing channel. +If no interceptors are found with a `@Identifier` but a `@Default` one is available, it is used. +When multiple interceptors are available, the bean with the highest priority is used. + +=== `IncomingInterceptor` + +To provide an incoming interceptor implement a bean exposing the interface link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/io/smallrye/reactive/messaging/IncomingInterceptor.html[`IncomingInterceptor`], qualified with a `@Identifier` with the channel name to intercept. + +[source,java] +---- +include::{examples-dir}/interceptors/MyIncomingInterceptor.java[indent=0] +---- + +An `IncomingInterceptor` can implement these three methods: + +* `Message++<++?++>++ afterMessageReceive(Message++<++?++>++ message)` : Called after receiving the message from an incoming connector. +The message can be altered by returning a new message from this method. The modified message will be consumed in incoming channels. +* `void onMessageAck(Message++<++?++>++ message)` : Called after message acknowledgment. +* `void onMessageNack(Message++<++?++>++ message, Throwable failure)` : Called after message negative-acknowledgment. + +[NOTE] +==== + +If you are willing to adapt an incoming message payload to fit a consuming method receiving type, +you can use xref:concepts/converters.adoc[`MessageConverter`]s. + +==== + +=== `OutgoingInterceptor` + +To provide an outgoing interceptor implement a bean exposing the interface link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/io/smallrye/reactive/messaging/OutgoingInterceptor.html[`OutgoingInterceptor`], qualified with a `@Identifier` with the channel name to intercept. + +[source,java] +---- +include::{examples-dir}/interceptors/MyOutgoingInterceptor.java[indent=0] +---- + +An `OutgoingInterceptor` can implement these three methods: + +* `Message++<++?++>++ beforeMessageSend(Message++<++?++>++ message)` : Called before passing the message to the outgoing connector for transmission. +The message can be altered by returning a new message from this method. +* `void onMessageAck(Message++<++?++>++ message)` : Called after message acknowledgment. +This callback can access `OutgoingMessageMetadata` which will hold the result of the message transmission to the broker, if supported by the connector. This is only supported by MQTT and Kafka connectors. +* `void onMessageNack(Message++<++?++>++ message, Throwable failure)` : Called after message negative-acknowledgment. diff --git a/content/docs/smallrye-reactive-messaging/4/concepts/emitter.adoc b/content/docs/smallrye-reactive-messaging/4/concepts/emitter.adoc new file mode 100644 index 000000000..a59761323 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/concepts/emitter.adoc @@ -0,0 +1,270 @@ += Emitter and Channels +:version-root: ../ +include::{version-root}_attributes.adoc[] + +It is not rare to combine in a single application imperative parts +(Jax-RS, regular CDI _beans_) and reactive parts (_beans_ with +`@Incoming` and `@Outgoing` annotations). In these case, it’s often +required to send _messages_ from the imperative part to the reactive +part. In other words, send messages to channels handled by reactive +messaging and how can you retrieve messages. + +== Emitter and @Channel + +To send _things_ (payload or `Message`) from imperative code to a +specific channel you need to use: + +[arabic] +. the link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/org/eclipse/microprofile/reactive/messaging/Channel.html[`org.eclipse.microprofile.reactive.messaging.Channel`] annotations +. the link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/org/eclipse/microprofile/reactive/messaging/Emitter.html[`org.eclipse.microprofile.reactive.messaging.Emitter`] type + +The `@Channel` lets you indicate to which channel you are going to send +your payloads or messages. The `Emitter` is the object to use to send +these payloads or messages. + +[source,java] +---- +include::{examples-dir}/emitter/MyImperativeBean.java[tags=intro,indent=0] +---- + +The `Emitter` class takes a type parameter. It’s the type of payload. +Even if you want to send `Messages`, the type is the payload type. + +[IMPORTANT] +==== + +You must have a `@Incoming("prices")` somewhere in your application +(meaning a method consuming messages transiting on the channel +`prices`), or an outbound connector configured to manage the `prices` +channel (`mp.messaging.outgoing.prices...`) + +==== + +== Sending payloads + +Sending payloads is done as follows: + +[source,java] +---- +include::{examples-dir}/emitter/EmitterExamples.java[tags=payload,indent=0] +---- + +When sending a payload, the emitter returns a `CompletionStage`. This +`CompletionStage` gets completed once the message created from the +payload is acknowledged: + +[source,java] +---- +include::{examples-dir}/emitter/EmitterExamples.java[tags=cs,indent=0] +---- + +If the processing fails, the `CompletionStage` gets completed +exceptionally (with the reason of the nack). + +== Sending messages + +You can also send `Messages`: + +[source,java] +---- +include::{examples-dir}/emitter/EmitterExamples.java[tags=message,indent=0] +---- + +When sending a `Message`, the emitter does not return a +`CompletionStage`, but you can pass the ack/nack callback, and be called +when the message is acked/nacked. + +[source,java] +---- +include::{examples-dir}/emitter/EmitterExamples.java[tags=message-ack,indent=0] +---- + +Sending messages also let you pass metadata. + +[source,java] +---- +include::{examples-dir}/emitter/EmitterExamples.java[tags=message-meta,indent=0] +---- + +Metadata can be used to propagate some context objects with the message. + +== Overflow management + +When sending messages from imperative code to reactive code, you must be +aware of back-pressure. Indeed, messages sent using the emitter and +stored in a _queue_. If the consumer does not process the messages +quickly enough, this queue can become a memory hog and you may even run +out of memory. + +To control what need to happen when the queue becomes out of control, +use the link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/org/eclipse/microprofile/reactive/messaging/OnOverflow.html[`OnOverflow`] annotation. `@OnOverflow` lets you configure: + +* the maximum size of the queue (default is 256) +* what needs to happen when this size is reached (fail, drop...) + +[source,java] +---- +include::{examples-dir}/emitter/EmitterExamples.java[tags=overflow,indent=0] +---- + +The supported strategies are: + +* `OnOverflow.Strategy.BUFFER` - use a buffer to store the elements +until they are consumed. If the buffer is full, a failure is +propagated (and the thread using the emitted gets an exception) +* `OnOverflow.Strategy.UNBOUNDED++_++BUFFER` - use an unbounded buffer to +store the elements +* `OnOverflow.Strategy.DROP` - drops the most recent value if the +downstream can’t keep up. It means that new value emitted by the +emitter are ignored. +* `OnOverflow.Strategy.FAIL` - propagates a failure in case the +downstream can’t keep up. +* `OnOverflow.Strategy.LATEST` - keeps only the latest value, dropping +any previous value if the downstream can’t keep up. +* `OnOverflow.Strategy.NONE` - ignore the back-pressure signals +letting the downstream consumer to implement a strategy. + +=== Defensive emission + +Having an emitter injected into your code does not guarantee that +someone is ready to consume the message. For example, a subscriber may +be connecting to a remote broker. If there are no subscribers, using the +`send` method will throw an exception. + +The `emitter.hasRequests()` method indicates that a subscriber +subscribes to the channel and requested items. So, you can wrap your +emission with: + +[source,java] +---- +if (emitter.hasRequests()) { + emitter.send("hello"); +} +---- + +If you use the `OnOverflow.Strategy.DROP`, you can use the `send` method +even with no subscribers nor demands. The message will be nacked +immediately. + +== Retrieving channels + +You can use the `@Channel` annotation to inject in your bean the +underlying stream. Note that in this case, you will be responsible for +the subscription: + +[source,java] +---- +include::{examples-dir}/emitter/ChannelExamples.java[tags=channel,indent=0] +---- + +[IMPORTANT] +==== + +You must have a `@Outgoing("my-channel")` somewhere in your application +(meaning a method generating messages transiting on the channel +`my-channel`), or an inbound connector configured to manage the `prices` +channel (`mp.messaging.incoming.prices...`) + +==== + +Injected _channels_ merge all the matching _outgoing_ - so if you have +multiple `@Outgoing("out")`, `@Inject @Channel("out")` gets all the +messages. + +If your injected _channel_ receives _payloads_ (`Multi++<++T++>++`), it +acknowledges the message automatically, and support multiple +subscribers. If you injected _channel_ receives `Message` +(`Multi++<++Message++<++T++>>++`), you will be responsible for the acknowledgement +and broadcasting. + +== Emitter and @Broadcast + +When using an `Emitter`, you can now `@Broadcast` what is emitted to all +subscribers. + +Here is an example of emitting a price with two methods marked +`@Incoming` to receive the broadcast: + +[source,java] +---- +include::{examples-dir}/emitter/EmitterExamples.java[tags=broadcast,indent=0] +---- + +For more details see xref:concepts/broadcast.adoc[@Broadcast] +documentation. + +== Mutiny Emitter + +If you prefer to utilize `Uni` in all your code, there is now a +`MutinyEmitter` that will return `Uni++<++Void++>++` instead of `void`. + +[source,java] +---- +include::{examples-dir}/emitter/MutinyExamples.java[tags=uni,indent=0] +---- + +There’s also the ability to block on sending the event to the emitter. +It will only return from the method when the event is acknowledged, or +nacked, by the receiver: + +[source,java] +---- +include::{examples-dir}/emitter/MutinyExamples.java[tags=uni-await,indent=0] +---- + +And if you don’t need to worry about the success or failure of sending +an event, you can `sendAndForget`: + +[source,java] +---- +include::{examples-dir}/emitter/MutinyExamples.java[tags=uni-forget,indent=0] +---- + +== Custom Emitter Implementations + +[WARNING] +.Experimental +==== + +Custom emitter implementations is an experimental feature. + +==== + +`Emitter` and `MutinyEmitter` are two implementations of the emitter concept, +where imperative code in your application can send messages to Reactive Messaging channels. + +With `EmitterFactory` it is possible to provide custom implementations, and application facing emitter interfaces. + +In the following example, the injectable custom emitter interface is `CustomEmitter`, +and it is implemented by `CustomEmitterImpl`: + +[source,java] +---- +include::{examples-dir}/emitter/EmitterFactoryExamples.java[tags=custom-emitter-declaration,indent=0] +---- + +Note that `CustomEmitter` interface extends `EmitterType`, which is a marker interface for discovering custom emitter types. +Also, `CustomEmitterImpl` implements the `MessagePublisherProvider`, which is used by the framework to transform this emitter to a channel. + +Then we need to provide an implementation of the `EmitterFactory` interface: + +[source,java] +---- +include::{examples-dir}/emitter/EmitterFactoryExamples.java[tags=custom-emitter-factory,indent=0] +---- + +The `CustomEmitterFactory` is a CDI managed bean, which implements the `EmitterFactory`. +It is qualified with `EmitterFactoryFor` annotation which is configured with the emitter interface `CustomEmitter` that this factory provides. + +Smallrye Reactive Messaging discovers the emitter factory during the CDI deployment validation +and verifies that custom emitters used by the application have corresponding emitter factories. +It'll use the emitter factory to create the emitter implementation and will register the implementation into the `ChannelRegistry`. + +Note that the `CustomEmitterFactory` also uses the `ChannelRegistry` and provides the custom emitter with `@Produces`. + +Finally, the application can inject and use the `CustomEmitter` as a normal emitter channel: + +[source,java] +---- +include::{examples-dir}/emitter/EmitterFactoryExamples.java[tags=custom-emitter-usage,indent=0] +---- diff --git a/content/docs/smallrye-reactive-messaging/4/concepts/generic-payloads.adoc b/content/docs/smallrye-reactive-messaging/4/concepts/generic-payloads.adoc new file mode 100644 index 000000000..fd7193fd7 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/concepts/generic-payloads.adoc @@ -0,0 +1,55 @@ += Generic Payloads +:version-root: ../ +include::{version-root}_attributes.adoc[] + +[WARNING] +.Experimental +==== + +Generic payloads are an experimental feature and the API is subject to change. + +==== + +When using reactive messaging, `Message` flow in your system +each message has a payload but can also contain _metadata_, as explained in xref:concepts/concepts.adoc#messages-payload-metadata[Messages, Payload, Metadata]. +The metadata can hold information, for example in an outgoing channel, additional properties of the outgoing message to be sent to the broker. + +It is sometimes preferable to continue using the xref:concepts/model.adoc#messages-vs-payloads[payload signatures], +and also being able to attach metadata. +Using `GenericPayload` allows customizing metadata when handling payloads in SmallRye Reactive Messaging `@Incoming` and `@Outgoing` methods. +`GenericPayload` is a wrapper type, like the `Message`, containing a payload and metadata, +without requiring handling acknowledgments manually. + +[source,java] +---- +include::{examples-dir}/genericpayload/GenericPayloadExample.java[tags=code,indent=0] +---- + +You can combine generic payloads with xref:concepts/incoming-metadata-injection.adoc[metadata injection] : + +[source,java] +---- +include::{examples-dir}/genericpayload/GenericPayloadExample.java[tags=injection,indent=0] +---- + +Note that the metadata provided with the outgoing generic payload is merged with the incoming message metadata. + +`GenericPayload` can also be used with emitters to attach metadata without dealing with `Message` directly: + +[source,java] +---- +include::{examples-dir}/genericpayload/GenericPayloadExample.java[tags=emitter,indent=0] +---- + +The inner payload is extracted and the metadata from the `GenericPayload` is merged into the outgoing message. +Note that existing metadata entries are not overwritten by the `GenericPayload` metadata. +If a metadata entry of the same type already exists, it is preserved unless the metadata class implements `MergeableMetadata`. + +[WARNING] +.Limitations +==== + +While `GenericPayload++<++T++>++` can be used as an incoming payload type, +xref:concepts/converters.adoc[message converters] are not applied to the payload type `T`. + +==== diff --git a/content/docs/smallrye-reactive-messaging/4/concepts/incoming-concurrency.adoc b/content/docs/smallrye-reactive-messaging/4/concepts/incoming-concurrency.adoc new file mode 100644 index 000000000..058eb070f --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/concepts/incoming-concurrency.adoc @@ -0,0 +1,73 @@ += Incoming Channel Concurrency +:version-root: ../ +include::{version-root}_attributes.adoc[] + +[WARNING] +.Experimental +==== + +Incoming channel `concurrency` config is an experimental feature. + +==== + +The `concurrency` attribute for incoming channels provides a mechanism to enable concurrent non-blocking processing of incoming messages. +When applied to a channel, this attribute specifies the number of copies of that channel to be created and wired to the processing method, +allowing multiple messages to be processed concurrently. + +For example, concurrency configuration for a Kafka incoming channel the configuration will look like: + +[source,properties] +---- +mp.messaging.incoming.my-channel.connector=smallrye-kafka +mp.messaging.incoming.my-channel.topic=orders +mp.messaging.incoming.my-channel.value.deserializer=org.apache.kafka.common.serialization.DoubleDeserializer +mp.messaging.incoming.my-channel.concurrency=4 +---- + +In this example, there will be 4 copies of the `my-channel` running concurrently, with distinctive internal channel names, +`my-channel$1`, `my-channel$2`, etc. but all registered with the name `my-channel` to the `ChannelRegistry`. + +[NOTE] +.Kafka connector `partitions` +==== + +This is essentially very similar to the Kafka connector `partitions` configuration, but addresses some its limitations. +Using `partitions` config in Kafka connector, channels are merged into the downstream message processor +(method annotated with `@Incoming` or an injected channel) which is therefore called sequentially. +This prevents concurrently processing messages from multiple partitions. + +The `concurrency` mechanism effectively allows polling Kafka partitions from separate clients +and concurrently processing records while preserving the in-partition order. + +==== + +Copy channels inherit all configuration attributes of the main channel config. +Per-copy channel attributes can be configured separately using the `$` separated channel names: `mp.messaging.incoming.my-channel$1.attribute`. + +For example, the following AMQP 1.0 channel defines 3 channels each with a different selector: + +[source,properties] +---- +mp.messaging.incoming.data.connector=smallrye-amqp +mp.messaging.incoming.data.address=address +mp.messaging.incoming.data.durable=false +mp.messaging.incoming.data.concurrency=3 +mp.messaging.incoming.data$1.selector=x='foo' +mp.messaging.incoming.data$2.selector=x='bar' +mp.messaging.incoming.data$3.selector=x='baz' +---- + +While the `concurrency` attribute is applicable to channels of any connector type, +the channel implementation may need to take this configuration into account and adjust the threading accordingly. +Connectors based on Vert.x event loop create a new event loop context per copy-channel to dispatch messages on distinct contexts. + +[IMPORTANT] +.Non-blocking processing +==== + +Note that while this allows concurrent processing, messages are still dispatched on Vert.x event loop threads, and should not be blocked. + +==== + +Otherwise, connectors treat copy channels as independent channels. +For example, health check reports are registered separately for each copy-channel. diff --git a/content/docs/smallrye-reactive-messaging/4/concepts/incoming-metadata-injection.adoc b/content/docs/smallrye-reactive-messaging/4/concepts/incoming-metadata-injection.adoc new file mode 100644 index 000000000..fb27198b2 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/concepts/incoming-metadata-injection.adoc @@ -0,0 +1,29 @@ += Incoming Metadata Injection +:version-root: ../ +include::{version-root}_attributes.adoc[] + +[WARNING] +.Experimental +==== + +Metadata injection is an experimental feature. + +==== + +When using reactive messaging, `Message` flow in your system. +Each message has a payload but can also contain _metadata_, as explained in xref:concepts/concepts.adoc#messages-payload-metadata[Messages, Payload, Metadata]. + +You can inject the incoming message metadata as a parameter depending on the method signature. +*Only methods receiving the payload as a parameter can also receive the metadata.* +For example, `@Incoming("in") void consume(String s)` can also receive the metadata using the following signature: +`@Incoming("in") void consume(String s, MyMetadata m)`. +Note that the payload must be the first parameter. + +The metadata can be injected using the class of the metadata to inject. +In this case, the method is invoked with `null` if the metadata is missing. +You can also inject the metadata using `Optional++<++MetadataClass++>++,` which would be empty if the incoming message does not contain metadata of type `MetadataClass.` + +[source,java] +---- +include::{examples-dir}/incomings/MetadataInjectionExample.java[tags=code,indent=0] +---- diff --git a/content/docs/smallrye-reactive-messaging/4/concepts/incomings.adoc b/content/docs/smallrye-reactive-messaging/4/concepts/incomings.adoc new file mode 100644 index 000000000..8fc179d3a --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/concepts/incomings.adoc @@ -0,0 +1,20 @@ += Multiple Incoming Channels +:version-root: ../ +include::{version-root}_attributes.adoc[] + +[WARNING] +.Experimental +==== + +Multiple `@Incomings` is an experimental feature. + +==== + +The `@Incoming` annotation is repeatable. It means that the method +receives the messages transiting on every listed channels, in no +specific order: + +[source,java] +---- +include::{examples-dir}/incomings/IncomingsExamples.java[tags=code,indent=0] +---- diff --git a/content/docs/smallrye-reactive-messaging/4/concepts/keyed-multi.adoc b/content/docs/smallrye-reactive-messaging/4/concepts/keyed-multi.adoc new file mode 100644 index 000000000..cc1824499 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/concepts/keyed-multi.adoc @@ -0,0 +1,52 @@ += Using `KeyedMulti` +:version-root: ../ +include::{version-root}_attributes.adoc[] + +[WARNING] +.Experimental +==== + +`KeyedMulti` is an experimental feature. + +==== + +When implementing a data streaming application, it's common to handle messages partitioned using a _key_. +In this case, your stream manipulation often has to 1) group by key and 2) do the manipulation. +Reactive Messaging can do the first step for you and reduce the code complexity. +To do this, it injects `io.smallrye.reactive.messaging.keyed.KeyedMulti` in your method instead of a _bare_ `Multi`. + +For example, imagine the following stream, represented as `key:value`: `"a:1", "b:1", "a:2", "b:2", "b:3"...` +Then, let's consider the following method: + +[source,java] +---- +include::{examples-dir}/keyed/KeyedMultiExample.java[tags=code,indent=0] +---- + +Reactive Messaging automatically extracts the key and value from the incoming stream and invokes the method for each key. +The received `KeyedMulti` represent the stream for each key. +The `key()` method returns the extracted key. + +The key and value can be extracted from the payload but also (and often) from the message's metadata. + +When using Kafka, it automatically extracts the key/value from the Kafka records. +In the other cases, or if you need custom extraction, you can implement your own `io.smallrye.reactive.messaging.keyed.KeyValueExtractor`. +Implementations are exposed `ApplicationScoped` beans, and are used to extract the key and value. +The following implementation extracts the key and value from payloads structured as "key:value": + +[source,java] +---- +include::{examples-dir}/keyed/KeyValueExtractorFromPayload.java[tags=code,indent=0] +---- + +The extractor selection uses the `canExtract` method. +When multiple extractors are available, you can implement the `getPriority()` method to give a lower priority. +Default extractors have the priority 100. +So, if you have a custom extractor with the priority 99, it will be used (if it replies `true` to the `canExtract` call). +In addition, you can use the `io.smallrye.reactive.messaging.keyed.Keyed` annotation to indicate the class of the extractor to use. +The extractor must still be a CDI bean, but the `canExtract` method is not called, and priority does not matter: + +[source,java] +---- +include::{examples-dir}/keyed/KeyedExample.java[tags=code,indent=0] +---- diff --git a/content/docs/smallrye-reactive-messaging/4/concepts/logging.adoc b/content/docs/smallrye-reactive-messaging/4/concepts/logging.adoc new file mode 100644 index 000000000..2a8fc8df3 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/concepts/logging.adoc @@ -0,0 +1,269 @@ += Logging +:version-root: ../ +include::{version-root}_attributes.adoc[] + +SmallRye Reactive Messaging uses https://github.com/jboss-logging[JBoss +Logging] as logging API. This section +explains how to configure the loggers for various logging backends. + +[TIP] +==== + +If you are developing SmallRye Reactive Messaging and wonder about how +the logs are managed, it uses https://jboss-logging.github.io/jboss-logging-tools/#introduction[JBoss Logging +Tools]. + +==== + +== Logging Backends + +SmallRye Reactive Messaging uses the JBoss Logging library to write +messages to a log file. This library is a logging _bridge_ that +integrates different log frameworks. You can decide which of the +following frameworks you want to use for your application: + +* JBoss LogManager (`jboss`) +* Log4j 2 (`log4j2`) +* Log4j 1 (`log4j`) +* Slf4j (`slf4j`) +* JDK logging (`jul`) + +You only need to add the chosen framework to the classpath, and the +JBoss Logging library will pick it up. If there are multiple frameworks +available on the classpath, it picks the first found (in the order from +the list). Alternatively, you can set the `org.jboss.logging.provider` +system property is one of the values given above. + +The concepts and log categories are the same for all frameworks. +However, the format of the configuration file and the names of the log +levels differ. Check the documentation of your logging library to find +out which _dependencies_ are required, the exact name of the log levels, +and where the configuration should be written. + +== Log Categories + +As all applications and frameworks, SmallRye Reactive Messaging writes +log messages in different categories and log levels. The categories +group messages from specific connectors, classes or components. The +following table shows the essential log categories used by SmallRye +Reactive Messaging: + +[width="100%",cols="36%,64%",options="header",] +|=== +|Category |Description +|`io.smallrye.reactive.messaging` |This category contains all the messages written by SmallRye Reactive Messaging. +|`io.smallrye.reactive.messaging.provider` |This category contains all the messages generated by the _core_ (provider). +|`io.smallrye.reactive.messaging.kafka` |This category contains all the messages generated by the Kafka Connector. +|`io.smallrye.reactive.messaging.amqp` |This category contains all the messages generated by the AMQP Connector. +|`io.smallrye.reactive.messaging.jms` |This category contains all the messages generated by the JMS Connector. +|`io.smallrye.reactive.messaging.camel` |This category contains all the messages generated by the Camel Connector. +|`io.smallrye.reactive.messaging.mqtt` |This category contains all the messages generated by the MQTT (Client) Connector. +|`io.smallrye.reactive.messaging.mqtt-server` |This category contains all the messages generated by the MQTT (Server) Connector. +|=== + +The names of the log levels are defined by your logging framework and +determine the amount and granularity of the log messages. You can assign +a log level to each category. If you do not specify a specific +category’s log level, it will inherit the level from its parent +category. Thus, setting the log level of +`io.smallrye.reactive.messaging` influences every loggers from SmallRye +Reactive Messaging. + +== Message Code + +Each message has an identifier code. They are all prefixed with `SRMSG`, +followed with the numeric code. + +In the following output, the code is `SRMSG00229`: + +.... +[2020-06-15 13:35:07] [INFO ] SRMSG00229: Channel manager initializing... +.... + +== Recommended logging configurations + +=== Development + +==== Log4J 1 + +_log4j.properties_ + +[source,properties] +---- +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.Target=System.out +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d{HH:mm:ss,SSS} %-5p [%c] - %m%n + +log4j.rootLogger=info, stdout +log4j.logger.io.smallrye.reactive.messaging=info +log4j.logger.org.jboss.weld=warn +---- + +==== Log4J 2 + +_log4j2.xml_ + +[source,xml] +---- + + + PropertiesConfiguration + + + + + + [%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %c{1} - %msg%n + > + + + + + + + + + + + + + + + +---- + +==== JDK (JUL) + +_logging.properties_ + +[source,properties] +---- +handlers=java.util.logging.ConsoleHandler + +java.util.logging.ConsoleHandler.level=FINEST +java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter +java.util.logging.SimpleFormatter.format=[%1$tF %1$tT] [%4$-7s] %5$s %n + +.level=INFO +io.smallrye.reactive.messaging.level=INFO +org.jboss.weld.level=WARNING +---- + +==== LogBack via SLF4J++*++ + +_logback.xml_ + +[source,xml] +---- + + + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + +---- + +=== Production + +==== Log4J 1 + +_log4j.properties_ + +[source,properties] +---- +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.Target=System.out +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d{HH:mm:ss,SSS} %-5p [%c] - %m%n + +log4j.rootLogger=info, stdout +log4j.logger.io.smallrye.reactive.messaging=warn +log4j.logger.org.jboss.weld=error +---- + +==== Log4J 2 + +_log4j2.xml_ + +[source,xml] +---- + + + PropertiesConfiguration + + + + + + [%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %c{1} - %msg%n + > + + + + + + + + + + + + + + + +---- + +==== JDK (JUL) + +_logging.properties_ + +[source,properties] +---- +handlers=java.util.logging.ConsoleHandler + +java.util.logging.ConsoleHandler.level=INFO +java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter +java.util.logging.SimpleFormatter.format=[%1$tF %1$tT] [%4$-7s] %5$s %n + +.level=INFO +io.smallrye.reactive.messaging.level=WARNING +org.jboss.weld.level=SEVERE +---- + +_logback.xml_ + +[source,xml] +---- + + + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + +---- diff --git a/content/docs/smallrye-reactive-messaging/4/concepts/merge.adoc b/content/docs/smallrye-reactive-messaging/4/concepts/merge.adoc new file mode 100644 index 000000000..d7bd6cbd7 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/concepts/merge.adoc @@ -0,0 +1,47 @@ += Merge channels +:version-root: ../ +include::{version-root}_attributes.adoc[] + +[WARNING] +.Experimental +==== + +`@Merge` is an experimental feature. + +==== + +By default, messages transiting in a channel can arise from a single +producer. Having multiple producers is considered erroneous and is +reported at deployment time. + +The link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/io/smallrye/reactive/messaging/annotations/Merge.html[`Merge`] +annotation changes this behavior and indicates that a channel can have +multiple producers. `@Merge` must be used with the `@Incoming` +annotation: + +[source,java] +---- +include::{examples-dir}/merge/MergeExamples.java[tags=chain,indent=0] +---- + +In the previous example, the consumer gets all the messages (from both +producers). + +The `@Merge` annotation allows configuring how the incoming messages +(from the different producers) are merged into the channel. The `mode` +attribute allows configuring this behavior: + +* `ONE` picks a single producer, discarding the other producer; +* `MERGE` (default) gets all the messages as they come, without any +defined order. Messages from different producers may be interleaved. +* `CONCAT` concatenates the producers. The messages from one producer +are received until the messages from other producers are received. + +[NOTE] +==== + +Outbound connectors also support a `merge` attribute that allows +consuming the messages to multiple upstreams. It will dispatch all the +received messages. + +==== diff --git a/content/docs/smallrye-reactive-messaging/4/concepts/message-context.adoc b/content/docs/smallrye-reactive-messaging/4/concepts/message-context.adoc new file mode 100644 index 000000000..411587683 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/concepts/message-context.adoc @@ -0,0 +1,83 @@ += Message Contexts +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Message context provides a way to propagate data along the processing of a message. +It can be used to propagate message specific objects in an implicit manner and be able to retrieve them later, such as the user, session or transaction. + +[IMPORTANT] +==== + +Message contexts are only support by Kafka, AMQP, RabbitMQ and MQTT connectors. + +==== + +[NOTE] +==== + +Message context support is an experimental and SmallRye only feature. + +==== + +== What's a message context + +A message context is execution context on which a message is processed. +Each stage of the processing is going to use the same execution context. +Thus, it allows storing _data_ which can later be restored. +For example, you can imagine storing some authentication (`User` in the following example) data in one part of your processing and restore it in a later stage. + +[source,java] +---- +@Incoming("data") +@Outgoing("process") +public Message process(Message input) { + // Extract some data from the message and store it in the context + User user = ...; + // Store the extracted data into the message context. + ContextLocals.put("user", user); + record input; +} + +@Incoming("process") +@Outgoing("after-process") +public String handle(String payload) { + // You can retrieve the store data using + User user = ContextLocals.get("user", null); + + // ... + return payload; +} +---- + +The Message context is also available when using blocking or asynchronous stages (stage returning `Uni` or `CompletionStage`) + +== The difference with metadata + +Message metadata can be used to provide a similar feature. +However, it requires using `Messages` which can be inconvenient (need to handle the acknowledgement manually). +Message Contexts provide a simpler API, closer to a _Message CDI scope_: you can save data, and restore it later. +The implicit propagation avoid having to deal with `Messages`. + +== Supported signatures + +Message context works with: + +* methods consuming or producing `Messages`, `Uni++<++Message++<++T++>>++` and `CompletionStage++<++Message++<++T++>>++` +* methods consuming or producing payloads, `Uni++<++Payload++>++` and `CompletionStage++<++Payload++>++`. +* blocking and non-blocking methods + +However, message context are *NOT* enforced when using methods consuming or producing: + +* `Multi`, `Flow.Publisher`, `Publisher` and `PublisherBuilder` +* `Subscriber`, `Flow.Subscriber`, and `SubscriberBuilder` +* `Processor`, `Flow.Processor`, and `ProcessorBuilder` + +== Under the hood + +Under the hood, the message context feature uses Vert.x _duplicated contexts_. +A duplicated context is a view of the "root" (event loop) context, which is restored at each stage of the message processing. + +Each time that a compatible connector receives a message from a broker, it creates a new _duplicated context_ and attaches it to the message. +So the context is stored in the metadata of the message. + +When the message is processed, SmallRye Reactive Messaging makes sure that this processing is executed on the stored duplicated context. diff --git a/content/docs/smallrye-reactive-messaging/4/concepts/model.adoc b/content/docs/smallrye-reactive-messaging/4/concepts/model.adoc new file mode 100644 index 000000000..c7adc4e70 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/concepts/model.adoc @@ -0,0 +1,422 @@ += Development Model +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Reactive Messaging proposes a CDI-based programming model to implement event-driven applications. +Following the CDI principles, _beans_ are forming the main building block of your application. +Reactive Messaging provides a set of annotations and types to implement beans that generate, consume or process messages. + +== @Incoming and @Outgoing + +Reactive Messaging provides two main annotations: + +* link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/org/eclipse/microprofile/reactive/messaging/Incoming.html[`org.eclipse.microprofile.reactive.messaging.Incoming`] - indicates the consumed channel +* link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/org/eclipse/microprofile/reactive/messaging/Outgoing.html[`org.eclipse.microprofile.reactive.messaging.Outgoing`] - indicates the populated channel + +These annotations are used on _methods_: + +[source,java] +---- +include::{examples-dir}/beans/MessageProcessingBean.java[indent=0] +---- + +[NOTE] +==== + +Reactive Messaging beans can either be in the _application_ scope (`@ApplicationScoped`) or dependent scope (`@Dependent`). + +==== + +Manipulating messages can be cumbersome. +When you are only interested in the payload, you can use the following syntax: The following code is equivalent to the snippet from above: + +[source,java] +---- +include::{examples-dir}/beans/PayloadProcessingBean.java[indent=0] +---- + +[IMPORTANT] +==== + +You should not call methods annotated with `@Incoming` and/or +`@Outgoing` directly from your code. They are invoked by the framework. +Having user code invoking them would not have the expected outcome. + +==== + +SmallRye Reactive Messaging automatically binds matching `@Outgoing` to +`@Incoming` to form a chain: + +.A chain of components +image::images/chain.png[A chain of components] + +If we consider the following code: + +[source,java] +---- +include::{examples-dir}/beans/Chain.java[tags=chain,indent=0] +---- + +It would generate the following chain: + +[source,text] +---- +generate --> [ source ] --> process --> [ sink ] --> consume +---- + +Methods annotated with `@Incoming` or `@Outgoing` don’t have to be in the same _bean_ (_class_). +You can distribute them among a set of beans. +Remote interactions are also possible when using connectors. + +Methods annotated with: + +* only `@Outgoing` are used to generate messages or payloads +* only `@Incoming` are used to consume messages or payloads +* both `@Incoming` and `@Outgoing` are used to process messages or payloads; or transform the stream + +== Creating messages + +Messages are envelopes around payload. +They are the vehicle. +While manipulating payload is convenient, messages let you add metadata, handle acknowledgement... + +Creating `Messages` is done using the link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/io/eclipse/microprofile/reactive/messaging/Message.html[`Message`] interface directly: + +[source,java] +---- +include::{examples-dir}/messages/MessageExamples.java[tags=creation,indent=0] +---- + +Messages accept `null` as payload. +Channels connected to outbound connectors interpret messages with `null` payload differently depending on the technology. + +[source,java] +---- +include::{examples-dir}/messages/MessageExamples.java[tags=null-payload,indent=0] +---- + +You can also create new instance of `Message` from an existing one: + +[source,java] +---- +include::{examples-dir}/messages/MessageExamples.java[tags=copy,indent=0] +---- + +[WARNING] +.Acknowledgement? +==== + +Acknowledgement is an important part of messaging systems. This will be covered in the xref:concepts/acknowledgement.adoc[acknowledgement] section. + +==== + +[NOTE] +.Connector Metadata +==== + +Most connectors are providing metadata to let you extract technical +details about the message, but also customize the outbound dispatching. + +==== + +== Messages vs. Payloads + +Reactive messaging offers flexibility when it comes to handling messages and their acknowledgements. +The application developer can choose to finely handle acknowledgements per-message basis, by handling the +`Message`-based signatures. Otherwise, when handling payloads, acknowledgements +(and negative-acknowledgements) are handled by the framework. +The following sections in this documentation detail both development models. + +While being the easier development model, in the past using payload-based signatures did not allow associating connector-specific metadata, +making the `Message`-based signatures the de-facto choice even for the most common scenarios. +This lead to using the connector custom-message implementation types, +such as `IncomingKafkaRecord`, `KafkaRecord` or `IncomingRabbitMQMessage`, +as a convenience for accessing connector-specific metadata. + +Not only this forces to handle acknowledgements manually, +it also doesn't allow for incoming or outgoing `Messages` to be xref:concepts/decorators.adoc#intercepting-incoming-and-outgoing-messages[intercepted] +or xref:concepts/observability.adoc[observed]. + +[WARNING] +.Custom `Message` types & Message interception +==== + +Custom `Message` types such as `IncomingKafkaRecord`, `KafkaRecord` or `IncomingRabbitMQMessage` +are not compatible with features intercepting messages. +Therefore, it is no longer recommended to use custom `Message` implementations in consumptions methods. +Instead, you can either use the generic `Message` type and access specific metadata, +or use the payload with xref:concepts/incoming-metadata-injection.adoc[metadata injection] + +==== + +Since xref:concepts/incoming-metadata-injection.adoc[incoming metadata injection] and xref:concepts/generic-payloads.adoc[generic payloads] +features added to SmallRye Reactive Messaging, +it is easier to use payload signatures and benefit from acknowledgement handling and still access connector-specific metadata. + +== Generating Messages + +To produce messages to a channel, you need to use the `@Outgoing` +annotation. This annotation takes a single parameter: the name of the +populated channel. + +=== Generating messages synchronously + +You can generate messages synchronously. In this case, the method is +called for every _request_ from the downstream: + +[source,java] +---- +include::{examples-dir}/generation/GenerationExamples.java[tags=message-sync,indent=0] +---- + +[NOTE] +.Requests? +==== + +Reactive Messaging connects components to build a reactive stream. +In a reactive stream, the emissions are controlled by the consumer +(downstream) indicating to the publisher (upstream) how many items it +can consume. With this protocol, the consumers are never flooded. + +==== + +=== Generating messages using CompletionStage + +You can also return a `CompletionStage` / `CompletableFuture`. +In this case, Reactive Messaging waits until the `CompletionStage` gets completed before calling it again. + +For instance, this signature is useful to poll messages from a source using an asynchronous client: + +[source,java] +---- +include::{examples-dir}/generation/GenerationExamples.java[tags=message-cs,indent=0] +---- + +=== Generating messages using Uni + +You can also return a https://smallrye.io/smallrye-mutiny/#_uni_and_multi[Uni] instance. +In this case, Reactive Messaging waits until the `Uni` emits its item before calling it again. + +This signature is useful when integrating asynchronous clients providing a Mutiny API. + +[source,java] +---- +include::{examples-dir}/generation/GenerationExamples.java[tags=message-uni,indent=0] +---- + +=== Generating Reactive Streams of messages + +Instead of producing the message one by one, you can return the stream +directly. If you have a data source producing Reactive Streams +`Publisher` (or sub-types, such as `Multi`), this is the signature you are looking for: + +[source,java] +---- +include::{examples-dir}/generation/GenerationExamples.java[tags=message-stream,indent=0] +---- + +In this case, the method is called once to retrieve the `Publisher`. + +== Generating Payloads + +Instead of `Message`, you can produce _payloads_. In this case, Reactive +Messaging produces a simple message from the _payload_ using +`Message.of`. + +=== Generating payload synchronously + +You can produce _payloads_ synchronously. The framework calls the method +upon request and create `Messages` around the produced payloads. + +[source,java] +---- +include::{examples-dir}/generation/GenerationExamples.java[tags=payload-sync,indent=0] +---- + +=== Generating payload using CompletionStage + +You can also return `CompletionStage` or `CompletableFuture`. For +example, if you have an asynchronous client returning `CompletionStage`, +you can use it as follows, to poll the data one by one: + +[source,java] +---- +include::{examples-dir}/generation/GenerationExamples.java[tags=payload-cs,indent=0] +---- + +=== Generating payload by producing Unis + +You can also return a `Uni` if you have a client using Mutiny types: + +[source,java] +---- +include::{examples-dir}/generation/GenerationExamples.java[tags=payload-uni,indent=0] +---- + +=== Generating Reactive Streams of payloads + +Finally, you can return a `Publisher` (or a sub-type such as a `Multi`): + +[source,java] +---- +include::{examples-dir}/generation/GenerationExamples.java[tags=payload-stream,indent=0] +---- + +In this case, Reactive Messaging calls the method only once to retrieve +the `Publisher`. + +== Consuming Messages + +To consume messages from a channel, you need to use the `@Incoming` +annotation. This annotation takes a single parameter: the name of the +consumed channel. + +Because `Messages` must be acknowledged, consuming messages requires +returning _asynchronous results_ that would complete when the incoming +message get acknowledged. + +For example, you can receive the `Message`, process it and return the +acknowledgement as result: + +[source,java] +---- +include::{examples-dir}/consumption/ConsumptionExamples.java[tags=message-cs,indent=0]' +---- + +You can also return a `Uni` if you need to implement more complicated +processing: + +[source,java] +---- +include::{examples-dir}/consumption/ConsumptionExamples.java[tags=message-uni,indent=0] +---- + +== Consuming Payloads + +Unlike consuming messages, consuming payloads support both synchronous +and asynchronous consumption. + +For example, you can consume a payload as follows: + +[source,java] +---- +include::{examples-dir}/consumption/ConsumptionExamples.java[tags=payload-sync,indent=0] +---- + +In this case, you don’t need to deal with the acknowledgement yourself. +The framework acknowledges the incoming message (that wrapped the +payload) once your method returns successfully. + +If you need to achieve asynchronous actions, you can return a +`CompletionStage` or a `Uni`: + +[source,java] +---- +include::{examples-dir}/consumption/ConsumptionExamples.java[tags=payload-cs,indent=0] +---- + +[source,java] +---- +include::{examples-dir}/consumption/ConsumptionExamples.java[tags=payload-uni,indent=0] +---- + +In these 2 cases, the framework acknowledges the incoming message when +the returned construct gets _completed_. + +== Processing Messages + +You can process `Message` both synchronously or asynchronously. +This later case is useful when you need to execute an asynchronous action during your processing such as invoking a remote service. + +Do process `Messages` synchronously uses: + +[source,java] +---- +include::{examples-dir}/processing/ProcessingExamples.java[tags=process-message,indent=0] +---- + +This method transforms the `int` payload to a `String`, and wraps it +into a `Message`. + +'''important "Using `Message.withX` methods" +You may be surprised by the usage of `Message.withX` methods. It allows +metadata propagation as the metadata would be copied from the incoming +message and so dispatched to the next method. + +You can also process `Messages` asynchronously: + +[source,java] +---- +include::{examples-dir}/processing/ProcessingExamples.java[tags=process-message-cs,indent=0] +---- + +Or using Mutiny: + +[source,java] +---- +include::{examples-dir}/processing/ProcessingExamples.java[tags=process-message-uni,indent=0] +---- + +In general, you want to create the new `Message` from the incoming one. +It enables metadata propagation and post-acknowledgement. For this, use +the `withX` method from the `Message` class returning a new `Message` +instance but copy the _content_ (metadata, ack/nack...). + +== Processing payloads + +If you don’t need to manipulate the envelope, you can process payload +directly either synchronously or asynchronously: + +[source,java] +---- +include::{examples-dir}/processing/ProcessingExamples.java[tags=process-payload,indent=0] +---- + +[source,java] +---- +include::{examples-dir}/processing/ProcessingExamples.java[tags=process-payload-cs,indent=0] +---- + +[source,java] +---- +include::{examples-dir}/processing/ProcessingExamples.java[tags=process-payload-uni,indent=0] +---- + +[NOTE] +.What about metadata? +==== + +With these methods, the metadata are automatically propagated. + +==== + +== Processing streams + +The previous processing method were taking single `Message` or payload. +Sometimes you need more advanced manipulation. For this, SmallRye +Reactive Messaging lets you process the stream of `Message` or the +stream of payloads directly: + +[source,java] +---- +include::{examples-dir}/processing/StreamExamples.java[tags=processing-stream-message,indent=0] +---- + +[source,java] +---- +include::{examples-dir}/processing/StreamExamples.java[tags=processing-stream-payload,indent=0] +---- + +You can receive either a (Reactive Streams) `Publisher`, a +`PublisherBuilder` or (Mutiny) `Multi`. You can return any subclass of +`Publisher` or a `Publisher` directly. + +[IMPORTANT] +==== + +These signatures do not support metadata propagation. In the case of a +stream of `Message`, you need to propagate the metadata manually. In the +case of a stream of payload, propagation is not supported, and incoming +metadata are lost. + +==== diff --git a/content/docs/smallrye-reactive-messaging/4/concepts/observability.adoc b/content/docs/smallrye-reactive-messaging/4/concepts/observability.adoc new file mode 100644 index 000000000..aa57e8238 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/concepts/observability.adoc @@ -0,0 +1,38 @@ += Observability API +:version-root: ../ +include::{version-root}_attributes.adoc[] + +[IMPORTANT] +==== + +Observability API is experimental and SmallRye only feature. + +==== + +Smallrye Reactive Messaging proposes an observability API that allows to observe messages received and send through inbound and outbound channels. + +For any observation to happen, you need to provide an implementation of the `MessageObservationCollector`, discovered as a CDI-managed bean. + +At wiring time the discovered `MessageObservationCollector` implementation `initObservation` method is called once per channel to initialize the `ObservationContext`. +The default `initObservation` implementation returns a default `ObservationContext` object, +but the collector implementation can provide a custom per-channel `ObservationContext` object that'll hold information necessary for the observation. +The `ObservationContext#complete` method is called each time a message observation is completed – message being acked or nacked. +The collector implementation can decide at initialization time to disable the observation per channel by returning a `null` observation context. + +For each new message, the collector is on `onNewMessage` method with the channel name, the `Message` and the `ObservationContext` object initialized beforehand. +This method can react to the creation of a new message but also is responsible for instantiating and returning a `MessageObservation`. +While custom implementations can augment the observability capability, SmallRye Reactive Messaging provides a default implementation `DefaultMessageObservation`. + +So a simple observability collector can be implemented as such: + +[source,java] +---- +include::{examples-dir}/observability/SimpleMessageObservationCollector.java[indent=0] +---- + +A collector with a custom `ObservationContext` can be implemented as such : + +[source,java] +---- +include::{examples-dir}/observability/ContextMessageObservationCollector.java[indent=0] +---- diff --git a/content/docs/smallrye-reactive-messaging/4/concepts/outgoings.adoc b/content/docs/smallrye-reactive-messaging/4/concepts/outgoings.adoc new file mode 100644 index 000000000..64adca658 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/concepts/outgoings.adoc @@ -0,0 +1,79 @@ += Multiple Outgoing Channels +:version-root: ../ +include::{version-root}_attributes.adoc[] + +[WARNING] +.Experimental +==== + +Multiple `@Outgoings` is an experimental feature. + +==== + +The `@Outgoing` annotation is repeatable. It means that the method +dispatches outgoing messages to multiple listed channels: + +[source,java] +---- +include::{examples-dir}/outgoings/OutgoingsExample.java[tags=code,indent=0] +---- + +The default behaviour is same as the xref:concepts/broadcast.adoc[@Broadcast annotation], +meaning that outbound messages are dispatched to all listed outgoing channels. + +However, different dispatching mechanism can be employed: + +== Selectively dispatching messages using `Targeted` messages + +You can selectively dispatch messages to multiple outgoings by returning +link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/io/smallrye/reactive/messaging/Targeted.html[`Targeted`] : + +[source,java] +---- +include::{examples-dir}/outgoings/TargetedExample.java[tags=code,indent=0] +---- + +In this example, three outgoing channels are declared on the `process` method +but in some condition channel `out3` does not receive any messages. + +[IMPORTANT] +.Coordinated acknowledgements +==== + +`Targeted` return types coordinate acknowledgements between outgoing messages +and the incoming message, therefore the incoming message will be ack'ed only +when all outgoing messages are ack'ed. + +==== + +In cases where you need to consume `Message` and handle metadata propagation +more finely you can use link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/io/smallrye/reactive/messaging/TargetedMessages.html[`TargetedMessages`] +which is a `Message` type: + +[source,java] +---- +include::{examples-dir}/outgoings/TargetedExample.java[tags=targeted-messages,indent=0] +---- + +Note that in this case coordinated acknowledgements is handled explicitly +using link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/io/smallrye/reactive/messaging/Messages.html[`Messages`] utility. + +== Branching outgoing channels with `MultiSplitter` + +In stream transformer processors it can be useful to branch out an incoming +stream into different sub-streams, based on some conditions. + +When consuming a `Multi`, you can use the `Multi.split` +(see https://smallrye.io/smallrye-mutiny/latest/guides/multi-split/[Mutiny documentation]) +operation to define multiple branches. +The stream transformer method with multiple outgoings must return a +link:https://javadoc.io/doc/io.smallrye.reactive/mutiny/{javadoc-version}/io/smallrye/mutiny/operators/multi/split/MultiSplitter.html[`MultiSplitter`]. + +[source,java] +---- +include::{examples-dir}/outgoings/SplitterMultiExample.java[tags=code,indent=0] +---- + +In this case the number of outgoing channels must match the number of branches given to `split` operation. +Outgoing channels will be tried to be matched to branch identifier enum `toString` ignoring case. +If not all branches are matched, it will fall back to one-by-one matching depending on the order of outgoing channel declarations and enum ordinals. diff --git a/content/docs/smallrye-reactive-messaging/4/concepts/pausable-channels.adoc b/content/docs/smallrye-reactive-messaging/4/concepts/pausable-channels.adoc new file mode 100644 index 000000000..2aeae29c4 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/concepts/pausable-channels.adoc @@ -0,0 +1,64 @@ += Pausable Channels +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Based on reactive streams, Smallrye Reactive Messaging ensures that channels are back-pressured. +This means that the flow of messages is controlled by the downstream consumer, +whether it is a processing method or outgoing channel. +Sometimes you may want to pause the flow of messages, for example, when the consumer is not ready to process them. + +Injected xref:concepts/emitter.adoc#retrieving-channels[`@Channel`] streams are not subscribed to by default, so the flow of messages is controlled by the application. +But for xref:concepts/model.adoc#incoming-and-outgoing[`@Incoming`] methods, the flow of messages is controlled by the runtime. + +Pausable channels are useful when you want to control the flow of messages within your application. + +== Creating a Pausable Channel + +To use pausable channels, you need to activate it with the configuration property `pausable` set to `true`. + +[source,properties] +---- +mp.messaging.incoming.my-channel.pausable=true +---- + +=== Configuration Options + +Pausable channels support the following configuration options: + +* `pausable.initially-paused` - Whether the channel starts in a paused state (default: `false`) +* `pausable.late-subscription` - Whether to subscribe to the upstream after pausing (default: `false`) +* `pausable.buffer-size` - Maximum buffer size for items received while the channel is paused (optional) +* `pausable.buffer-strategy` - Strategy for handling already requested items that arrive while paused. Possible values are: +** `BUFFER` - Buffer items received while paused and deliver them when resumed (default) +** `DROP` - Drop already requested items received while paused +** `IGNORE` - Let already requested items flow through even while paused + +Example configuration: + +[source,properties] +---- +mp.messaging.incoming.my-channel.pausable=true +mp.messaging.incoming.my-channel.pausable.initially-paused=true +mp.messaging.incoming.my-channel.pausable.buffer-size=100 +mp.messaging.incoming.my-channel.pausable.buffer-strategy=BUFFER +mp.messaging.incoming.my-channel.pausable.late-subscription=false +---- + +== Controlling the flow of messages + +If a channel is configured to be pausable, +you can inject the `PausableChannel` qualified with the `@Channel("channel-name")` annotation to control the flow of messages. +You can also get the `PausableChannel` by channel name from the `ChannelRegistry` programmatically, +and pause or resume the channel as needed: + +[source,java] +---- +include::{examples-dir}/pausable/PausableController.java[indent=0] +---- + +[WARNING] +==== + +Pausable channels only work with back-pressure aware subscribers, with bounded downstream requests. + +==== diff --git a/content/docs/smallrye-reactive-messaging/4/concepts/signatures.adoc b/content/docs/smallrye-reactive-messaging/4/concepts/signatures.adoc new file mode 100644 index 000000000..ef35c854f --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/concepts/signatures.adoc @@ -0,0 +1,105 @@ += Supported signatures +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The following tables list the supported method signatures and indicate +the various supported features. For instance, they indicate the default +and available acknowledgement strategies (when applicable). + +== Method signatures to generate data + +[width="100%",cols="32%,68%",options="header",] +|=== +|Signature |Invocation time +|`@Outgoing Publisher++<++Message++<++O++>>++ method()` `{vbar} Called once at ++*++assembly++*++ time {vbar} {vbar}`@Outgoing Publisher method()`` |Called once at _assembly_ time +|`@Outgoing Multi++<++Message++<++O++>>++ method()` `{vbar} Called once at ++*++assembly++*++ time {vbar} {vbar}`@Outgoing Multi method()`` |Called once at _assembly_ time +|`@Outgoing Flow.Publisher++<++Message++<++O++>>++ method()` `{vbar} Called once at ++*++assembly++*++ time {vbar} {vbar}`@Outgoing Flow.Publisher method()`` |Called once at _assembly_ time +|`@Outgoing PublisherBuilder++<++Message++<++O++>>++ method()` `{vbar} Called once at ++*++assembly++*++ time {vbar} {vbar}`@Outgoing PublisherBuilder method()`` |Called once at _assembly_ time +|`@Outgoing Message++<++O++>++ method()` `{vbar} Called for every downstream request, sequentially {vbar} {vbar}`@Outgoing O method()`` |Called for every downstream request, sequentially +|`@Outgoing CompletionStage++<++Message++<++O++>>++ method()` `{vbar} Called for every downstream request, sequentially (After the completion of the last returned CompletionStage) {vbar} {vbar}`@Outgoing CompletionStage method()`` |Called for every downstream request, , sequentially (After the completion of the last returned CompletionStage) +|`@Outgoing Uni++<++Message++<++O++>>++ method()` `{vbar} Called for every downstream request, sequentially (After the completion of the last returned Uni) {vbar} {vbar}`@Outgoing Uni method()`` |Called for every downstream request, , sequentially (After the completion of the last returned Uni) +|=== + +== Method signatures to consume data + +[width="100%",cols="37%,32%,31%",options="header",] +|=== +|Signature |Invocation time |Supported Acknowledgement Strategies +|`@Incoming void method(I p)` |Called for every incoming payload (sequentially) |_POST++_++PROCESSING_, NONE, PRE++_++PROCESSING +|`@Incoming CompletionStage++<++?++>++ method(Message++<++I++>++ msg)` |Called for every incoming message (sequentially) |_MANUAL_, NONE, PRE++_++PROCESSING +|`@Incoming CompletionStage++<++?++>++ method(I p)` |Called for every incoming payload (sequentially) |_POST++_++PROCESSING_, PRE++_++PROCESSING, NONE +|`@Incoming Uni++<++?++>++ method(Message++<++I++>++ msg)` |Called for every incoming message (sequentially) |_MANUAL_, NONE, PRE++_++PROCESSING +|`@Incoming Uni++<++?++>++ method(I p)` |Called for every incoming payload (sequentially) |_POST++_++PROCESSING_, PRE++_++PROCESSING, NONE +|`@Incoming Subscriber++<++Message++<++I++>>++ method()` |Called once at _assembly_ time |_MANUAL_, POST++_++PROCESSING, NONE, PRE++_++PROCESSING +|`@Incoming Subscriber++<++I++>++ method()` |Called once at _assembly_ time |_POST++_++PROCESSING_, NONE, PRE++_++PROCESSING +|`@Incoming Flow.Subscriber++<++Message++<++I++>>++ method()` |Called once at _assembly_ time |_MANUAL_, POST++_++PROCESSING, NONE, PRE++_++PROCESSING +|`@Incoming Flow.Subscriber++<++I++>++ method()` |Called once at _assembly_ time |_POST++_++PROCESSING_, NONE, PRE++_++PROCESSING +|`@Incoming SubscriberBuilder++<++Message++<++I++>++, ?++>++ method()` |Called once at _assembly_ time |_MANUAL_, POST++_++PROCESSING, NONE, PRE++_++PROCESSING +|`@Incoming SubscriberBuilder++<++I, ?++>++ method()` |Called once at _assembly_ time |_MANUAL_, POST++_++PROCESSING, NONE, PRE++_++PROCESSING +|=== + +== Method signatures to process data + +[width="99%",cols="37%,23%,30%,10%",options="header",] +|=== +|Signature |Invocation time |Supported Acknowledgement Strategies |Metadata Propagation +|`@Outgoing @Incoming Message++<++O++>++ method(Message++<++I++>++ msg)` |Called for every incoming message (sequentially) |POST++_++PROCESSING (Smallrye only), _MANUAL_, NONE, PRE++_++PROCESSING |manual +|`@Outgoing @Incoming Message++<++O++>++ method(I payload)` |Called for every incoming message (sequentially) |_POST++_++PROCESSING_ (Smallrye only), NONE, PRE++_++PROCESSING |automatic +|`@Outgoing @Incoming O method(I payload)` |Called for every incoming payload (sequentially) |_POST++_++PROCESSING_, NONE, PRE++_++PROCESSING |automatic +|`@Outgoing @Incoming CompletionStage++<++Message++<++O++>>++ method(Message++<++I++>++ msg)` |Called for every incoming message (sequentially) |_MANUAL_, NONE, PRE++_++PROCESSING |manual +|`@Outgoing @Incoming CompletionStage++<++O++>++ method(I payload)` |Called for every incoming payload (sequentially) |_POST++_++PROCESSING_, NONE, PRE++_++PROCESSING |automatic +|`@Outgoing @Incoming CompletionStage++<++Message++<++O++>>++ method(I payload)` |Called for every incoming payload (sequentially) |_POST++_++PROCESSING_ (Smallrye only), NONE, PRE++_++PROCESSING |automatic +|`@Outgoing @Incoming Uni++<++Message++<++O++>>++ method(Message++<++I++>++ msg)` |Called for every incoming message (sequentially) |_MANUAL_, NONE, PRE++_++PROCESSING |manual +|`@Outgoing @Incoming Uni++<++Message++<++O++>>++ method(I payload)` |Called for every incoming payload (sequentially) |_POST++_++PROCESSING_ (Smallrye only), NONE, PRE++_++PROCESSING |automatic +|`@Outgoing @Incoming Uni++<++O++>++ method(I payload)` |Called for every incoming payload (sequentially) |_POST++_++PROCESSING_, NONE, PRE++_++PROCESSING |automatic +|`@Outgoing @Incoming Processor++<++Message++<++I++>++, Message++<++O++>>++ method()` |Called once at _assembly_ time |_MANUAL_, PRE++_++PROCESSING, NONE |manual +|`@Outgoing @Incoming Processor++<++I, O++>++ method()` |Called once at _assembly_ time |_PRE++_++PROCESSING_, NONE |not supported +|`@Outgoing @Incoming Flow.Processor++<++Message++<++I++>++, Message++<++O++>>++ method()` |Called once at _assembly_ time |_MANUAL_, PRE++_++PROCESSING, NONE |manual +|`@Outgoing @Incoming Flow.Processor++<++I, O++>++ method()` |Called once at _assembly_ time |_PRE++_++PROCESSING_, NONE |not supported +|`@Outgoing @Incoming ProcessorBuilder++<++Message++<++I++>++, Message++<++O++>>++ method()` |Called once at _assembly_ time |_MANUAL_, PRE++_++PROCESSING, NONE |manual +|`@Outgoing @Incoming ProcessorBuilder++<++I, O++>++ method()` |Called once at _assembly_ time |_PRE++_++PROCESSING_, NONE |not supported +|`@Outgoing @Incoming Publisher++<++Message++<++O++>>++ method(Message++<++I++>++ msg)` |Called for every incoming message (sequentially) |_MANUAL_, PRE++_++PROCESSING, NONE |manual +|`@Outgoing @Incoming Publisher++<++O++>++ method(I payload)` |Called for every incoming payload (sequentially) |_PRE++_++PROCESSING_, POST++_++PROCESSING, NONE |automatic +|`@Outgoing @Incoming Multi++<++Message++<++O++>>++ method(Message++<++I++>++ msg)` |Called for every incoming message (sequentially) |_MANUAL_, PRE++_++PROCESSING, NONE |manual +|`@Outgoing @Incoming Multi++<++O++>++ method(I payload)` |Called for every incoming payload (sequentially) |_PRE++_++PROCESSING_, POST++_++PROCESSING, NONE |automatic +|`@Outgoing @Incoming Flow.Publisher++<++Message++<++O++>>++ method(Message++<++I++>++ msg)` |Called for every incoming message (sequentially) |_MANUAL_, PRE++_++PROCESSING, NONE |manual +|`@Outgoing @Incoming Flow.Publisher++<++O++>++ method(I payload)` |Called for every incoming payload (sequentially) |_PRE++_++PROCESSING_, POST++_++PROCESSING, NONE |automatic +|`@Outgoing @Incoming PublisherBuilder++<++Message++<++O++>>++ method(Message++<++I++>++ msg)` |Called for every incoming message (sequentially) |_MANUAL_, PRE++_++PROCESSING, NONE |manual +|`@Outgoing @Incoming PublisherBuilder++<++O++>++ method(I payload)` |Called for every incoming payload (sequentially) |_PRE++_++PROCESSING_, POST++_++PROCESSING, NONE |automatic +|=== + +Note that in additional to the MicroProfile Reactive Messaging specification, +SmallRye Reactive Messaging supports the post-processing acknowledgment handling with automatic metadata propagation for the following signatures: + +* `@Outgoing @Incoming Message++<++O++>++ method(I payload)` +* `@Outgoing @Incoming CompletionStage++<++Message++<++O++>>++ method(I payload)` +* `@Outgoing @Incoming Uni++<++Message++<++O++>>++ method(I payload)` +* `@Outgoing @Incoming Message++<++O++>++ method(Message++<++I++>++ payload)` : For this signature, the post-processing acknowledgment handling is limited. +It covers cases for nacking incoming messages on caught exceptions at the method body, acking incoming messages when outgoing message is skipped by returning `null`, and chaining acknowlegment from outgoing message to the incoming. +However, if the incoming message has already been (n)acked, you will experience duplicate (n)acks. + +== Method signatures to manipulate streams + +[width="100%",cols="52%,17%,20%,11%",options="header",] +|=== +|Signature |Invocation time |Supported Acknowledgement Strategies |Metadata Propagation +|`@Outgoing @Incoming Publisher++<++Message++<++O++>>++ method(Publisher++<++Message++<++I++>>++ pub)` |Called once at _assembly_ time |_MANUAL_, NONE, PRE++_++PROCESSING |manual +|`@Outgoing @Incoming Publisher++<++O++>++ method(Publisher++<++I++>++ pub)` |Called once at _assembly_ time |_PRE++_++PROCESSING_, NONE |not supported +|`@Outgoing @Incoming Multi++<++Message++<++O++>>++ method(Multi++<++Message++<++I++>>++ pub)` |Called once at _assembly_ time |_MANUAL_, NONE, PRE++_++PROCESSING |manual +|`@Outgoing @Incoming Multi++<++O++>++ method(Multi++<++I++>++ pub)` |Called once at _assembly_ time |_PRE++_++PROCESSING_, NONE |not supported +|`@Outgoing @Incoming Flow.Publisher++<++Message++<++O++>>++ method(Flow.Publisher++<++Message++<++I++>>++ pub)` |Called once at _assembly_ time |_MANUAL_, NONE, PRE++_++PROCESSING |manual +|`@Outgoing @Incoming Flow.Publisher++<++O++>++ method(Flow.Publisher++<++I++>++ pub)` |Called once at _assembly_ time |_PRE++_++PROCESSING_, NONE |not supported +|`@Outgoing @Incoming PublisherBuilder++<++Message++<++O++>>++ method(PublisherBuilder++<++Message++<++I++>>++ pub)` |Called once at _assembly_ time |_MANUAL_, NONE, PRE++_++PROCESSING |manual +|`@Outgoing @Incoming PublisherBuilder++<++O++>++ method(PublisherBuilder++<++I++>++ pub)` |Called once at _assembly_ time |NONE, PRE++_++PROCESSING |not supported +|=== + +[IMPORTANT] +==== + +When processing `Message`, it is often required to _chain_ the incoming +`Message` to enable post-processing acknowledgement and metadata +propagation. Use the `with` (like `withPayload`) methods from the +incoming message, so it copies the metadata and ack/nack methods. It +returns a new `Message` with the right content. + +==== diff --git a/content/docs/smallrye-reactive-messaging/4/concepts/skipping.adoc b/content/docs/smallrye-reactive-messaging/4/concepts/skipping.adoc new file mode 100644 index 000000000..2717afe78 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/concepts/skipping.adoc @@ -0,0 +1,29 @@ += Skipping messages +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Sometimes you receive a message and don’t want to produce an output +message. To handle this, you have several choices: + +[arabic] +. for method processing single message or payload, producing `null` +would produce an ignored message (not forwarded) +. for method processing streams, you can generate an _empty_ stream. + +== Skipping a single item + +To skip a single message or payload, return `null`: + +[source,java] +---- +include::{examples-dir}/skip/SingleSkip.java[tags=skip,indent=0] +---- + +== Skipping in a stream + +To skip a message or payload when manipulating a stream, emit an _empty_ `Multi` (or `Publisher`): + +[source,java] +---- +include::{examples-dir}/skip/StreamSkip.java[tags=skip,indent=0] +---- diff --git a/content/docs/smallrye-reactive-messaging/4/concepts/testing.adoc b/content/docs/smallrye-reactive-messaging/4/concepts/testing.adoc new file mode 100644 index 000000000..df3acdddb --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/concepts/testing.adoc @@ -0,0 +1,80 @@ += Testing your application +:version-root: ../ +include::{version-root}_attributes.adoc[] + +It’s not rare to have to test your application but deploying the +infrastructure can be cumbersome. While Docker or Test Containers have +improved the testing experience, you may want to _mock_ this +infrastructure. + +SmallRye Reactive Messaging proposes an _in-memory_ connector for this +exact purpose. It allows switching the connector used for a channel with +an _in-memory_ connector. This in-memory connector provides a way to +send messages to incoming channels, or check the received messages for +outgoing channels. + +To use the _in-memory_ connector, you need to add the following +dependency to your project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.reactive + smallrye-reactive-messaging-in-memory + {project-version} + test + +---- + +Then, in a test, you can do something like: + +[source,java] +---- +include::{examples-dir}/testing/MyTest.java[indent=0] +---- + +When switching a channel to the in-memory connector, all the +configuration properties are ignored. + +[WARNING] +==== + +This connector has been designed for testing purpose only. +Switching the channel to in-memory connector means that the +original connector is not invoked at all during tests. +Therefore, if your code depends on a specific connector behaviour +or a custom metadata you need to simulate those in your tests. + +==== + +The _switch_ methods return `Map++<++String, String++>++` instances containing +the set properties. While these system properties are already set, you +can retrieve them and pass them around, for example if you need to start +an external process with these properties: + +[source,java] +---- +include::{examples-dir}/testing/MyTestSetup.java[tags=code,indent=0] +---- + +[NOTE] +==== + +The in-memory connector support the `broadcast` and `merge` attributes. +So, if your connector is configured with `broadcast: true`, the +connector broadcasts the messages to all the channel consumers. If your +connector is configured with `merge:true`, the connector receives all +the messages sent to the mapped channel even when coming from multiple +producers. + +==== + +== Vert.x Context with In-memory Connector + +For the sake of simplicity, In-memory connector channels dispatch messages on the caller thread of `InMemorySource#send` method. +However, most of the other connectors handle context propagation dispatching messages on separate xref:concepts/message-context.adoc[duplicated Vert.x contexts]. + +If this causes a change of behaviour in your tests, +you can configure the in-memory connector channels with `run-on-vertx-context` attribute to dispatch events, +including messages and acknowledgements, on a Vert.x context. +Alternatively you can switch this behaviour using the `InMemorySource#runOnVertxContext` method. diff --git a/content/docs/smallrye-reactive-messaging/4/getting-started.adoc b/content/docs/smallrye-reactive-messaging/4/getting-started.adoc new file mode 100644 index 000000000..9cc19d837 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/getting-started.adoc @@ -0,0 +1,63 @@ += Getting Started +include::_attributes.adoc[] + +The easiest way to start using SmallRye Reactive Messaging is from https://quarkus.io[Quarkus]. +SmallRye Reactive Messaging can also be used standalone, or with https://openliberty.io/guides/microprofile-reactive-messaging.html[Open Liberty]. + +First, go to https://code.quarkus.io/?g=io.smallrye&a=getting-started-with-reactive-messaging&e=smallrye-reactive-messaging[code.quarkus.io]. +Select the `smallrye-reactive-messaging` extension (already done if you use the link), and then click on the generate button to download the code. + +One downloaded, unzip the project and import it in your IDE. + +If you look at the `pom.xml` file, you will see the following dependency: + +[source,xml] +---- + + io.quarkus + quarkus-smallrye-reactive-messaging + +---- + +It provides the support for SmallRye Reactive Messaging. + +Ok, so far so good, but we need event-driven _beans_. + +Create the `quickstart` package, and copy the following class into it: + +For instance: + +[source,java] +---- +include::{examples-dir}/quickstart/ReactiveMessagingExample.java[indent=0] +---- + +This class contains a set of methods: + +* producing messages (`source`) +* processing messages (`toUpperCase`) +* transforming the stream by skipping messages (`filter`) +* consuming messages (`sink`) + +Each of these methods are connected through _channels_. + +Now, let's see this in action. +For the terminal, run: + +[source,shell] +---- +> ./mvnw quarkus:dev +---- + +Running the previous example should give the following output: + +[source,text] +---- +>> HELLO +>> SMALLRYE +>> REACTIVE +>> MESSAGE +---- + +Of course, this is a very simple example. +To go further, let's have a look to the core xref:concepts/concepts.adoc[concepts] behind SmallRye Reactive Messaging. diff --git a/content/docs/smallrye-reactive-messaging/4/images/chain.png b/content/docs/smallrye-reactive-messaging/4/images/chain.png new file mode 100644 index 000000000..eacfc241c Binary files /dev/null and b/content/docs/smallrye-reactive-messaging/4/images/chain.png differ diff --git a/content/docs/smallrye-reactive-messaging/4/images/channels.png b/content/docs/smallrye-reactive-messaging/4/images/channels.png new file mode 100644 index 000000000..8b2052ce4 Binary files /dev/null and b/content/docs/smallrye-reactive-messaging/4/images/channels.png differ diff --git a/content/docs/smallrye-reactive-messaging/4/images/connectors.png b/content/docs/smallrye-reactive-messaging/4/images/connectors.png new file mode 100644 index 000000000..ebd50c302 Binary files /dev/null and b/content/docs/smallrye-reactive-messaging/4/images/connectors.png differ diff --git a/content/docs/smallrye-reactive-messaging/4/images/favicon.png b/content/docs/smallrye-reactive-messaging/4/images/favicon.png new file mode 100644 index 000000000..93f705b9c Binary files /dev/null and b/content/docs/smallrye-reactive-messaging/4/images/favicon.png differ diff --git a/content/docs/smallrye-reactive-messaging/4/images/incoming.png b/content/docs/smallrye-reactive-messaging/4/images/incoming.png new file mode 100644 index 000000000..69e57a8b3 Binary files /dev/null and b/content/docs/smallrye-reactive-messaging/4/images/incoming.png differ diff --git a/content/docs/smallrye-reactive-messaging/4/images/logo.png b/content/docs/smallrye-reactive-messaging/4/images/logo.png new file mode 100644 index 000000000..2d957bd52 Binary files /dev/null and b/content/docs/smallrye-reactive-messaging/4/images/logo.png differ diff --git a/content/docs/smallrye-reactive-messaging/4/images/message.png b/content/docs/smallrye-reactive-messaging/4/images/message.png new file mode 100644 index 000000000..fb38e78b0 Binary files /dev/null and b/content/docs/smallrye-reactive-messaging/4/images/message.png differ diff --git a/content/docs/smallrye-reactive-messaging/4/images/messages.png b/content/docs/smallrye-reactive-messaging/4/images/messages.png new file mode 100644 index 000000000..c0b37e6a2 Binary files /dev/null and b/content/docs/smallrye-reactive-messaging/4/images/messages.png differ diff --git a/content/docs/smallrye-reactive-messaging/4/images/outgoing.png b/content/docs/smallrye-reactive-messaging/4/images/outgoing.png new file mode 100644 index 000000000..f5aaef29d Binary files /dev/null and b/content/docs/smallrye-reactive-messaging/4/images/outgoing.png differ diff --git a/content/docs/smallrye-reactive-messaging/4/images/overview.png b/content/docs/smallrye-reactive-messaging/4/images/overview.png new file mode 100644 index 000000000..42e00e441 Binary files /dev/null and b/content/docs/smallrye-reactive-messaging/4/images/overview.png differ diff --git a/content/docs/smallrye-reactive-messaging/4/images/processor.png b/content/docs/smallrye-reactive-messaging/4/images/processor.png new file mode 100644 index 000000000..faac01281 Binary files /dev/null and b/content/docs/smallrye-reactive-messaging/4/images/processor.png differ diff --git a/content/docs/smallrye-reactive-messaging/4/images/redhat_reversed.svg b/content/docs/smallrye-reactive-messaging/4/images/redhat_reversed.svg new file mode 100644 index 000000000..37fe83c48 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/images/redhat_reversed.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/content/docs/smallrye-reactive-messaging/4/index.adoc b/content/docs/smallrye-reactive-messaging/4/index.adoc new file mode 100644 index 000000000..153fa5e3d --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/index.adoc @@ -0,0 +1,16 @@ += SmallRye Reactive Messaging +include::_attributes.adoc[] + +*SmallRye Reactive Messaging is a framework for building event-driven, data streaming, and event-sourcing applications.* + +It lets your application interact using various messaging technologies such as Apache Kafka, AMQP or MQTT. +The framework provides a flexible programming model bridging https://www.cdi-spec.org/[CDI] and event-driven. + +SmallRye Reactive Messaging is an implementation of the Eclipse MicroProfile Reactive Messaging specification {spec-version}. + +.Event-Driven Architectures +image::images/overview.png[Event-Driven Architectures] + +SmallRye Reactive Messaging is used in http://quarkus.io[Quarkus], and https://openliberty.io/[Open Liberty]. + +Start with the xref:getting-started.adoc[Getting Started] guide to know how it can be used and introduce various features. diff --git a/content/docs/smallrye-reactive-messaging/4/jms/advanced-jms.adoc b/content/docs/smallrye-reactive-messaging/4/jms/advanced-jms.adoc new file mode 100644 index 000000000..7a33879c2 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/jms/advanced-jms.adoc @@ -0,0 +1,37 @@ += Advanced configuration +:version-root: ../ +include::{version-root}_attributes.adoc[] + +== Underlying thread pool + +Lots of JMS operations are blocking and so not cannot be done on the +_caller_ thread. For this reason, these blocking operations are executed +on a worker thread. + +You can configure the thread pool providing these worker threads using +the following MicroProfile Config properties: + +* `smallrye.jms.threads.max-pool-size` - the max number of threads +(Defaults to 10) +* `smallrye.jms.threads.ttl` - the ttl of the created threads +(Defaults to 60 seconds) + +== Selecting the ConnectionFactory + +The JMS Connector requires a `jakarta.jms.ConnectionFactory` to be exposed +as a CDI bean. The connector looks for a `jakarta.jms.ConnectionFactory` +and delegate the interaction with the JMS server to this factory. + +In case you have several connection factories, you can use the +`@Identifier` qualifier on your factory to specify the name. Then, in +the channel configuration, configure the name as follows: + +[source,properties] +---- +# Configure the connector globally +mp.messaging.connector.smallrye-jms.connection-factory-name=my-factory-name +# Configure a specific incoming channel +mp.messaging.incoming.my-channel.connection-factory-name=my-factory-name +# Configure a specific outgoing channel +mp.messaging.outgoing.my-channel.connection-factory-name=my-factory-name +---- diff --git a/content/docs/smallrye-reactive-messaging/4/jms/jms.adoc b/content/docs/smallrye-reactive-messaging/4/jms/jms.adoc new file mode 100644 index 000000000..b94527d64 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/jms/jms.adoc @@ -0,0 +1,74 @@ += The JMS connector +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The JMS connector adds support for https://en.wikipedia.org/wiki/Jakarta_Messaging[Jakarta +Messaging] to Reactive +Messaging. It is designed to integrate with _JakartaEE_ applications +that are sending or receiving Jakarta Messaging Messages. + +Jakarta Messaging is a Java Message Oriented Middleware API for sending +messages between two or more clients. It is a programming model to +handle the producer-consumer messaging problem. It is a messaging +standard that allows application components based on Jakarta EE to +create, send, receive, and read messages. It allows the communication +between different components of a distributed application to be loosely +coupled, reliable, and asynchronous. + +Using the JMS connector + +To you the JMS Connector, add the following dependency to your project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.reactive + smallrye-reactive-messaging-jms + {project-version} + +---- + +The connector name is: `smallrye-jms`. + +So, to indicate that a channel is managed by this connector you need: + +[source,properties] +---- +# Inbound +mp.messaging.incoming.[channel-name].connector=smallrye-jms + +# Outbound +mp.messaging.outgoing.[channel-name].connector=smallrye-jms +---- + +The JMS Connector requires a `jakarta.jms.ConnectionFactory` to be exposed +(as CDI bean). The connector looks for a `jakarta.jms.ConnectionFactory` +and delegate the interaction with the JMS server to this factory. In +other words, it creates the JMS connection and context using this +factory. + +So, in order to use this connector you would need to expose a +`jakarta.jms.ConnectionFactory`: + +[source,java] +---- +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; +import jakarta.jms.ConnectionFactory; + +import org.apache.activemq.artemis.jms.client.ActiveMQJMSConnectionFactory; + +@ApplicationScoped +public class ConnectionFactoryBean { + + @Produces + ConnectionFactory factory() { + return new ActiveMQJMSConnectionFactory( + "tcp://localhost:61616", + null, null); + } + +} +---- + +The factory class may depend on your JMS connector/server. diff --git a/content/docs/smallrye-reactive-messaging/4/jms/receiving-jms-messages.adoc b/content/docs/smallrye-reactive-messaging/4/jms/receiving-jms-messages.adoc new file mode 100644 index 000000000..08f9af3f5 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/jms/receiving-jms-messages.adoc @@ -0,0 +1,115 @@ += Receiving messages from JMS +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The JMS Connector retrieves JMS Message and maps each of them into +Reactive Messaging `Messages`. + +== Example + +Let’s imagine you have a `jakarta.jms.ConnectionFactory` bean exposed and +connected to your JMS server. Don’t forget that it’s required to use the +JMS connector. + +Configure your application to receive JMS messages on the `prices` +channel as follows: + +[source,properties] +---- +mp.messaging.incoming.prices.connector=smallrye-jms +---- + +[NOTE] +==== + +You don’t need to set the destination. By default, it uses the channel +name (`prices`). You can configure the `destination` attribute to +override it. + +==== + +[NOTE] +==== + +By default the connector uses a `queue`. You can configure it to use a +`topic` by setting `destination-type=topic`. + +==== + +Then, your application receives `Message++<++Double++>++`. You can consume the +payload directly: + +[source,java] +---- +include::{examples-dir}/jms/inbound/JmsPriceConsumer.java[indent=0] +---- + +Or, you can retrieve the `Message++<++Double++>++`: + +[source,java] +---- +include::{examples-dir}/jms/inbound/JmsPriceMessageConsumer.java[indent=0] +---- + +== Deserialization + +The content of the incoming JMS message is mapped to a Java object. + +By default it extracts the JMS Message _body_ as a `java.lang.Object`. +This can be changed by setting, in the incoming JMS Message: + +[arabic] +. The `++_++classname` property +. the `JMSType` + +The value must be a fully qualified class name. The connector then load +the associated class. + +[NOTE] +==== + +The connector loads the associated `Class` using the `TCCL` and if not +found, the classloader used to load the connector. + +==== + +If the target type is a primitive type ort `String`, the resulting +message contains the mapped payload. + +If the target type is a class, the object is built using included JSON +deserializer (JSON-B and Jackson provided OOB from the `JMSType`. +If not, the default behavior is used (Java deserialization). + +== Inbound Metadata + +Messages coming from JMS contains an instance of link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-jms/{javadoc-version}/io/smallrye/reactive/messaging/jms/IncomingJmsMessageMetadata.html[`io.smallrye.reactive.messaging.jms.IncomingJmsMessageMetadata`] in the metadata. + +[source,java] +---- +include::{examples-dir}/jms/inbound/JmsMetadataExample.java[tags=code,indent=0] +---- + +== Acknowledgement + +When the Reactive Messaging `Message` gets acknowledged, the associated +JMS Message is acknowledged. As JMS acknowledgement is blocking, this +acknowledgement is delegated to a worker thread. + +== Failure Handling + +If a message produced from a JMS message is _nacked_, a failure strategy is applied. The JMS connector supports 3 strategies: + +* `fail` - (default) fail the application, no more messages will be processed. The failing message is not acknowledged and may be redelivered by the JMS broker. The application is marked as unhealthy (impacting liveness checks). +* `ignore` - the failure is logged, but the processing continues. The failing message is acknowledged and will not be redelivered. +* `dead-letter-queue` - the failing message is acknowledged and sent to a JMS _dead letter queue_ destination. The processing continues with the next message. ++ +The dead letter queue destination can be configured using the `dead-letter-queue.destination` attribute. If not specified, it defaults to `dead-letter-queue-$channel`. +Messages sent to the dead letter queue preserve the original message body and properties. In addition, the following properties are added: +** `dead++_++letter++_++exception++_++class++_++name` - the fully qualified class name of the exception +** `dead++_++letter++_++reason` - the exception message +** `dead++_++letter++_++cause++_++class++_++name` - the fully qualified class name of the root cause (if available) +** `dead++_++letter++_++cause` - the root cause exception message (if available) + +== Configuration Reference + +include::{generated-dir}/smallrye-jms-incoming.adoc[indent=0] diff --git a/content/docs/smallrye-reactive-messaging/4/jms/sending-jms-messages.adoc b/content/docs/smallrye-reactive-messaging/4/jms/sending-jms-messages.adoc new file mode 100644 index 000000000..0e107b798 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/jms/sending-jms-messages.adoc @@ -0,0 +1,106 @@ += Sending messages to JMS +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The JMS Connector can send Reactive Messaging `Messages` as JMS Message. + +== Example + +Let’s imagine you have a `jakarta.jms.ConnectionFactory` bean exposed and +connected to your JMS server. Don’t forget that it’s required to use the +JMS connector. + +Configure your application to write the messages from the `prices` +channel into a JMS Message as follows: + +[source,properties] +---- +mp.messaging.outgoing.prices.connector=smallrye-jms +---- + +[NOTE] +==== + +You don’t need to set the destination. By default, it uses the channel +name (`prices`). You can configure the `destination` attribute to +override it. + +==== + +[NOTE] +==== + +By default the connector uses a `queue`. You can configure it to use a +`topic` by setting `destination-type=topic`. + +==== + +Then, your application must send `Message++<++Double++>++` to the `prices` +channel. It can use `double` payloads as in the following snippet: + +[source,java] +---- +include::{examples-dir}/jms/outbound/JmsPriceProducer.java[indent=0] +---- + +Or, you can send `Message++<++Double++>++`: + +[source,java] +---- +include::{examples-dir}/jms/outbound/JmsPriceMessageProducer.java[indent=0] +---- + +== Serialization + +The connector serializes the incoming message payload into the body of +the outgoing JMS Message. + +If the payload is a `String` or a primitive type, the payload is encoded +as `String` and the `JMSType` is set to the target class. The +`++_++classname` property is also set. The JMS Message is a `TextMessage`. + +If the payload is a `byte++[]++`, it’s passed as `byte++[]++` in a JMS +`BytesMessage`. + +Otherwise, the payload is encoded using included JSON serializer (JSON-B and Jackson provided OOB. +The `JMSType` is set to the target class. +The `++_++classname` property is also set. +The JMS Message is a `TextMessage`. + +For example, the following code serialize the produced `Person` using +JSON-B. + +[source,java] +---- +@Incoming("...") +@Outgoing("my-channel") +public Person sendToJms(...) { + // ... + return new Person("bob", 42); +} +---- + +It requires that the `Person` class can be serialized to JSON. The +classname is passed in the `JMSType` property and `++_++classname` property. + +== Outbound Metadata + +When sending `Messages`, you can add an instance of link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-jms/{javadoc-version}/io/smallrye/reactive/messaging/jms/OutgoingJmsMessageMetadata.html[`OutgoingJmsMessageMetadata`] +to influence how the message is going to be written to JMS. + +[source,java] +---- +include::{examples-dir}/jms/outbound/JmsOutboundMetadataExample.java[tags=code,indent=0] +---- + +The metadata allow adding properties but also override the destination. + +== Acknowledgement + +Once the JMS message is sent to the JMS server, the message is +acknowledged. Sending a JMS message is a blocking operation. So, sending +is done on a worker thread. + +== Configuration Reference + +include::{generated-dir}/smallrye-jms-outgoing.adoc[indent=0] diff --git a/content/docs/smallrye-reactive-messaging/4/kafka/avro-configuration.adoc b/content/docs/smallrye-reactive-messaging/4/kafka/avro-configuration.adoc new file mode 100644 index 000000000..a714f9cb9 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/kafka/avro-configuration.adoc @@ -0,0 +1,152 @@ += Using Apache Avro serializer/deserializer +:version-root: ../ +include::{version-root}_attributes.adoc[] + +If you are using https://avro.apache.org/[Apache Avro] +serializer/deserializer, please note the following configuration +properties. + +== For https://docs.confluent.io/current/schema-registry/serdes-develop/serdes-avro.html[Confluent] Schema Registry + +Confluent Avro library is `io.confluent:kafka-avro-serializer`. Note +that this library is not available in Maven Central, you need to use the +https://docs.confluent.io/clients-kafka-java/current/overview.html[Confluent Maven +repository]. + +=== Consumer + +[width="100%",cols="25%,75%",options="header",] +|=== +|Property |Recommended value +|value.deserializer |io.confluent.kafka.serializers.KafkaAvroDeserializer +|schema.registry.url |http://%7Byour_host%7D:%7Byour_port%7D/ +|specific.avro.reader |true +|=== + +Example: + +.... +mp.messaging.incoming.[channel].value.deserializer=io.confluent.kafka.serializers.KafkaAvroDeserializer +mp.messaging.incoming.[channel].schema.registry.url=http://{your_host}:{your_port}/ +mp.messaging.incoming.[channel].specific.avro.reader=true +.... + +=== Producer + +[width="100%",cols="24%,76%",options="header",] +|=== +|Property |Recommended value +|value.serializer |io.confluent.kafka.serializers.KafkaAvroSerializer +|schema.registry.url |http://%7Byour_host%7D:%7Byour_port%7D/ +|=== + +Example: + +.... +mp.messaging.outgoing.[channel].value.serializer=io.confluent.kafka.serializers.KafkaAvroSerializer +mp.messaging.outgoing.[channel].schema.registry.url=http://{your_host}:{your_port}/ +.... + +== For https://www.apicur.io/registry/[Apicurio] Registry 1.x + +Apicurio Registry 1.x Avro library is +`io.apicurio:apicurio-registry-utils-serde`. + +The configuration properties listed here are meant to be used with the +Apicurio Registry 1.x client library and Apicurio Registry 1.x server. + +=== Consumer + +[width="100%",cols="38%,62%",options="header",] +|=== +|Property |Recommended value +|value.deserializer |io.apicurio.registry.utils.serde.AvroKafkaDeserializer +|apicurio.registry.url |http://%7Byour_host%7D:%7Byour_port%7D/api +|apicurio.registry.avro-datum-provider |io.apicurio.registry.utils.serde.avro.DefaultAvroDatumProvider +|apicurio.registry.use-specific-avro-reader |true +|=== + +Example: + +.... +mp.messaging.incoming.[channel].value.deserializer=io.apicurio.registry.utils.serde.AvroKafkaDeserializer +mp.messaging.incoming.[channel].apicurio.registry.url=http://{your_host}:{your_port}/api +mp.messaging.incoming.[channel].apicurio.registry.avro-datum-provider=io.apicurio.registry.utils.serde.avro.DefaultAvroDatumProvider +mp.messaging.incoming.[channel].apicurio.registry.use-specific-avro-reader=true +.... + +=== Producer + +[width="100%",cols="24%,76%",options="header",] +|=== +|Property |Recommended value +|value.serializer |io.apicurio.registry.utils.serde.AvroKafkaSerializer +|apicurio.registry.url |http://%7Byour_host%7D:%7Byour_port%7D/api +|=== + +To automatically register schemas with the registry, add: + +[width="100%",cols="31%,69%",options="header",] +|=== +|Property |Value +|apicurio.registry.global-id |io.apicurio.registry.utils.serde.strategy.GetOrCreateIdStrategy +|=== + +Example: + +.... +mp.messaging.outgoing.[channel].value.serializer=io.apicurio.registry.utils.serde.AvroKafkaSerializer +mp.messaging.outgoing.[channel].apicurio.registry.url=http://{your_host}:{your_port}/api +mp.messaging.outgoing.[channel].apicurio.registry.global-id=io.apicurio.registry.utils.serde.strategy.GetOrCreateIdStrategy +.... + +== For https://www.apicur.io/registry/[Apicurio] Registry 2.x + +Apicurio Registry 2.x Avro library is +`io.apicurio:apicurio-registry-serdes-avro-serde`. + +The configuration properties listed here are meant to be used with the +Apicurio Registry 2.x client library and Apicurio Registry 2.x server. + +=== Consumer + +[width="100%",cols="31%,69%",options="header",] +|=== +|Property |Recommended value +|value.deserializer |io.apicurio.registry.serde.avro.AvroKafkaDeserializer +|apicurio.registry.url |http://%7Byour_host%7D:%7Byour_port%7D/apis/registry/v2 +|apicurio.registry.use-specific-avro-reader |true +|=== + +Example: + +.... +mp.messaging.incoming.[channel].value.deserializer=io.apicurio.registry.serde.avro.AvroKafkaDeserializer +mp.messaging.incoming.[channel].apicurio.registry.url=http://{your_host}:{your_port}/apis/registry/v2 +mp.messaging.incoming.[channel].apicurio.registry.use-specific-avro-reader=true +.... + +=== Producer + +[width="100%",cols="19%,81%",options="header",] +|=== +|Property |Recommended value +|value.serializer |io.apicurio.registry.serde.avro.AvroKafkaSerializer +|apicurio.registry.url |http://%7Byour_host%7D:%7Byour_port%7D/apis/registry/v2 +|=== + +To automatically register schemas with the registry, add: + +[cols=",",options="header",] +|=== +|Property |Value +|apicurio.registry.auto-register |true +|=== + +Example: + +.... +mp.messaging.outgoing.[channel].value.serializer=io.apicurio.registry.serde.avro.AvroKafkaSerializer +mp.messaging.outgoing.[channel].apicurio.registry.url=http://{your_host}:{your_port}/apis/registry/v2 +mp.messaging.outgoing.[channel].apicurio.registry.auto-register=true +.... diff --git a/content/docs/smallrye-reactive-messaging/4/kafka/client-service.adoc b/content/docs/smallrye-reactive-messaging/4/kafka/client-service.adoc new file mode 100644 index 000000000..2487db3de --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/kafka/client-service.adoc @@ -0,0 +1,39 @@ += KafkaClientService +:version-root: ../ +include::{version-root}_attributes.adoc[] + +For advanced use cases, SmallRye Reactive Messaging provides a bean of +type `KafkaClientService` that you can inject: + +[source,java] +---- +@Inject +KafkaClientService kafka; +---- + +From there, you can obtain an +`io.smallrye.reactive.messaging.kafka.KafkaProducer` and an +`io.smallrye.reactive.messaging.kafka.KafkaConsumer`. + +`KafkaProducer` and `KafkaConsumer` expose a non-blocking API on top of +the Kafka client API. They also mediate access to the threads that +SmallRye Reactive Messaging uses to run all Kafka operations: the +_polling thread_, used for consuming records from Kafka topics, and the +_sending thread_, used for producing records to Kafka topics. (Just to +be clear: each _channel_ has its own polling thread and sending thread.) + +The reason why SmallRye Reactive Messaging uses a special thread to run +the poll loop should be obvious: the `Consumer` API is blocking. The +`Producer` API, on the other hand, is documented to be non-blocking. +However, in present versions, Kafka doesn’t guarantee that in all cases; +see https://issues.apache.org/jira/browse/KAFKA-3539[KAFKA-3539] for +more details. That is why SmallRye Reactive Messaging uses a dedicated +thread to run the send operations as well. + +Sometimes, SmallRye Reactive Messaging provides direct access to the +Kafka `Producer` or `Consumer`. For example, a +xref:kafka/consumer-rebalance-listener.adoc[`KafkaConsumerRebalanceListener`] +methods are always invoked on the polling thread, so they give you +direct access to `Consumer`. In such case, you should use the +`Producer`/`Consumer` API directly, instead of the +`KafkaProducer`/`KafkaConsumer` API. diff --git a/content/docs/smallrye-reactive-messaging/4/kafka/consumer-rebalance-listener.adoc b/content/docs/smallrye-reactive-messaging/4/kafka/consumer-rebalance-listener.adoc new file mode 100644 index 000000000..fbbdd605f --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/kafka/consumer-rebalance-listener.adoc @@ -0,0 +1,67 @@ += Consumer Rebalance Listener +:version-root: ../ +include::{version-root}_attributes.adoc[] + +To handle offset commit and assigned partitions yourself, you can +provide a consumer rebalance listener. To achieve this, implement the +`io.smallrye.reactive.messaging.kafka.KafkaConsumerRebalanceListener` +interface, make the implementing class a bean, and add the `@Identifier` +qualifier. A usual use case is to store offset in a separate data store +to implement exactly-once semantic, or starting the processing at a +specific offset. + +The listener is invoked every time the consumer topic/partition +assignment changes. For example, when the application starts, it invokes +the `partitionsAssigned` callback with the initial set of +topics/partitions associated with the consumer. If, later, this set +changes, it calls the `partitionsRevoked` and `partitionsAssigned` +callbacks again, so you can implement custom logic. + +Note that the rebalance listener methods are called from the Kafka +_polling_ thread and must block the caller thread until completion. +That’s because the rebalance protocol has synchronization barriers, and +using asynchronous code in a rebalance listener may be executed after +the synchronization barrier. + +When topics/partitions are assigned or revoked from a consumer, it +pauses the message delivery and restarts once the rebalance completes. + +If the rebalance listener handles offset commit on behalf of the user +(using the `ignore` commit strategy), the rebalance listener *must* +commit the offset synchronously in the `partitionsRevoked` callback. We +also recommend applying the same logic when the application stops. + +Unlike the `ConsumerRebalanceListener` from Apache Kafka, the +`io.smallrye.reactive.messaging.kafka.KafkaConsumerRebalanceListener` +methods pass the Kafka `Consumer` and the set of topics/partitions. + +== Example + +In this example we set-up a consumer that always starts on messages from +at most 10 minutes ago (or offset 0). First we need to provide a bean +that implements the +`io.smallrye.reactive.messaging.kafka.KafkaConsumerRebalanceListener` +interface and is annotated with `@Identifier`. We then must configure +our inbound connector to use this named bean. + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaRebalancedConsumerRebalanceListener.java[indent=0] +---- + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaRebalancedConsumer.java[indent=0] +---- + +To configure the inbound connector to use the provided listener we +either set the consumer rebalance listener’s name: + +* `mp.messaging.incoming.rebalanced-example.consumer-rebalance-listener.name=rebalanced-example.rebalancer` + +Or have the listener’s name be the same as the group id: + +* `mp.messaging.incoming.rebalanced-example.group.id=rebalanced-example.rebalancer` + +Setting the consumer rebalance listener’s name takes precedence over +using the group id. diff --git a/content/docs/smallrye-reactive-messaging/4/kafka/default-configuration.adoc b/content/docs/smallrye-reactive-messaging/4/kafka/default-configuration.adoc new file mode 100644 index 000000000..a8da3024e --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/kafka/default-configuration.adoc @@ -0,0 +1,80 @@ += Retrieving Kafka default configuration +:version-root: ../ +include::{version-root}_attributes.adoc[] + +If your application/runtime exposes as a CDI _bean_ of type +`Map++<++String, Object` with the identifier `default-kafka-broker`, this +configuration is used to establish the connection with the Kafka broker. + +For example, you can imagine exposing this map as follows: + +[source,java] +---- +@Produces +@ApplicationScoped +@Identifier("default-kafka-broker") +public Map createKafkaRuntimeConfig() { + Map properties = new HashMap<>(); + + StreamSupport + .stream(config.getPropertyNames().spliterator(), false) + .map(String::toLowerCase) + .filter(name -> name.startsWith("kafka")) + .distinct() + .sorted() + .forEach(name -> { + final String key = name.substring("kafka".length() + 1).toLowerCase().replaceAll("[^a-z0-9.]", "."); + final String value = config.getOptionalValue(name, String.class).orElse(""); + properties.put(key, value); + }); + + return properties; +} +---- + +This previous example would extract all the configuration keys from +MicroProfile Config starting with `kafka`. + +[NOTE] +.Quarkus +==== + +Starting with Quarkus 1.5, a map corresponding to the previous example +is automatically provided. + +==== + +In addition to this default configuration, you can configure the name of +the `Map` producer using the `kafka-configuration` attribute: + +[source,properties] +---- +mp.messaging.incoming.my-channel.connector=smallrye-kafka +mp.messaging.incoming.my-channel.kafka-configuration=my-configuration +---- + +In this case, the connector looks for the `Map` associated with the +`my-configuration` name. If `kafka-configuration` is not set, an +optional lookup for a `Map` exposed with the channel name (`my-channel` +in the previous example) is done. + +[IMPORTANT] +==== + +If `kafka-configuration` is set and no `Map` can be found, the +deployment fails. + +==== + +Attribute values are resolved as follows: + +[arabic] +. if the attribute is set directly on the channel configuration +(`mp.messaging.incoming.my-channel.attribute=value`), this value is +used +. if the attribute is not set on the channel, the connector looks for +a `Map` with the channel name or the configured +`kafka-configuration` (if set) and the value is retrieved from that +`Map` +. If the resolved `Map` does not contain the value the default `Map` +is used (exposed with the `default-kafka-broker` name) diff --git a/content/docs/smallrye-reactive-messaging/4/kafka/health.adoc b/content/docs/smallrye-reactive-messaging/4/kafka/health.adoc new file mode 100644 index 000000000..2b233b117 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/kafka/health.adoc @@ -0,0 +1,107 @@ += Health reporting +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The Kafka connector reports the startup, readiness and liveness of each channel managed by the connector. + +[NOTE] +==== + +To disable health reporting, set the `health-enabled` attribute for the +channel to `false`. + +==== + +== Startup & Readiness + +=== Metrics-based strategy + +By default, both inbound and outbound channels use underlying Kafka client metrics to check if at least one active connection exists with a broker. +This strategy is lightweight and does not require additional remote interactions with the broker. + +=== Client-based strategy + +You can also enable another strategy by setting the `health-topic-verification-enabled` attribute to `true`. +With this second strategy, the health checks use a Kafka Admin Client to access the broker and retrieve the list of existing topics. +Retrieving this list can be a lengthy and expensive operation. +You can configure a timeout using the `health-topic-verification-timeout` attribute. +The default timeout is set to 2 seconds. +Note that if the timeout is reached, the health check fails. + +[WARNING] +.Deprecated +==== + +`health-readiness-topic-verification` and `health-readiness-timeout` attributes are deprecated and replaced by `health-topic-verification-enabled` and `health-topic-verification-timeout`. + +==== + +For *startup* checks both _inbound_ and _outbound_ side verify that the Kafka topic is created and its partitions are available in the broker. +If multiple topics are consumed using the `topics` attribute, the readiness check verifies that all the consumed topics are available. +If you use a pattern (using the `pattern` attribute), the readiness check verifies that at least one existing topic matches the pattern. + +For *readiness* checks inbound channels verify that the underlying consumer is assigned at least a partition to consume. +On the outbound side (writing records to Kafka) verify that the broker is still accessible. + +[NOTE] +==== + +If `health-topic-verification-enabled` is enabled, both for startup and readiness checks use this strategy. They can be disabled explicitly using `health-topic-verification-startup-disabled` and `health-topic-verification-readiness-disabled` flags. + +==== + +==== Admin Client Pooling + +By default, the client-based strategy creates a dedicated Kafka Admin Client per channel, using client IDs that include the channel name (e.g. `kafka-admin-incoming-prices`, `kafka-admin-outgoing-orders`). +This makes it easy to identify each client in monitoring, logs, and ACLs. + +If your application has many channels sharing the same Kafka broker configuration, you can opt in to admin client pooling to reduce the number of connections: + +[source,properties] +---- +smallrye.messaging.kafka.admin-client.pooling.enabled=true +---- + +When pooling is enabled, channels with identical broker configurations share a single Admin Client instance, using counter-based client IDs (`smallrye-kafka-admin-1`, `smallrye-kafka-admin-2`, ...). + +To summarize for startup and readiness health checks: + +*Startup* + +[width="100%",cols="12%,50%,38%",options="header",] +|=== +| |Inbound |Outbound +|Metrics-based |`connection-count` metric ++>++ 0 or + + no subscribers to the stream |`connection-count` metric ++>++ 0 +|Client-based |Subscribed topic(s) exist in the broker and are available |Produced topic exist in the broker and is available +|=== + +*Readiness* + +[width="100%",cols="11%,56%,33%",options="header",] +|=== +| |Inbound |Outbound +|Metrics-based |`connection-count` metric ++>++ 0 or + + no subscribers to the stream |`connection-count` metric ++>++ 0 +|Client-based |Consumer has at least one partition assignment + + no subscribers to the stream |Cluster is acessible using the Kafka admin API +|=== + +== Liveness + +On the inbound side (receiving records from Kafka), the liveness check +verifies that: + +* no failures have been caught +* the client is connected to the broker + +On the outbound side (writing records to Kafka), the liveness check +verifies that: + +* no failures have been caught + +Note that a message processing failures _nacks_ the message which is +then handled by the failure-strategy. It the responsibility of the +failure-strategy to report the failure and influence the outcome of the +liveness checks. The `fail` failure strategy reports the failure and so +the liveness check will report the failure. diff --git a/content/docs/smallrye-reactive-messaging/4/kafka/kafka.adoc b/content/docs/smallrye-reactive-messaging/4/kafka/kafka.adoc new file mode 100644 index 000000000..dbe86b6d0 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/kafka/kafka.adoc @@ -0,0 +1,47 @@ += Apache Kafka Connector +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The Kafka connector adds support for Kafka to Reactive Messaging. With +it you can receive Kafka Records as well as write `message` into Kafka. + +https://kafka.apache.org/[Apache Kafka] is a popular distributed +streaming platform. It lets you: + +* Publish and subscribe to streams of records, similar to a message +queue or enterprise messaging system. +* Store streams of records in a fault-tolerant durable way. +* Process streams of records as they occur. + +The Kafka cluster stores streams of _records_ in categories called +_topics_. Each record consists of a _key_, a _value_, and a _timestamp_. + +For more details about Kafka, check the +https://kafka.apache.org/intro[documentation]. + +== Using the Kafka Connector + +To use the Kafka Connector, add the following dependency to your +project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.reactive + smallrye-reactive-messaging-kafka + {project-version} + +---- + +The connector name is: `smallrye-kafka`. + +So, to indicate that a channel is managed by this connector you need: + +[source,properties] +---- +# Inbound +mp.messaging.incoming.[channel-name].connector=smallrye-kafka + +# Outbound +mp.messaging.outgoing.[channel-name].connector=smallrye-kafka +---- diff --git a/content/docs/smallrye-reactive-messaging/4/kafka/kerberos.adoc b/content/docs/smallrye-reactive-messaging/4/kafka/kerberos.adoc new file mode 100644 index 000000000..873617497 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/kafka/kerberos.adoc @@ -0,0 +1,22 @@ += Kerberos authentication +:version-root: ../ +include::{version-root}_attributes.adoc[] + +When using https://en.wikipedia.org/wiki/Kerberos_(protocol)[Kerberos] +authentication, you need to configure the connector with: + +* the security protocol set to `SASL++_++PLAINTEXT` +* the SASL mechanism set to `GSSAPI` +* the Jaas config configured with `Krb5LoginModule` +* the Kerberos service name + +The following snippet provides an example: + +[source,properties] +---- +kafka.bootstrap.servers=ip-192-168-0-207.us-east-2.compute.internal:9094 +kafka.sasl.mechanism=GSSAPI +kafka.security.protocol=SASL_PLAINTEXT +kafka.sasl.jaas.config=com.sun.security.auth.module.Krb5LoginModule required doNotPrompt=true refreshKrb5Config=true useKeyTab=true storeKey=true keyTab="file:/opt/kafka/krb5/kafka-producer.keytab" principal="kafka-producer/ip-192-168-0-207.us-east-2.compute.internal@INTERNAL"; +kafka.sasl.kerberos.service.name=kafka +---- diff --git a/content/docs/smallrye-reactive-messaging/4/kafka/protobuf-configuration.adoc b/content/docs/smallrye-reactive-messaging/4/kafka/protobuf-configuration.adoc new file mode 100644 index 000000000..36e025d1e --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/kafka/protobuf-configuration.adoc @@ -0,0 +1,51 @@ += Using Google Protobuf serializer/deserializer +:version-root: ../ +include::{version-root}_attributes.adoc[] + +If you are using https://developers.google.com/protocol-buffers/[Protocol +Buffers] +serializer/deserializer, please note the following configuration +properties. + +== For https://docs.confluent.io/platform/current/schema-registry/serdes-develop/serdes-protobuf.html#serdes-and-formatter-protobuf[Confluent] Schema Registry + +Confluent protobuf library is `io.confluent:kafka-protobuf-serializer`. +Note that this library is not available in Maven Central, you need to +use the https://docs.confluent.io/clients-kafka-java/current/overview.html[Confluent Maven +repository]. + +=== Consumer + +[width="100%",cols="49%,51%",options="header",] +|=== +|Property |Recommended value +|value.deserializer |io.confluent.kafka.serializers.protobuf.KafkaProtobufDeserializer +|schema.registry.url |http://%7Byour_host%7D:%7Byour_port%7D/ +|mp.messaging.incoming.++[++channel++]++.specific.protobuf.value.type |your.package.DomainObjectKey$Key +|mp.messaging.incoming.++[++channel++]++.specific.protobuf.key.type |your.package.DomainObjectValue$Value +|=== + +Example: + +.... +mp.messaging.incoming.[channel].value.deserializer=io.confluent.kafka.serializers.protobuf.KafkaProtobufDeserializer +mp.messaging.incoming.[channel].schema.registry.url=http://{your_host}:{your_port}/ +mp.messaging.incoming.[channel].specific.protobuf.value.type=your.package.DomainObjectKey$Key +mp.messaging.incoming.[channel].specific.protobuf.key.type=your.package.DomainObjectValue$Value +.... + +=== Producer + +[width="100%",cols="24%,76%",options="header",] +|=== +|Property |Recommended value +|value.serializer |io.confluent.kafka.serializers.protobuf.KafkaProtobufSerializer +|schema.registry.url |http://%7Byour_host%7D:%7Byour_port%7D/ +|=== + +Example: + +.... +mp.messaging.outgoing.[channel].value.serializer=io.confluent.kafka.serializers.protobuf.KafkaProtobufSerializer +mp.messaging.outgoing.[channel].schema.registry.url=http://{your_host}:{your_port}/ +.... diff --git a/content/docs/smallrye-reactive-messaging/4/kafka/receiving-kafka-records.adoc b/content/docs/smallrye-reactive-messaging/4/kafka/receiving-kafka-records.adoc new file mode 100644 index 000000000..7b6e44f00 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/kafka/receiving-kafka-records.adoc @@ -0,0 +1,1058 @@ += Receiving Kafka Records +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The Kafka Connector retrieves Kafka Records from Kafka Brokers and maps +each of them to Reactive Messaging `Messages`. + +== Example + +Let’s imagine you have a Kafka broker running, and accessible using the +`kafka:9092` address (by default it would use `localhost:9092`). +Configure your application to receive Kafka records from a Kafka _topic_ +on the `prices` channel as follows: + +[source,properties] +---- +kafka.bootstrap.servers=kafka:9092 # <1> + +mp.messaging.incoming.prices.connector=smallrye-kafka # <2> +mp.messaging.incoming.prices.value.deserializer=org.apache.kafka.common.serialization.DoubleDeserializer # <3> +mp.messaging.incoming.prices.broadcast=true # <4> +---- + +[arabic] +. Configure the broker location. You can configure it globally or per +channel +. Configure the connector to manage the `prices` channel +. Sets the (Kafka) deserializer to read the record’s value +. Make sure that we can receive from more than one consumer (see +`KafkaPriceConsumer` and `KafkaPriceMessageConsumer` below) + +[NOTE] +==== + +You don’t need to set the Kafka topic. By default, it uses the channel +name (`prices`). You can configure the `topic` attribute to override it. + +==== + +Then, your application receives `Message++<++Double++>++`. You can consume the +payload directly: + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaPriceConsumer.java[indent=0] +---- + +Or, you can retrieve the `Message++<++Double++>++`: + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaPriceMessageConsumer.java[indent=0] +---- + +== Deserialization + +The deserialization is handled by the underlying Kafka Client. You need +to configure the: + +* `mp.messaging.incoming.++[++channel-name++]++.value.deserializer` to +configure the value deserializer (mandatory) +* `mp.messaging.incoming.++[++channel-name++]++.key.deserializer` to configure +the key deserializer (optional, default to `String`) + +If you want to use a custom deserializer, add it to your `CLASSPATH` and +configure the associate attribute. + +In addition, the Kafka Connector also provides a set of _message +converters_. So you can receive _payloads_ representing records from +Kafka using: + +* link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-kafka/{javadoc-version}/io/smallrye/reactive/messaging/kafka/Record.html[`Record`] - a pair key/value +* https://kafka.apache.org/26/javadoc/index.html?org/apache/kafka/clients/consumer/ConsumerRecord.html[ConsumerRecord++<++K,V++>++] - +a structure representing the record with all its metadata + +[source,java] +---- +include::{examples-dir}/kafka/inbound/Converters.java[tags=code,indent=0] +---- + +== Inbound Metadata + +Messages coming from Kafka contains an instance of +link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-kafka/{javadoc-version}/io/smallrye/reactive/messaging/kafka/api/IncomingKafkaRecordMetadata.html[`IncomingKafkaRecordMetadata`] +in the metadata. It provides the key, topic, partitions, +headers and so on: + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaMetadataExample.java[tags=code,indent=0] +---- + +== Acknowledgement + +When a message produced from a Kafka record is acknowledged, the +connector invokes a _commit strategy_. These strategies decide when the +consumer offset for a specific topic/partition is committed. Committing +an offset indicates that all previous records have been processed. It is +also the position where the application would restart the processing +after a crash recovery or a restart. + +Committing every offset has performance penalties as Kafka offset +management can be slow. However, not committing the offset often enough +may lead to message duplication if the application crashes between two +commits. + +The Kafka connector supports three strategies: + +* `throttled` keeps track of received messages and commit to the next +offset after the latest _acked_ message in sequence. This strategy +guarantees _at-least-once delivery_ even if the channel performs +asynchronous processing. The connector tracks the received records +and periodically (period specified by `auto.commit.interval.ms` +(default: 5000)) commits the highest consecutive offset. The +connector will be marked as unhealthy if a message associated with a +record is not acknowledged in +`throttled.unprocessed-record-max-age.ms` (default: 60000). Indeed, +this strategy cannot commit the offset as soon as a single record +processing fails (see failure-strategy to configure what happens on +failing processing). If `throttled.unprocessed-record-max-age.ms` is +set to less than or equal to 0, it does not perform any health check +verification. Such a setting might lead to running out of memory if +there are poison pill messages. This strategy is the default if +`enable.auto.commit` is not explicitly set to `true`. ++ +The `throttled` strategy supports concurrent processing with ordering +guarantees using the `throttled.ordered` configuration. See +xref:#concurrent-processing-with-ordering-guarantees[Concurrent Processing with Ordering Guarantees] +for more details. +* `checkpoint` allows persisting consumer offsets on a "state store", +instead of committing them back to the Kafka broker. Using the +`CheckpointMetadata` API, consumer code can persist a processing +state with the offset to mark the progress of a consumer. +When the processing continues from a previously persisted offset, +it seeks the Kafka consumer to that offset and also restores the +persisted state, continuing the stateful processing from where it +left off. The `checkpoint` strategy holds locally the processing +state associated with the latest offset, and persists it +periodically to the state store (period specified by +`auto.commit.interval.ms` (default: 5000)). The connector will be +marked as unhealthy if no processing state is persisted to the state +store in `checkpoint.unsynced-state-max-age.ms` (default: 10000). +Using the `CheckpointMetadata` API the user code can force to persist +the state on message ack. If `checkpoint.unsynced-state-max-age.ms` +is set to less than or equal to 0, it does not perform any health +check verification. For more information, see +xref:#stateful-processing-with-checkpointing[Stateful processing with Checkpointing] +* `latest` commits the record offset received by the Kafka consumer as +soon as the associated message is acknowledged (if the offset is +higher than the previously committed offset). This strategy provides +_at-least-once_ delivery if the channel processes the message +without performing any asynchronous processing. This strategy should +not be used on high-load as offset commit is expensive. However, it +reduces the risk of duplicates. +* `ignore` performs no commit. This strategy is the default strategy +when the consumer is explicitly configured with `enable.auto.commit` +to `true`. It delegates the offset commit to the Kafka client. When +`enable.auto.commit` is `true` this strategy *DOES NOT* guarantee +at-least-once delivery. However, if the processing failed between +two commits, messages received after the commit and before the +failure will be re-processed. + +[IMPORTANT] +==== + +The Kafka connector disables the Kafka _auto commit_ if not explicitly +enabled. This behavior differs from the traditional Kafka consumer. + +==== + +If high-throughout is important for you, and not limited by the +downstream, we recommend to either: + +* Use the `throttled` policy +* or set `enable.auto.commit` to `true` and annotate the consuming +method with `@Acknowledgment(Acknowledgment.Strategy.NONE)` + +== Concurrent Processing with Ordering Guarantees + +[WARNING] +.Experimental +==== + +Ordered concurrent processing is experimental, and APIs and features are subject to change. + +==== + +When processing Kafka messages concurrently (using `@Blocking(ordered = false)`), +you can configure ordering guarantees using the `ordered` attribute. + +=== Choosing a Commit Strategy + +* *`ordered=key`*: Messages from the same partition but with different keys can be acknowledged +out of order. Use `commit-strategy=throttled`, which tracks out-of-order acknowledgements and +commits offsets correctly. +* *`ordered=partition`*: Messages within a partition are processed sequentially, so +acknowledgements are always in order. This works with any commit strategy, including +`commit-strategy=ignore` for exactly-once transactional processing. + +=== Processing Order Modes + +* *`unordered`* (default): Messages are processed without any ordering guarantees, +allowing maximum concurrency across all partitions and keys. +* *`key`*: Messages with the same key from the same topic-partition are +processed sequentially, while messages with different keys or from different +partitions can be processed concurrently. This ensures per-key ordering within +each partition. +* *`partition`*: Messages from the same topic-partition are processed +sequentially, but messages from different partitions can be processed concurrently. +This ensures partition-level ordering while still allowing parallelism across +partitions. + +=== Examples + +Configure the `ordered` property and use the `@Blocking(ordered = false)` annotation +to enable concurrent processing with ordering guarantees: + +[source,properties] +---- +mp.messaging.incoming.orders.connector=smallrye-kafka +mp.messaging.incoming.orders.topic=orders +mp.messaging.incoming.orders.commit-strategy=throttled +mp.messaging.incoming.orders.ordered=key + +# Configure worker pool concurrency for thread pool size +smallrye.messaging.worker.order-pool.max-concurrency=10 + +# Configure merge concurrency (how many groups can process concurrently) +# Should typically match or be less than worker pool max-concurrency +mp.messaging.incoming.orders.ordered.max-concurrency=10 +---- + +[source,java] +---- +@Incoming("orders") +@Blocking(ordered = false, value = "order-pool") +public void processOrder(Order order) { + // Orders with the same key are processed sequentially + // Orders with different keys can be processed concurrently +} +---- + +For exactly-once transactional processing, use `ordered=partition` with `commit-strategy=ignore` +to avoid dual offset commits: + +[source,properties] +---- +mp.messaging.incoming.data.connector=smallrye-kafka +mp.messaging.incoming.data.topic=data +mp.messaging.incoming.data.commit-strategy=ignore +mp.messaging.incoming.data.ordered=partition +---- + +In order to allow creating more groups (of keys or partitions) that can be processed concurrently, +each group buffers ordered messages to process. +The `max-queue-size-factor` (default: 2) attribute controls the buffer size per group, calculated as +`max.poll.records ++*++ max-queue-size-factor`. +The `ordered.max-concurrency` attribute controls how many groups can be processed concurrently. +If not set, it defaults to `max.poll.records`. +When buffers of all groups are full, backpressure is applied to pause consumption. + +[NOTE] +.Deprecation +==== + +The `throttled.ordered` and `throttled.ordered.max-concurrency` attributes are deprecated. +Use `ordered` and `ordered.max-concurrency` instead, which work with any commit strategy. + +==== + +== Failure Management + +If a message produced from a Kafka record is _nacked_, a failure +strategy is applied. The Kafka connector supports 3 strategies: + +* `fail` - fail the application, no more records will be processed. +(default) The offset of the record that has not been processed +correctly is not committed. +* `ignore` - the failure is logged, but the processing continue. The +offset of the record that has not been processed correctly is +committed. +* `dead-letter-queue` - the offset of the record that has not been +processed correctly is committed, but the record is written to a +(Kafka) _dead letter queue_ topic. +* `delayed-retry-topic` - the offset of the record that has not been +processed correctly is still committed, but the record is written to +a series of Kafka topics for retrying the processing with some delay. +This allows retrying failed records by reconsuming them later without +blocking the processing of the latest records. + +The strategy is selected using the `failure-strategy` attribute. + +=== Dead Letter Queue + +In the case of `dead-letter-queue`, you can configure the following +attributes: + +* `dead-letter-queue.topic`: the topic to use to write the records not +processed correctly, default is `dead-letter-topic-$channel`, with +`$channel` being the name of the channel. +* `dead-letter-queue.producer-client-id`: the client id used by the kafka +producer when sending records to dead letter queue topic. If not specified +it will default to `kafka-dead-letter-topic-producer-$client-id`, with $client-id +being the value obtained from consumer client id. +* `dead-letter-queue.key.serializer`: the serializer used to write the +record key on the dead letter queue. By default, it deduces the +serializer from the key deserializer. +* `dead-letter-queue.value.serializer`: the serializer used to write +the record value on the dead letter queue. By default, it deduces +the serializer from the value deserializer. + +The record written on the dead letter topic contains the original +record’s headers, as well as a set of additional headers about the +original record: + +* `dead-letter-reason` - the reason of the failure (the `Throwable` +passed to `nack()`) +* `dead-letter-cause` - the cause of the failure (the `getCause()` of +the `Throwable` passed to `nack()`), if any +* `dead-letter-topic` - the original topic of the record +* `dead-letter-partition` - the original partition of the record +(integer mapped to String) +* `dead-letter-offset` - the original offset of the record (long +mapped to String) + +When using `dead-letter-queue`, it is also possible to change some +metadata of the record that is sent to the dead letter topic. To do +that, use the `Message.nack(Throwable, Metadata)` method: + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaDeadLetterExample.java[tags=code,indent=0] +---- + +The `Metadata` may contain an instance of `OutgoingKafkaRecordMetadata`. +If the instance is present, the following properties will be used: + +* key; if not present, the original record’s key will be used +* topic; if not present, the configured dead letter topic will be used +* partition; if not present, partition will be assigned automatically +* headers; combined with the original record’s headers, as well as the +`dead-letter-++*++` headers described above + +=== Delayed Retry Topic + +[NOTE] +==== + +Delayed retry topic feature is experimental. + +==== + +The delayed retry topic strategy allows failed records to be automatically retried by forwarding them to a series of retry topics. +Each retry topic is associated with a specific delay time, which is expressed in milliseconds. +When a record processing fails, it is forwarded to the first retry topic. +The failure strategy then consumes these records and dispatches them to be retried again once the delay time of the topic has elapsed. + +If the processing of a record fails again, the message is forwarded to the next topic in the list, with possibly a longer delay time. +If the processing of a record keeps failing, it will eventually be abandoned. +Alternatively, if the `dead-letter-queue.topic` property is configured, the record will be sent to the dead letter queue. + +The Kafka producer client used when forwarding records to retry topics can be configured using the _dead-letter-queue_ properties +namely, `dead-letter-queue.producer-client-id`, `dead-letter-queue.key.serializer` and `dead-letter-queue.value.serializer`. + +Delayed retry topics and delays can be configured with following attributes: + +* `delayed-retry-topic.topics` : +The comma-separated list of retry topics, each one suffixed with `++_[++DELAY++_++IN++_++MILLISECONDS++]++` for indicating the delay time. +For example, `my++_++retry++_++topic++_++2000,my++_++retry++_++topic++_++4000,my++_++retry++_++topic++_++10000` will use three topics +_my++_++retry++_++topic++_++2000_, _my++_++retry++_++topic++_++4000_ and _my++_++retry++_++topic++_++10000_, with 2000ms 4000ms and 10000ms respectively. ++ +If not configured the source channel name is used, with 10, 20 and 50 seconds of delay, +ex. for a channel named `source`, retry topics will be `source++_++retry++_++10000`, `source++_++retry++_++20000`, `source++_++retry++_++50000`. +* `delayed-retry-topic.max-retries` : +The maximum number of retries before abandoning the retries. +If configured higher than the number of retry topics the last topic is used until maximum number of retries is reached. +This can be configured to use a single retry topic with a fixed delay and multiple retries. ++ +For example, `delayed-retry-topic.topics=source++_++retry++_++10000` and `delayed-retry-topic.max-retries=4` will forward failed records +to the topic _source++_++retry++_++10000_ with maximum of 4 retries. +* `delayed-retry-topic.timeout` : +The global timeout in milliseconds for a retried record. +The timeout is calculated from the first failure for a record. +If the next retry will reach the timeout, instead of forwarding to the retry topic the retry is abandoned and, +if configured, the record is forwarded to the dead letter queue. ++ +The default is 120 seconds. + +[IMPORTANT] +==== + +While you can use xref:#retrying-processing[Smallrye Fault Tolerance to retry processing], +it will block the processing of further messages until the retried record is processed successfully, or abandoned. + +Delayed retry topic failure strategy allows effectively implementing non-blocking retries. +But it will not preserve the order of messages inside a topic-partition. + +==== + +The record written on the delayed retry topics will preserve the key and partition of the original record. +It also contains the original record’s headers, as well as a set of additional headers about the original record: + +* `delayed-retry-count` the current number of retries +* `delayed-retry-original-timestamp` the original timestamp of the record +* `delayed-retry-first-processing-timestamp` the first processing timestamp of the record +* `delayed-retry-reason` the reason of the failure (the `Throwable` passed to `nack()`) +* `delayed-retry-cause` the cause of the failure (the `getCause()` of the `Throwable` passed to `nack()`), if any +* `delayed-retry-topic` the original topic of the record +* `delayed-retry-partition` the original partition of the record +* `delayed-retry-offset` the original offset of the record +* `delayed-retry-exception-class-name` the class name of the throwable passed to `nack()` +* `delayed-retry-cause-class-name` the class name of the the `getCause()` of the `Throwable` passed to `nack()`, if any + +As for the dead letter queue it is possible to change forwarded values by providing a `OutgoingKafkaRecordMetadata` +when the message is nacked using `Message.nack(Throwable, Metadata)`. + +[NOTE] +.Multiple partitions +==== + +The delayed retry topic strategy does not create retry topics automatically. +If the source topic has multiple partitions, delayed retry and dead letter queue topics would need to be setup with the same number of partitions. + +It is possible to scale consumer application instances according to the number of partitions. +But it is not guaranteed that the retry topics consumer will be assigned the same partition(s) as the main topic consumer. +Therefore, retry processing of a record can happen in an other instance. + +==== + +== Custom commit and failure strategies + +In addition to provided strategies, it is possible to implement custom +commit and failure strategies and configure Kafka channels with them. + +For example, for a custom commit strategy, implement the +link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-kafka/{javadoc-version}/io/smallrye/reactive/messaging/kafka/commit/KafkaCommitHandler.html[`KafkaCommitHandler`] interface, +and provide a managed bean implementing the `KafkaCommitHandler.Factory` interface, +identified using `@Identifier` qualifier. + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaCustomCommit.java[indent=0] +---- + +Finally, to use the custom commit strategy, +set the `commit-strategy` attribute to the identifier of the commit handler factory: +`mp.messaging.incoming.$channel.commit-strategy=custom`. +Similarly, custom failure strategies can be configured using `failure-strategy` attribute. + +[NOTE] +==== + +If the custom strategy implementation inherits +link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-kafka/{javadoc-version}/io/smallrye/reactive/messaging/kafka/commit/ContextHolder.html[`ContextHolder`] class it can access the +Vert.x event-loop context created for the Kafka consumer + +==== + +== Retrying processing + +You can combine Reactive Messaging with https://github.com/smallrye/smallrye-fault-tolerance[SmallRye Fault +Tolerance], and +retry processing when it fails: + +[source,java] +---- +@Incoming("kafka") +@Outgoing("processed") +@Retry(delay = 10, maxRetries = 5) +public String process(String v) { + // ... retry if this method throws an exception +} +---- + +You can configure the delay, the number of retries, the jitter... + +If your method returns a `Uni`, you need to add the `@NonBlocking` +annotation: + +[source,java] +---- +@Incoming("kafka") +@Outgoing("processed") +@Retry(delay = 10, maxRetries = 5) +@NonBlocking +public Uni process(String v) { + // ... retry if this method throws an exception or the returned Uni produce a failure +} +---- + +The incoming messages are acknowledged only once the processing +completes successfully. So, it commits the offset after the successful +processing. If after the retries the processing still failed, the +message is _nacked_ and the failure strategy is applied. + +You can also use `@Retry` on methods only consuming incoming messages: + +[source,java] +---- +@Incoming("kafka") +@Retry(delay = 10, maxRetries = 5) +public void consume(String v) { + // ... retry if this method throws an exception +} +---- + +== Handling deserialization failures + +Because deserialization happens before creating a `Message`, the failure +strategy presented above cannot be applied. However, when a +deserialization failure occurs, you can intercept it and provide a +fallback value. To achieve this, create a CDI bean implementing the +link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-kafka/{javadoc-version}/io/smallrye/reactive/messaging/kafka/api/DeserializationFailureHandler.html[`DeserializationFailureHandler`] +interface: + +[source,java] +---- +@ApplicationScoped +@Identifier("failure-retry") // Set the name of the failure handler +public class MyDeserializationFailureHandler + implements DeserializationFailureHandler { // Specify the expected type + + @Override + public JsonObject decorateDeserialization(Uni deserialization, + String topic, boolean isKey, String deserializer, byte[] data, + Headers headers) { + return deserialization + .onFailure().retry().atMost(3) + .await().atMost(Duration.ofMillis(200)); + } +} +---- + +The bean must be exposed with the `@Identifier` qualifier specifying the +name of the bean. Then, in the connector configuration, specify the +following attribute: + +* `mp.messaging.incoming.$channel.key-deserialization-failure-handler`: +name of the bean handling deserialization failures happening for the +record’s key +* `mp.messaging.incoming.$channel.value-deserialization-failure-handler`: +name of the bean handling deserialization failures happening for the +record’s value, + +The handler is called with the deserialization action as a `Uni++<++T++>++`, the +record’s topic, a boolean indicating whether the failure happened on a +key, the class name of the deserializer that throws the exception, the +corrupted data, the exception, and the records headers augmented with +headers describing the failure (which ease the write to a dead letter). +On the deserialization `Uni` failure strategies like retry, providing a +fallback value or applying timeout can be implemented. Note that the +method must await on the result and return the deserialized object. +Alternatively, the handler can only implement +`handleDeserializationFailure` method and provide a fallback value, +which may be `null`. + +If you don’t configure a deserialization failure handlers and a +deserialization failure happens, the application is marked unhealthy. +You can also ignore the failure, which will log the exception and +produce a `null` value. To enable this behavior, set the +`mp.messaging.incoming.$channel.fail-on-deserialization-failure` +attribute to `false`. + +If the `fail-on-deserialization-failure` attribute is set to `false` and +the `failure-strategy` attribute is `dead-letter-queue` the failed record +will be sent to the corresponding _dead letter queue_ topic. +The forwarded record will have the original key and value, +and the following headers set: + +* `deserialization-failure-reason`: The deserialization failure message +* `deserialization-failure-cause`: The deserialization failure cause if any +* `deserialization-failure-key`: Whether the deserialization failure happened on a key +* `deserialization-failure-topic`: The topic of the incoming message when a deserialization failure happen +* `deserialization-failure-deserializer`: The class name of the underlying deserializer +* `deserialization-failure-key-data`: If applicable the key data that was not able to be deserialized +* `deserialization-failure-value-data`: If applicable the value data that was not able to be deserialized + +== Receiving Cloud Events + +The Kafka connector supports https://cloudevents.io/[Cloud Events]. +When the connector detects a _structured_ or _binary_ Cloud Events, it +adds a link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-kafka/{javadoc-version}/io/smallrye/reactive/messaging/kafka/IncomingKafkaCloudEventMetadata.html[`IncomingKafkaCloudEventMetadata`] in the metadata of the +Message. `IncomingKafkaCloudEventMetadata` contains the various (mandatory and optional) Cloud Event attributes. + +If the connector cannot extract the Cloud Event metadata, it sends the +Message without the metadata. + +=== Binary Cloud Events + +For `binary` Cloud Events, *all* mandatory Cloud Event attributes must +be set in the record header, prefixed by `ce++_++` (as mandated by the +https://github.com/cloudevents/spec/blob/v1.0/kafka-protocol-binding.md[protocol +binding]). +The connector considers headers starting with the `ce++_++` prefix but not +listed in the specification as extensions. You can access them using the +`getExtension` method from `IncomingKafkaCloudEventMetadata`. You can +retrieve them as `String`. + +The `datacontenttype` attribute is mapped to the `content-type` header +of the record. The `partitionkey` attribute is mapped to the record’s +key, if any. + +Note that all headers are read as UTF-8. + +With binary Cloud Events, the record’s key and value can use any +deserializer. + +=== Structured Cloud Events + +For `structured` Cloud Events, the event is encoded in the record’s +value. Only JSON is supported, so your event must be encoded as JSON in +the record’s value. + +Structured Cloud Event must set the `content-type` header of the record +to `application/cloudevents` or prefix the value with +`application/cloudevents` such as: +`application/cloudevents{plus}json; charset=UTF-8`. + +To receive structured Cloud Events, your value deserializer must be: + +* `org.apache.kafka.common.serialization.StringDeserializer` +* `org.apache.kafka.common.serialization.ByteArrayDeserializer` +* `io.vertx.kafka.client.serialization.JsonObjectDeserializer` + +As mentioned previously, the value must be a valid JSON object +containing at least all the mandatory Cloud Events attributes. + +If the record is a structured Cloud Event, the created Message’s payload +is the Cloud Event `data`. + +The `partitionkey` attribute is mapped to the record’s key if any. + +== Consumer Rebalance Listener + +To handle offset commit and assigned partitions yourself, you can +provide a consumer rebalance listener. To achieve this, implement the +`io.smallrye.reactive.messaging.kafka.KafkaConsumerRebalanceListener` +interface, make the implementing class a bean, and add the `@Identifier` +qualifier. A usual use case is to store offset in a separate data store +to implement exactly-once semantic, or starting the processing at a +specific offset. + +The listener is invoked every time the consumer topic/partition +assignment changes. For example, when the application starts, it invokes +the `partitionsAssigned` callback with the initial set of +topics/partitions associated with the consumer. If, later, this set +changes, it calls the `partitionsRevoked` and `partitionsAssigned` +callbacks again, so you can implement custom logic. + +Note that the rebalance listener methods are called from the Kafka +_polling_ thread and must block the caller thread until completion. +That’s because the rebalance protocol has synchronization barriers, and +using asynchronous code in a rebalance listener may be executed after +the synchronization barrier. + +When topics/partitions are assigned or revoked from a consumer, it +pauses the message delivery and restarts once the rebalance completes. + +If the rebalance listener handles offset commit on behalf of the user +(using the `ignore` commit strategy), the rebalance listener *must* +commit the offset synchronously in the `partitionsRevoked` callback. We +also recommend applying the same logic when the application stops. + +Unlike the `ConsumerRebalanceListener` from Apache Kafka, the +`io.smallrye.reactive.messaging.kafka.KafkaConsumerRebalanceListener` +methods pass the Kafka `Consumer` and the set of topics/partitions. + +=== Example + +In this example we set-up a consumer that always starts on messages from +at most 10 minutes ago (or offset 0). First we need to provide a bean +that implements the +`io.smallrye.reactive.messaging.kafka.KafkaConsumerRebalanceListener` +interface and is annotated with `@Identifier`. We then must configure +our inbound connector to use this named bean. + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaRebalancedConsumerRebalanceListener.java[indent=0] +---- + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaRebalancedConsumer.java[indent=0] +---- + +To configure the inbound connector to use the provided listener we +either set the consumer rebalance listener’s name: + +* `mp.messaging.incoming.rebalanced-example.consumer-rebalance-listener.name=rebalanced-example.rebalancer` + +Or have the listener’s name be the same as the group id: + +* `mp.messaging.incoming.rebalanced-example.group.id=rebalanced-example.rebalancer` + +Setting the consumer rebalance listener’s name takes precedence over +using the group id. + +== Receiving Kafka Records in Batches + +By default, incoming methods receive each Kafka record individually. +Under the hood, Kafka consumer clients poll the broker constantly and +receive records in batches, presented inside the `ConsumerRecords` +container. + +In *batch* mode, your application can receive all the records returned +by the consumer *poll* in one go. + +To achieve this you need to set +`mp.messaging.incoming.$channel.batch=true` *and* specify a compatible +container type to receive all the data: + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaRecordBatchPayloadExample.java[tags=code,indent=0] +---- + +The incoming method can also receive `Message++<++List++<++Payload++>++` or `ConsumerRecords++<++Key, Payload++>++` types, They +give access to record details such as offset or timestamp : + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaRecordBatchExample.java[tags=code,indent=0] +---- + +Note that the successful processing of the incoming record batch will +commit the latest offsets for each partition received inside the batch. +The configured commit strategy will be applied for these records only. + +Conversely, if the processing throws an exception, all messages are +_nacked_, applying the failure strategy for all the records inside the +batch. + +=== Accessing metadata of batch records + +When receiving records in batch mode, the metadata of each record is accessible through the `IncomingKafkaRecordBatchMetadata` : + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaRecordBatchExample.java[tags=batch,indent=0] +---- + +Like in this example, this can be useful to propagate the tracing information of each record. + +== Manual topic-partition assignment + +The default behavior of Kafka incoming channels is to subscribe to one or more topics in order to receive records from the Kafka broker. +Channel attributes `topic` and `topics` allow specifying topics to subscribe to, +or `pattern` attribute allows to subscribe to all topics matching a regular expression. +Subscribing to topics allows partitioning consumption of topics by dynamically assigning (rebalancing) partitions between members of a consumer group. + +The `assign-seek` configuration attribute allows manually assigning topic-partitions to a Kafka incoming channel, +and optionally seek to a specified offset in the partition to start consuming records. +If `assign-seek` is used, the consumer will not be dynamically subscribed to topics, +but instead will statically assign the described partitions. +In manual topic-partition rebalancing doesn't happen and therefore rebalance listeners are never called. + +The attribute takes a list of triplets separated by commas: `++<++topic++>++:++<++partition++>++:++<++offset++>++`. + +For example, the following configuration + +[source,properties] +---- +mp.messaging.incoming.data.assign-seek=topic1:0:10, topic2:1:20 +---- + +assigns the consumer to: +- Partition 0 of topic 'topic1', setting the initial position at offset 10. +- Partition 1 of topic 'topic2', setting the initial position at offset 20. + +The topic, partition, and offset in each triplet can have the following variations: +- If the topic is omitted, the configured `topic` will be used. +- If the offset is omitted, partitions are assigned to the consumer but won't be seeked to offset. +- If offset is 0, it seeks to the beginning of the topic-partition. +- If offset is -1, it seeks to the end of the topic-partition. + +== Stateful processing with Checkpointing + +[WARNING] +.Experimental +==== + +Checkpointing is experimental, and APIs and features are subject +to change in the future. + +==== + +The `checkpoint` commit strategy allows for a Kafka incoming channel to +manage topic-partition offsets, not by committing on the Kafka broker, +but by persisting consumers' advancement on a +xref:#implementing-state-stores[_state store_]. + +In addition to that, if the consumer builds an internal state as +a result of consumed records, the topic-partition offset persisted +to the state store can be associated with a _processing state_, +saving the local state to the persistent store. When a consumer +restarts or consumer group instances scale, i.e. when new partitions +get assigned to the consumer, the checkpointing works by resuming the +processing from the latest offset and its saved state. + +The `@Incoming` channel consumer code can manipulate the processing +state through the `CheckpointMetadata` API: + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaCheckpointExample.java[tags=code,indent=0] +---- + +The `transform` method allows applying a transformation function to +the current state, producing a changed state and registering it +locally for checkpointing. By default, the local state is synced +(persisted) to the state store periodically, period specified by +`auto.commit.interval.ms`, (default: 5000). If `persistOnAck` flag +is given, the latest state is persisted to the state store eagerly +on message acknowledgment. The `setNext` method works similarly +directly setting the latest state. + +The `checkpoint` commit strategy tracks when a processing state +is last persisted for each topic-partition. If an outstanding state +change can not be persisted for `checkpoint.unsynced-state-max-age.ms` +(default: 10000), the channel is marked unhealthy. + +Where and how processing states are persisted is decided by the +state store implementation. This can be configured on the incoming +channel using `checkpoint.state-store` configuration property, +using the state store factory identifier name. +The serialization of state objects depends on the state store +implementation. In order to instruct state stores for serialization +can require configuring the type name of state objects +using `checkpoint.state-type` property. + +In order to keep Smallrye Reactive Messaging free of persistence-related +dependencies, this library includes only a default state store named `file`. +It is based on Vert.x Filesystem API and stores the processing state +in Json formatted files, in a local directory configured by the +`checkpoint.file.state-dir` property. State files follow the naming +scheme `++[++consumer-group-id++]++:++[++topic++]++:++[++partition++]++`. + +=== Implementing State Stores + +State store implementations are required to implement `CheckpointStateStore` +interface, and provide a managed bean implementing +`CheckpointStateStore.Factory`, identified with `@Identifier` bean +qualifier indicating the name of the state-store. +The factory bean identifier indicates the name to configure on +`checkpoint.state-store` config property. +The factory is discovered as a CDI managed bean and state store is +created once per channel: + +[source,java] +---- +include::{examples-dir}/kafka/inbound/MyCheckpointStateStore.java[indent=0] +---- + +The checkpoint commit strategy calls the state store in following events: + +* `fetchProcessingState` : on partitions assigned, to seek the consumer to the latest offset. +* `persistProcessingState` on partitions revoked, to persist the state of last processed record. +* `persistProcessingState` on message acknowledgement, if a new state is set during the processing and `persistOnAck` flag is set. +* `persistProcessingState` on `auto.commit.interval.ms` intervals, if a new state is set during processing. +* `persistProcessingState` on channel shutdown. +* `close` on channel shutdown. + +== Receiving Kafka Records Using Kafka Queues (Share Groups) + +[WARNING] +.Experimental +==== + +Kafka Queues (Share Groups) support is experimental. + +==== + +https://cwiki.apache.org/confluence/display/KAFKA/KIP-932[Kafka Share Groups] provide a new consumption model where records from a topic are cooperatively distributed across consumers without explicit partition assignment. +Unlike traditional consumer groups, share groups do not require managing offsets or partition rebalancing -- the broker handles record distribution and acquisition locks automatically. + +This is useful for *queue-like* workloads where you want records to be processed by exactly one consumer in a group, without worrying about partition count or consumer coordination. + +=== Enabling Share Groups + +To enable share group consumption on a channel, set the `share-group.enabled` attribute to `true`: + +[source,properties] +---- +mp.messaging.incoming.queue.connector=smallrye-kafka +mp.messaging.incoming.queue.topic=prices +mp.messaging.incoming.queue.value.deserializer=org.apache.kafka.common.serialization.DoubleDeserializer +mp.messaging.incoming.queue.share-group.enabled=true # <1> +---- + +[arabic] +. Enables share group consumption for this channel. + +Then consume records as usual: + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaShareGroupConsumer.java[indent=0] +---- + +By default, successfully processed records are acknowledged with `ACCEPT`. +If the processing method throws an exception, the record is released (`RELEASED`) and will be attempted for re-delivery. + +=== Controlling Acknowledgement + +Share groups support four acknowledgement types for each record: + +* *`ACCEPT`* Record processed successfully (default on ack). +* *`RELEASE`* Record could not be processed; release it for re-delivery to another consumer (default on nack). +* *`REJECT`* Record is permanently rejected and will not be re-delivered. +* *`RENEW`* Extend the acquisition lock for long-running processing (managed automatically when processing timeout is enabled). + +You can control the acknowledgement type using the `ShareGroupAcknowledgement` metadata, which is injected as a parameter in the consuming method: + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaShareGroupAckExample.java[tags=code,indent=0] +---- + +When the message is acked (or method completes normally), the connector uses the acknowledgement type set on `ShareGroupAcknowledgement`. +If no type is explicitly set, the default is `ACCEPT`. + +When calling `nack()` on the `Message`, you can pass the desired acknowledgement type via metadata: + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaShareGroupNackExample.java[tags=code,indent=0] +---- + +If no metadata is provided on nack, the default acknowledgement type is `RELEASE`. +This can be changed using the `share-group.failure-acknowledgement-type` attribute: + +[source,properties] +---- +mp.messaging.incoming.queue.share-group.enabled=true +mp.messaging.incoming.queue.share-group.failure-acknowledgement-type=reject # <1> +---- + +[arabic] +. Nacked records are permanently rejected instead of released for re-delivery. + +=== Handling Deserialization Failures + +When `fail-on-deserialization-failure` is set to `false`, records that fail deserialization are not forwarded to the consumer method. +They are automatically nacked using the acknowledgement type configured by `share-group.failure-deserialization-acknowledgement-type` (default: `reject`). + +[source,properties] +---- +mp.messaging.incoming.queue.share-group.enabled=true +mp.messaging.incoming.queue.fail-on-deserialization-failure=false # <1> +mp.messaging.incoming.queue.share-group.failure-deserialization-acknowledgement-type=reject # <2> +---- + +[arabic] +. Deserialization failures do not kill the application. +. Records that fail deserialization are permanently rejected (default). + +A custom `DeserializationFailureHandler` can override the acknowledgement type per record by setting the `deserialization-failure-share-ack-type` header +to the set `AcknowledgeType` value (e.g., `RELEASE`, `REJECT`, `ACCEPT`). + +=== Acquisition Lock Renewal and Processing Timeout + +Each record acquired by a share group consumer has a time-limited *acquisition lock* (default 30 seconds, configured on the broker via `group.share.record.lock.duration.ms`). +If the consumer does not acknowledge the record before the lock expires, the broker automatically releases it for re-delivery. + +The connector can periodically send `RENEW` acknowledgements to extend the lock for records still being processed. +This is controlled by the `share-group.unprocessed-record-max-age.ms` attribute: + +* When set to a value greater than `0` (default: `60000`), the connector enables *processing timeout monitoring*: +** Records still in progress are periodically renewed to prevent lock expiry. +** If a record exceeds the configured timeout, the connector reports a failure and marks the channel as unhealthy. +* When set to `0`, processing timeout monitoring is disabled. No `RENEW` acknowledgements are sent. If processing exceeds the broker's lock duration, the record will be automatically released and re-delivered. + +[source,properties] +---- +mp.messaging.incoming.queue.share-group.enabled=true +mp.messaging.incoming.queue.share-group.unprocessed-record-max-age.ms=30000 # <1> +---- + +[arabic] +. Records not processed within 30 seconds trigger a failure report. + +=== Batch Mode + +Share groups support batch consumption. Set `batch=true` to receive all records from a single poll as a batch: + +[source,properties] +---- +mp.messaging.incoming.queue.share-group.enabled=true +mp.messaging.incoming.queue.batch=true +---- + +When a batch is acknowledged, all individual records in the batch are acknowledged. +When a batch is nacked, all records are nacked with the specified acknowledgement type. + +You can control the acknowledgement type per record within a batch using the `ShareGroupAcknowledgement` metadata accessible through `IncomingKafkaRecordBatchMetadata`: + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaShareGroupBatchAckExample.java[tags=code,indent=0] +---- + +Each record in the batch has its own `ShareGroupAcknowledgement` instance. +When the batch is acked, each record is individually acknowledged using its configured type. +This allows accepting some records while rejecting or releasing others within the same batch. + +=== Incompatible Configuration + +The following configuration attributes are not supported with share groups and will be ignored: + +* `pattern` Share groups subscribe to explicit topics only. +* `assign-seek` Share groups do not support manual partition assignment. +* `partitions` Share groups handle record distribution automatically; multiple consumer instances are not needed. +* `commit-strategy` Share groups use their own acknowledgement mechanism. +* `failure-strategy` Share groups use their own failure handling based on `AcknowledgeType`. +* `consumer-rebalance-listener.name` Share groups do not have traditional partition rebalancing. + +If any of these are configured alongside `share-group.enabled=true`, a warning is logged at startup. + +== Configuration Reference + +include::{generated-dir}/smallrye-kafka-incoming.adoc[indent=0] + +You can also pass any property supported by the underlying https://kafka.apache.org/documentation/#consumerconfigs[Kafka +consumer]. + +For example, to configure the `max.poll.records` property, use: + +[source,properties] +---- +mp.messaging.incoming.[channel].max.poll.records=1000 +---- + +Some consumer client properties are configured to sensible default +values: + +If not set, `reconnect.backoff.max.ms` is set to `10000` to avoid high +load on disconnection. + +If not set, `key.deserializer` is set to +`org.apache.kafka.common.serialization.StringDeserializer`. + +The consumer `client.id` is configured according to the number of +clients to create. + +[WARNING] +.Deprecated +==== + +The `partitions` property (`mp.messaging.incoming.++[++channel++]++.partitions`) and the associated `requests` property are *deprecated*. +Use the `concurrency` channel attribute instead, which creates distinct indexed channels with their own consumer. + +==== + +* If a `client.id` is provided, it is used as-is or suffixed with +client index if the deprecated `partitions` property is set. +* If a `client.id` is not provided, it is generated as +`kafka-consumer-++[++channel++][++-index++]++`. diff --git a/content/docs/smallrye-reactive-messaging/4/kafka/request-reply.adoc b/content/docs/smallrye-reactive-messaging/4/kafka/request-reply.adoc new file mode 100644 index 000000000..4aedd2cc9 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/kafka/request-reply.adoc @@ -0,0 +1,175 @@ += Kafka Request/Reply +:version-root: ../ +include::{version-root}_attributes.adoc[] + +[WARNING] +.Tech Preview +==== + +Kafka Request Reply Emitter is a tech preview feature. + +==== + +The Kafka https://www.enterpriseintegrationpatterns.com/patterns/messaging/RequestReply.html[Request-Reply] pattern allows you to publish a message to a Kafka topic and then await for a reply message that responds to the initial request. + +The `KafkaRequestReply` emitter implements the requestor (or the client) of the request-reply pattern for Kafka outbound channels: + +[source,java] +---- +include::{examples-dir}/kafka/outbound/KafkaRequestReplyEmitter.java[indent=0] +---- + +The `request` method publishes the request record to the configured target topic of the outgoing channel, +and polls a reply topic (by default, the target topic with `-replies` suffix) for a reply record. +When the reply is received the returned `Uni` is completed with the record value. + +The request send operation generates a correlation id and sets a header (by default `REPLY++_++CORRELATION++_++ID`), +which it expects to be sent back in the reply record. Two additional headers are set on the request record: + +* The topic from which the reply is expected, by default `REPLY++_++TOPIC` header, +which can be configured using the `reply.topic` channel attribute. +* Optionally, the partition from which the reply is expected, by default `REPLY++_++PARTITION` header. +The reply partition header is added only when the Kafka request reply is configured specifically to receive records from a topic-partition +, using the `reply.partition` channel attribute. +The reply partition header integer value is encoded in 4 bytes, +and helper methods `KafkaRequestReply#replyPartitionFromBytes` and `KafkaRequestReply#replyPartitionToBytes` can be used for custom operations. + +The replier (or the server) can be implemented using a Reactive Messaging processor: + +[source,java] +---- +include::{examples-dir}/kafka/outbound/KafkaReplier.java[indent=0] +---- + +Kafka outgoing channels detect default `REPLY++_++CORRELATION++_++ID`, `REPLY++_++TOPIC` and `REPLY++_++PARTITION` headers +and send the reply record to the expected topic-partition by propagating back the correlation id header. + +Default headers can be configured, using `reply.correlation-id.header`, `reply.topic.header` and `reply.partition.header` channel attributes. +If custom headers are used the reply server needs some more manual work. +Given the following request/reply outgoing configuration: + +[source,properties] +---- +mp.messaging.outgoing.reqrep.topic=requests +mp.messaging.outgoing.reqrep.reply.correlation-id.header=MY_CORRELATION +mp.messaging.outgoing.reqrep.reply.topic.header=MY_TOPIC +mp.messaging.outgoing.reqrep.reply.partition.header=MY_PARTITION +mp.messaging.incoming.request.topic=requests +---- + +The reply server can be implemented as the following: + +[source,java] +---- +include::{examples-dir}/kafka/outbound/KafkaCustomHeaderReplier.java[indent=0] +---- + +== Requesting with `Message` types + +Like the core Emitter's `send` methods, `request` method also can receive a `Message` type and return a message: + +[source,java] +---- +include::{examples-dir}/kafka/outbound/KafkaRequestReplyMessageEmitter.java[indent=0] +---- + +[NOTE] +==== + +The ingested reply type of the `KafkaRequestReply` is discovered at runtime, +in order to configure a `MessageConveter` to be applied on the incoming message before returning the `Uni` result. + +==== + +== Requesting multiple replies + +You can use the `requestMulti` method to expect any number of replies represented by the `Multi` return type. + +For example this can be used to aggregate multiple replies to a single request. + +[source,java] +---- +include::{examples-dir}/kafka/outbound/KafkaRequestReplyMultiEmitter.java[indent=0] +---- + +Like the other `request` you can also request `Message` types. + +[NOTE] +==== + +The channel attribute `reply.timeout` will be applied between each message, if reached the returned `Multi` will +fail. + +==== + +== Scaling Request/Reply + +If multiple requestor instances are configured on the same outgoing topic, and the same reply topic, +each requestor consumer will generate a unique consumer group.id and +therefore all requestor instances will receive replies of all instances. If an observed correlation id doesn't match +the id of any pending replies, the reply is simply discarded. +With the additional network traffic this allows scaling requestors, (and repliers) dynamically. + +Alternatively, requestor instances can be configured to consume replies from dedicated topics using `reply.topic` attribute, +or distinct partitions of a single topic, using `reply.partition` attribute. +The later will configure the Kafka consumer to assign statically to the given partition. + +== Pending replies and reply timeout + +By default, the `Uni` returned from the `request` method is configured to fail with timeout exception if no replies is received after 5 seconds. +This timeout is configurable with the channel attribute `reply.timeout`. + +A snapshot of the list of pending replies is available through the `KafkaRequestReply#getPendingReplies` method. + +== Waiting for topic-partition assignment + +The requestor can be found in a position where a request is sent, and it's reply is already published to the reply topic, +before the requestor starts and polls the consumer. +In case the reply consumer is configured with `auto.offset.reset=latest`, which is the default value, this can lead to the requestor missing replies. + +If `auto.offset.reset` is `latest`, at wiring time, before any request can take place, the `KafkaRequestReply` +finds partitions that the consumer needs to subscribe and waits for their assignment to the consumer. +The timeout of the initial subscription can be adjusted with `reply.initial-assignment-timeout` which defaults to the `reply.timeout`. +If this timeout fails, `KafkaRequestReply` will enter an invalid state which will require it to be restarted. +If set to `-1`, the `KafkaRequestReply` will not wait for the initial assignment of the reply consumer to sent requests. + +On other occasions the `KafkaRequestReply#waitForAssignments` method can be used. + +== Correlation Ids + +The Kafka Request/Reply allows configuring the correlation id mechanism completely through a `CorrelationIdHandler` implementation. +The default handler is based on randomly generated UUID strings, written to byte array in Kafka record headers. +The correlation id handler implementation can be configured using the `reply.correlation-id.handler` attribute. +As mentioned the default configuration is `uuid`, +and an alternative `bytes` implementation can be used to generate 12 bytes random correlation ids. + +Custom handlers can be implemented by proposing a CDI-managed bean with `@Identifier` qualifier. + +== Reply Error Handling + +If the reply server produces an error and can or would like to propagate the error back to the requestor, failing the returned `Uni`. + +If configured using the `reply.failure.handler` channel attribute, +the `ReplyFailureHandler` implementations are discovered through CDI, matching the `@Identifier` qualifier. + +A sample reply error handler can lookup header values and return the error to be thrown by the reply: + +[source,java] +---- +include::{examples-dir}/kafka/outbound/MyReplyFailureHandler.java[indent=0] +---- + +`null` return value indicates that no error has been found in the reply record, and it can be delivered to the application. + +== Advanced configuration for the Kafka consumer + +The underlying Kafka consumer can be configured with the `reply` property prefix. +For example the underlying Kafka consuemer can be configured to batch mode using: + +[source,properties] +---- +mp.messaging.outgoing.reqrep.topic=requests +mp.messaging.outgoing.reqrep.reply.topic=quote-results +mp.messaging.outgoing.reqrep.reply.batch=true +mp.messaging.outgoing.reqrep.reply.commit-strategy=latest +---- diff --git a/content/docs/smallrye-reactive-messaging/4/kafka/test-companion.adoc b/content/docs/smallrye-reactive-messaging/4/kafka/test-companion.adoc new file mode 100644 index 000000000..d25aa111f --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/kafka/test-companion.adoc @@ -0,0 +1,158 @@ += Kafka Companion +:version-root: ../ +include::{version-root}_attributes.adoc[] + +[WARNING] +.Experimental +==== + +Kafka Companion is experimental and APIs are subject to change in the future. + +==== + +The *Kafka Companion* is a separate Java library for helping to test Kafka applications. +It is not intended to mock Kafka, but to the contrary, connect to a Kafka broker and provide high-level features. + +It is not limited to the SmallRye Reactive Messaging testing, but intends to improve the testability of applications using Kafka. Some of its features: + +* Running In-container Kafka broker for tests via https://github.com/strimzi/test-container[strimzi-test-container]. +* Running the Kafka broker behind a https://github.com/Shopify/toxiproxy[toxiproxy] for simulating network issues. +* Running embedded Kafka Kraft broker for tests. +* Base classes for tests to bootstrap tests. +* Companion classes for easily creating tasks to produce and consume Kafka records. +* Writing assertions for produce and consume tasks, state of consumers, topics, offsets etc. + +== Getting started writing tests + +Easiest way of starting to write Kafka tests is to extend `KafkaCompanionTestBase`. +It starts a single-node Kafka broker for the test suite using testcontainers and creates the `KafkaCompanion` to connect to this broker: + +[source,java] +---- +include::{examples-dir}/kafka/companion/KafkaWithBaseTest.java[tags=code,indent=0] +---- + +`KafkaCompanion` can be used on its own to connect to a broker: + +[source,java] +---- +include::{examples-dir}/kafka/companion/KafkaTest.java[tags=companion,indent=0] +---- + +There are a couple of things to note on how Kafka companion handles *producers*, *consumers* and *tasks*: + +`ProducerBuilder` and `ConsumerBuilder` lazy descriptions of with which configuration to create a producer or a consumer. + +`ProducerTask` and `ConsumerTask` run asynchronously on dedicated threads and are started as soon as they are created. +A task terminates when it is explicitly stopped, when it's predefined duration or number of records has been produced/consumed, or when it encounters an error. +An exterior thread can await on records processed, or simply on termination of the task. +At a given time produced or consumed records are accessible through the task. + +The actual creation of the producer or consumer happens when a task is started. When the task terminates the producer or the consumer is automatically closed. + +For example, in the previous example: + +[arabic] +. We described a producer with a client id `my-producer` and `max.block.ms` of 5 seconds. +. Then we created a task to produce 100 records using the generator function, without waiting for its completion. +. We then described a consumer with group id `my-group` and which commits offset synchronously on every received record. +. Finally, we created a task to consume records for 10 seconds and await its completion. + +== Producing records + +=== Produce from records + +Produce given records: + +[source,java] +---- +include::{examples-dir}/kafka/companion/ProducerTest.java[tags=records,indent=0] +---- + +=== Produce from generator function + +Produce 10 records using the generator function: + +[source,java] +---- +include::{examples-dir}/kafka/companion/ProducerTest.java[tags=generator,indent=0] +---- + +=== Produce from CSV file + +Given a comma-separated file `records.csv` with the following content + +[source,csv] +---- +messages,0,a,asdf +messages,1,b,asdf +messages,3,c,asdf +---- + +Produce records from the file: + +[source,java] +---- +include::{examples-dir}/kafka/companion/ProducerTest.java[tags=csv,indent=0] +---- + +== Consuming records + +=== Consume from topics + +[source,java] +---- +include::{examples-dir}/kafka/companion/ConsumerTest.java[tags=topics,indent=0] +---- + +=== Consume from offsets + +[source,java] +---- +include::{examples-dir}/kafka/companion/ConsumerTest.java[tags=offsets,indent=0] +---- + +=== Consumer assignment and offsets + +During execution of the consumer task, information about the underlying consumer's topic partition assignment, position or committed offsets can be accessed. + +[source,java] +---- +include::{examples-dir}/kafka/companion/ConsumerTest.java[tags=committed,indent=0] +---- + +== Registering Custom Serdes + +KafkaCompanion handles Serializers and Deserializers for default types such as primitives, String, ByteBuffer, UUID. + +Serdes for custom types can be registered to the companion object, and will be used for producer and consumer tasks: + +[source,java] +---- +include::{examples-dir}/kafka/companion/KafkaTest.java[tags=serdes,indent=0] +---- + +== Topics + +Create, list, describe and delete topics: + +[source,java] +---- +include::{examples-dir}/kafka/companion/TopicsTest.java[tags=code,indent=0] +---- + +== Consumer Groups and Offsets + +List topic partition offsets: + +[source,java] +---- +include::{examples-dir}/kafka/companion/OffsetsTest.java[tags=offsets,indent=0] +---- + +List, describe, alter consumer groups and their offsets: + +[source,java] +---- +include::{examples-dir}/kafka/companion/OffsetsTest.java[tags=consumerGroups,indent=0] +---- diff --git a/content/docs/smallrye-reactive-messaging/4/kafka/transactions.adoc b/content/docs/smallrye-reactive-messaging/4/kafka/transactions.adoc new file mode 100644 index 000000000..927012f01 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/kafka/transactions.adoc @@ -0,0 +1,165 @@ += Kafka Transactions and Exactly-Once Processing +:version-root: ../ +include::{version-root}_attributes.adoc[] + +https://cwiki.apache.org/confluence/display/KAFKA/KIP-98+-+Exactly+Once+Delivery+and+Transactional+Messaging[Kafka transactions] enable atomic writes to multiple Kafka topics and partitions. +Inside a transaction, a producer writes records to the Kafka topic partitions as it would normally do. +If the transaction completes successfully, all the records previously written to the broker inside that transaction will be _committed_, and will be readable for consumers. +If an error during the transaction causes it to be _aborted_, none will be readable for consumers. + +There are a couple of fundamental things to consider before using transactions: + +* Each transactional producer is configured with a unique identifier called the `transactional.id`. +This is used to identify the same producer instance across application restarts. +By default, it is not configured, and transactions cannot be used. +When it is configured, the producer is limited to idempotent delivery, therefore `enable.idempotence=true` is implied. +* For *only* reading transactional messages, a consumer needs to explicitly configure its `isolation.level` property to `read++_++committed`. +This will make sure that the consumer will deliver only records committed inside a transaction, and filter out messages from aborted transactions. +* It should also be noted that this does not mean all records atomically written inside a transaction will be read atomically by the consumer. +Transactional producers can write to multiple topics and partitions inside a transaction, but consumers do not know where the transaction starts or ends. +Not only multiple consumers inside a consumer group can share those partitions, +all records which were part of a single transaction can be consumed in different batch of records by a consumer. + +Kafka connector provides `KafkaTransactions` custom emitter for writing records inside a transaction. +Before using a transaction we need to make sure that `transactional.id` is configured on the channel properties. + +.... +mp.messaging.outgoing.tx-out-example.transactional.id=example-tx-producer +.... + +It can be used as a regular emitter `@Channel`: + +[source,java] +---- +include::{examples-dir}/kafka/outbound/KafkaTransactionalProducer.java[indent=0] +---- + +[NOTE] +==== + +When `transactional.id` is provided `KafkaProducer#initTransactions` is called when the underlying Kafka producer is created. + +==== + +The function given to the `withTransaction` method receives a `TransactionalEmitter` for producing records, and returns a `Uni` that provides the result of the transaction. +If the processing completes successfully, the producer is flushed and the transaction is committed. +If the processing throws an exception, returns a failing `Uni`, or marks the `TransactionalEmitter` for abort, the transaction is aborted. + +If this method is called on a Vert.x context, the processing function is also called on that context. +Otherwise, it is called on the sending thread of the producer. + +A synchronous variant `withTransactionAndAwait` is also available for blocking contexts such as REST endpoints or `@Blocking` annotated methods. +It blocks the calling thread until the transaction completes and must not be called on a Vert.x event loop thread. +The transaction completion phase (flush, commit/abort) is bounded by the Kafka producer's `transaction.timeout.ms` setting (default 60 seconds). + +[IMPORTANT] +==== + +By default (non-pooled mode), a transaction is considered _in progress_ from the call to the `withTransaction` until the returned `Uni` results in success or failure. +While a transaction is in progress, subsequent calls to the `withTransaction`, including nested ones inside the given function, will throw `IllegalStateException`. +Note that in Reactive Messaging, the execution of processing methods is already serialized, unless `@Blocking(ordered = false)` is used. +If `withTransaction` can be called concurrently, for example from a REST endpoint, it is recommended to limit the concurrency of the execution. +This can be done using the `@Bulkhead` annotation from Microprofile Fault Tolerance, or by using the xref:#pooled-producer-for-concurrent-transactions[pooled producer mode] which allows concurrent transactions. +In pooled producer mode, multiple transactions can run concurrently, each using a different producer from the pool. + +==== + +== Exactly-Once Processing + +Kafka Transactions API also allows managing consumer offsets inside a transaction, together with produced messages. +This in turn enables coupling a consumer with a transactional producer in a consume-transform-produce pattern, +also known as exactly-once processing. +It means that an application consumes messages from a topic-partition, processes them, publishes the results to a topic-partition, +and commits offsets of the consumed messages in a transaction. + +The `KafkaTransactions` emitter also provides a way to apply exactly-once processing to an incoming Kafka message inside a transaction. + +The following example includes a batch of Kafka records inside a transaction. + +.... +mp.messaging.outgoing.tx-out-example.transactional.id=example-tx-producer +mp.messaging.incoming.in-channel.batch=true +mp.messaging.incoming.in-channel.commit-strategy=ignore +mp.messaging.incoming.in-channel.failure-strategy=ignore +.... + +[source,java] +---- +include::{examples-dir}/kafka/outbound/KafkaExactlyOnceProcessor.java[indent=0] +---- + +If the processing completes successfully, before committing the transaction, the topic partition offsets of the given batch message will be committed to the transaction. + +If the processing needs to abort, after aborting the transaction, the consumer's position is reset to the last committed offset, effectively resuming the consumption from that offset. +If no consumer offset has been committed, the consumer's position is reset to the beginning of the topic, even if the offset reset policy is `latest`. + +[IMPORTANT] +==== + +When using exactly-once processing, consumed message offset commits are handled by the transaction and therefore `commit-strategy` needs to be `ignore`. + +==== + +Any strategy can be employed for the `failure-strategy`. +Note that the `Uni` returned from the `#withTransaction` will yield a failure if the transaction fails and is aborted. + +The application can choose to handle the error case, but for the message consumption to continue, `Uni` returned from the `@Incoming` method must not result in failure. +`KafkaTransactions#withTransactionAndAck` method will ack and nack the message but will not stop the reactive stream. +Ignoring the failure simply resets the consumer to the last committed offsets and resumes the processing from there. + +Synchronous variants `withTransactionAndAwait` are also available for exactly-once processing, +accepting `IncomingKafkaRecordMetadata` or `IncomingKafkaRecordBatchMetadata` for offset management within the transaction. + +[NOTE] +==== + +It is recommended to use exactly-once processing along with the batch consumption mode. +While it is possible to use it with a single Kafka message, it'll have a significant performance impact. + +==== + +== Pooled Producer for Concurrent Transactions + +By default, `KafkaTransactions` uses a single transactional producer and a global lock, meaning only one transaction can be in progress at a time. +This limits throughput when consuming from multiple partitions concurrently. + +The *pooled producer* mode uses a pool of Kafka producers, each with its own `transactional.id`. +Each transaction reserves a producer from the pool for the duration of the transaction, enabling concurrent exactly-once processing. + +To enable pooled producer mode, set `pooled-producer.enabled` to `true` on the outgoing channel. +Combined with incoming channel `batch` and `concurrency`, multiple partitions can be processed in parallel, +each with its own independent transaction: + +[source,properties] +---- +mp.messaging.outgoing.tx-out-example.transactional.id=my-tx-producer +mp.messaging.outgoing.tx-out-example.pooled-producer.enabled=true + +mp.messaging.incoming.in-channel.ordered=partition +mp.messaging.incoming.in-channel.commit-strategy=ignore +mp.messaging.incoming.in-channel.failure-strategy=ignore +---- + +The `KafkaTransactions` API is the same as for regular exactly-once processing. +The outgoing record's partition defaults to the incoming record's partition: + +[source,java] +---- +include::{examples-dir}/kafka/outbound/KafkaPooledExactlyOnceProcessor.java[indent=0] +---- + +On commit or abort, the producer is returned to the pool and can be reused for another transaction. +Only the partitions involved in the transaction are reset on abort, so other concurrent transactions are unaffected. + +The pool grows lazily to match actual concurrency, up to `pooled-producer.max-pool-size` (default 10). +Each inner producer gets a unique `transactional.id` derived from the base value, e.g. `my-tx-producer-1`, `my-tx-producer-2`, etc. +Producers are reused across transactions. + +You can also pre-create producers at startup with `pooled-producer.initial-pool-size` (default 0). +Pre-created producers respect the `lazy-client` setting, so with `lazy-client=false` (default), they eagerly initialize the Kafka producer and call `initTransactions()` at startup. + +[source,properties] +---- +mp.messaging.outgoing.tx-out-example.pooled-producer.max-pool-size=20 +mp.messaging.outgoing.tx-out-example.pooled-producer.initial-pool-size=5 +---- diff --git a/content/docs/smallrye-reactive-messaging/4/kafka/writing-kafka-records.adoc b/content/docs/smallrye-reactive-messaging/4/kafka/writing-kafka-records.adoc new file mode 100644 index 000000000..6d27eee5b --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/kafka/writing-kafka-records.adoc @@ -0,0 +1,354 @@ += Writing Kafka Records +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The Kafka Connector can write Reactive Messaging `Messages` as Kafka +Records. + +== Example + +Let’s imagine you have a Kafka broker running, and accessible using the +`kafka:9092` address (by default it would use `localhost:9092`). +Configure your application to write the messages from the `prices` +channel into a Kafka _topic_ as follows: + +[source,properties] +---- +kafka.bootstrap.servers=kafka:9092 # <1> + +mp.messaging.outgoing.prices-out.connector=smallrye-kafka # <2> +mp.messaging.outgoing.prices-out.value.serializer=org.apache.kafka.common.serialization.DoubleSerializer # <3> +mp.messaging.outgoing.prices-out.topic=prices # <4> +---- + +[arabic] +. Configure the broker location. You can configure it globally or per +channel +. Configure the connector to manage the `prices` channel +. Sets the (Kafka) serializer to encode the message payload into the +record’s value +. Make sure the topic name is `prices` (and not the default +`prices-out`) + +Then, your application must send `Message++<++Double++>++` to the `prices` +channel. It can use `double` payloads as in the following snippet: + +[source,java] +---- +include::{examples-dir}/kafka/outbound/KafkaPriceProducer.java[indent=0] +---- + +Or, you can send `Message++<++Double++>++`: + +[source,java] +---- +include::{examples-dir}/kafka/outbound/KafkaPriceMessageProducer.java[indent=0] +---- + +== Serialization + +The serialization is handled by the underlying Kafka Client. You need to +configure the: + +* `mp.messaging.outgoing.++[++channel-name++]++.value.serializer` to configure +the value serializer (mandatory) +* `mp.messaging.outgoing.++[++channel-name++]++.key.serializer` to configure +the key serializer (optional, default to `String`) + +If you want to use a custom serializer, add it to your `CLASSPATH` and +configure the associate attribute. + +By default, the written record contains: + +* the `Message` payload as _value_ +* no key, or the key configured using the `key` attribute or the key +passed in the metadata attached to the `Message` +* the timestamp computed for the system clock (`now`) or the timestamp +passed in the metadata attached to the `Message` + +== Sending key/value pairs + +In the Kafka world, it’s often necessary to send _records_, i.e. a +key/value pair. The connector provides the link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-kafka/{javadoc-version}/io/smallrye/reactive/messaging/kafka/Record.html[`io.smallrye.reactive.messaging.kafka.Record`] +class that you can use to send a pair: + +[source,java] +---- +include::{examples-dir}/kafka/outbound/KafkaRecordExample.java[tags=code,indent=0] +---- + +When the connector receives a message with a `Record` payload, it +extracts the key and value from it. The configured serializers for the +key and the value must be compatible with the record’s key and value. +Note that the `key` and the `value` can be `null`. It is also possible +to create a record with a `null` key AND a `null` value. + +If you need more control on the written records, use +`OutgoingKafkaRecordMetadata`. + +== Outbound Metadata + +When sending `Messages`, you can add an instance of +link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-kafka/{javadoc-version}/io/smallrye/reactive/messaging/kafka/api/OutgoingKafkaRecordMetadata.html[`OutgoingKafkaRecordMetadata`] +to influence how the message is going to be written to Kafka. For example, +you can add Kafka headers, configure the record key… + +[source,java] +---- +include::{examples-dir}/kafka/outbound/KafkaOutboundMetadataExample.java[tags=code,indent=0] +---- + +== Propagating Record Key + +When processing messages, you can propagate incoming record key to the +outgoing record. + +Consider the following example method, which consumes messages from the +channel `in`, transforms the payload, and writes the result to the +channel `out`. + +[source,java] +---- +include::{examples-dir}/kafka/outbound/KafkaPriceProcessor.java[tags=code,indent=0] +---- + +Enabled with +`mp.messaging.outgoing.++[++channel-name++]++.propagate-record-key=true` +configuration, record key propagation produces the outgoing record with +the same _key_ as the incoming record. + +If the outgoing record already contains a _key_, it *won’t be +overridden* by the incoming record key. If the incoming record does +have a _null_ key, the `mp.messaging.outgoing.++[++channel-name++]++.key` +property is used. + +== Propagating Record headers + +You can also propagate incoming record headers to the outgoing record, by specifying the list of headers to be considered. + +`mp.messaging.outgoing.++[++channel-name++]++.propagate-headers=Authorization,Proxy-Authorization` + +If the ougoing record already defines a header with the same key, it won't be overriden by the incoming header. + +== Dynamic topic names + +Sometimes it is desirable to select the destination of a message +dynamically. In this case, you should not configure the topic inside +your application configuration file, but instead, use the outbound +metadata to set the name of the topic. + +For example, you can route to a dynamic topic based on the incoming +message: + +[source,java] +---- +include::{examples-dir}/kafka/outbound/KafkaOutboundDynamicTopicExample.java[tags=code,indent=0] +---- + +== Acknowledgement + +Kafka acknowledgement can take times depending on the configuration. +Also, it stores in-memory the records that cannot be written. + +By default, the connector does wait for Kafka to acknowledge the record +to continue the processing (acknowledging the received `Message`). You +can disable this by setting the `waitForWriteCompletion` attribute to +`false`. + +Note that the `acks` attribute has a huge impact on the record +acknowledgement. + +If a record cannot be written, the message is `nacked`. + +== Back-pressure and inflight records + +The Kafka outbound connector handles back-pressure monitoring the number +of in-flight messages waiting to be written to the Kafka broker. The +number of in-flight messages is configured using the +`max-inflight-messages` attribute and defaults to 1024. + +The connector only sends that amount of messages concurrently. No other +messages will be sent until at least one in-flight message gets +acknowledged by the broker. Then, the connector writes a new message to +Kafka when one of the broker’s in-flight messages get acknowledged. Be +sure to configure Kafka’s `batch.size` and `linger.ms` accordingly. + +You can also remove the limit of inflight messages by setting +`max-inflight-messages` to `0`. However, note that the Kafka Producer +may block if the number of requests reaches +`max.in.flight.requests.per.connection`. + +== Handling serialization failures + +For Kafka producer client serialization failures are not recoverable, +thus the message dispatch is not retried. However, using schema +registries, serialization may intermittently fail to contact the +registry. In these cases you may need to apply a failure strategy for +the serializer. To achieve this, create a CDI bean implementing the link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-kafka/{javadoc-version}/io/smallrye/reactive/messaging/kafka/SerializationFailureHandler.html[`SerializationFailureHandler`] +interface: + +[source,java] +---- +@ApplicationScoped +@Identifier("failure-fallback") // Set the name of the failure handler +public class MySerializationFailureHandler + implements SerializationFailureHandler { // Specify the expected type + + @Override + public byte[] decorateSerialization(Uni serialization, String topic, + boolean isKey, String serializer, + Object data, Headers headers) { + return serialization + .onFailure().retry().atMost(3) + .await().atMost(Duration.ofMillis(200)); + } +} +---- + +The bean must be exposed with the `@Identifier` qualifier specifying the +name of the bean. Then, in the connector configuration, specify the +following attribute: + +* `mp.messaging.incoming.$channel.key-serialization-failure-handler`: +name of the bean handling serialization failures happening for the +record’s key +* `mp.messaging.incoming.$channel.value-serialization-failure-handler`: +name of the bean handling serialization failures happening for the +record’s value, + +The handler is called with the serialization action as a `Uni`, the +record’s topic, a boolean indicating whether the failure happened on a +key, the class name of the deserializer that throws the exception, the +corrupted data, the exception, and the records headers. Failure +strategies like retry, providing a fallback value or applying timeout +can be implemented. Note that the method must await on the result and +return the serialized byte array. Alternatively, the handler can +implement `decorateSerialization` method which can return a fallback +value. + +== Sending Cloud Events + +The Kafka connector supports https://cloudevents.io/[Cloud Events]. +The connector sends the outbound record as Cloud Events if: + +* the message metadata contains an +`io.smallrye.reactive.messaging.ce.OutgoingCloudEventMetadata` +instance, +* the channel configuration defines the `cloud-events-type` and +`cloud-events-source` attributes. + +You can create +`io.smallrye.reactive.messaging.ce.OutgoingCloudEventMetadata` instances +using: + +[source,java] +---- +include::{examples-dir}/kafka/outbound/KafkaCloudEventProcessor.java[indent=0] +---- + +If the metadata does not contain an id, the connector generates one +(random UUID). The `type` and `source` can be configured per message or +at the channel level using the `cloud-events-type` and +`cloud-events-source` attributes. Other attributes are also +configurable. + +The metadata can be contributed by multiple methods, however, you must +always retrieve the already existing metadata to avoid overriding the +values: + +[source,java] +---- +include::{examples-dir}/kafka/outbound/KafkaCloudEventMultipleProcessors.java[indent=0] +---- + +By default, the connector sends the Cloud Events using the _binary_ +format. You can write _structured_ Cloud Events by setting the +`cloud-events-mode` to `structured`. Only JSON is supported, so the +created records had its `content-type` header set to +`application/cloudevents{plus}json; charset=UTF-8` When using the +_structured_ mode, the value serializer must be set to +`org.apache.kafka.common.serialization.StringSerializer`, otherwise the +connector reports the error. In addition, in _structured_, the connector +maps the message’s payload to JSON, except for `String` passed directly. + +The record’s key can be set in the channel configuration (`key` +attribute), in the `OutgoingKafkaRecordMetadata` or using the +`partitionkey` Cloud Event attribute. + +[NOTE] +==== + +you can disable the Cloud Event support by setting the `cloud-events` +attribute to `false` + +==== + +== Using `ProducerRecord` + +Kafka built-in type +https://kafka.apache.org/26/javadoc/index.html?org/apache/kafka/clients/producer/ProducerRecord.html[ProducerRecord++<++K,V++>++] +can also be used for producing messages: + +[source,java] +---- +include::{examples-dir}/kafka/outbound/KafkaProducerRecordExample.java[tags=code,indent=0] +---- + +[WARNING] +==== + +This is an advanced feature. The `ProducerRecord` is sent to Kafka as +is. Any possible metadata attached through `Message++<++ProducerRecord++<++K, V++>>++` are ignored and lost. + +==== + +== Producer Interceptors + +Producer interceptors can be configured for Kafka producer clients using the standard `interceptor.classes` property. +Configured classes will be instantiated by the Kafka producer on client creation. + +Alternatively, producer clients can be configured with a CDI managed-bean implementing link:https://javadoc.io/doc/org.apache.kafka/kafka-clients/{javadoc-version}/org/apache/kafka/clients/producer/ProducerInterceptor.html[`org.apache.kafka.clients.producer.ProducerInterceptor`] interface: + +To achieve this, the bean must be exposed with the `@Identifier` qualifier specifying the name of the bean: + +[source,java] +---- +include::{examples-dir}/kafka/outbound/ProducerInterceptorBeanExample.java[indent=0] +---- + +Then, in the channel configuration, specify the following attribute: +`mp.messaging.incoming.$channel.interceptor-bean=my-producer-interceptor`. + +[WARNING] +==== + +The `onSend` method will be called on the producer _sending thread_ and `onAcknowledgement` will be called on the _Kafka producer I/O thread_. +In both cases if implementations are not fast, sending of messages could be delayed. + +==== + +== Configuration Reference + +include::{generated-dir}/smallrye-kafka-outgoing.adoc[indent=0] + +You can also pass any property supported by the underlying https://kafka.apache.org/documentation/#producerconfigs[Kafka +producer]. + +For example, to configure the `batch.size` property, use: + +[source,properties] +---- +mp.messaging.outgoing.[channel].batch.size=32768 +---- + +Some producer client properties are configured to sensible default +values: + +If not set, `reconnect.backoff.max.ms` is set to `10000` to avoid high +load on disconnection. + +If not set, `key.serializer` is set to +`org.apache.kafka.common.serialization.StringSerializer`. + +If not set, producer `client.id` is generated as +`kafka-producer-++[++channel++]++`. diff --git a/content/docs/smallrye-reactive-messaging/4/mqtt/client-customization.adoc b/content/docs/smallrye-reactive-messaging/4/mqtt/client-customization.adoc new file mode 100644 index 000000000..768513593 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/mqtt/client-customization.adoc @@ -0,0 +1,21 @@ += Customizing the underlying MQTT client +:version-root: ../ +include::{version-root}_attributes.adoc[] + +You can customize the underlying MQTT Client configuration by +_producing_ an instance of +`io.smallrye.reactive.messaging.mqtt.session.MqttClientSessionOptions`: + +[source,java] +---- +include::{examples-dir}/mqtt/customization/ClientProducers.java[tags=named,indent=0] +---- + +This instance is retrieved and used to configure the client used by the +connector. You need to indicate the name of the client using the +`client-options-name` attribute: + +[source,properties] +---- +mp.messaging.incoming.prices.client-options-name=my-options +---- diff --git a/content/docs/smallrye-reactive-messaging/4/mqtt/mqtt.adoc b/content/docs/smallrye-reactive-messaging/4/mqtt/mqtt.adoc new file mode 100644 index 000000000..afa02d06f --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/mqtt/mqtt.adoc @@ -0,0 +1,44 @@ += MQTT Connector +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The MQTT connector adds support for MQTT to Reactive Messaging. + +It lets you receive messages from an MQTT server or broker as well as +send MQTT messages. The MQTT connector is based on the https://vertx.io/docs/vertx-mqtt/java/#_vert_x_mqtt_client[Vert.x MQTT +Client]. + +== Introduction + +http://mqtt.org/[MQTT] is a machine-to-machine (M2M)/"Internet of +Things" connectivity protocol. It was designed as an extremely +lightweight publish/subscribe messaging transport. + +The MQTT Connector allows consuming messages from MQTT as well as +sending MQTT messages. + +== Using the MQTT connector + +To you the MQTT Connector, add the following dependency to your project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.reactive + smallrye-reactive-messaging-mqtt + {project-version} + +---- + +The connector name is: `smallrye-mqtt`. + +So, to indicate that a channel is managed by this connector you need: + +[source,properties] +---- +# Inbound +mp.messaging.incoming.[channel-name].connector=smallrye-mqtt + +# Outbound +mp.messaging.outgoing.[channel-name].connector=smallrye-mqtt +---- diff --git a/content/docs/smallrye-reactive-messaging/4/mqtt/receiving-mqtt-messages.adoc b/content/docs/smallrye-reactive-messaging/4/mqtt/receiving-mqtt-messages.adoc new file mode 100644 index 000000000..5a1e176bb --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/mqtt/receiving-mqtt-messages.adoc @@ -0,0 +1,114 @@ += Receiving messages from MQTT +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The MQTT Connector connects to a MQTT broker or router, and forward the +messages to the Reactive Messaging application. It maps each of them +into Reactive Messaging `Messages`. + +== Example + +Let’s imagine you have a MQTT server/broker running, and accessible +using the `mqtt:1883` address (by default it would use +`localhost:1883`). Configure your application to receive MQTT messages +on the `prices` channel as follows: + +[source,properties] +---- +mp.messaging.incoming.prices.connector=smallrye-mqtt # <1> +mp.messaging.incoming.prices.host=mqtt # <2> +mp.messaging.incoming.prices.port=1883 # <3> +---- + +[arabic] +. Sets the connector for the `prices` channel +. Configure the broker/server host name. +. Configure the broker/server port. 1883 is the default. + +[NOTE] +==== + +You don’t need to set the MQTT topic. By default, it uses the channel +name (`prices`). You can configure the `topic` attribute to override it. + +==== + +[NOTE] +==== + +It is generally recommended to set the `client-id`. By default, the connector is generating a unique `client-id`. + +==== + +[IMPORTANT] +==== + +Message coming from MQTT have a `byte++[]++` payload. + +==== + +Then, your application receives `Message++<++byte++[]>++`. You can consume the +payload directly: + +[source,java] +---- +include::{examples-dir}/mqtt/inbound/MqttPriceConsumer.java[indent=0] +---- + +Or, you can retrieve the `Message++<++byte++[]>++`: + +[source,java] +---- +include::{examples-dir}/mqtt/inbound/MqttPriceMessageConsumer.java[indent=0] +---- + +The inbound topic can use the https://mosquitto.org/man/mqtt-7.html[MQTT +wildcards] (`{plus}` and `#`). + +== Deserialization + +The MQTT Connector does not handle the deserialization and creates a +`Message++<++byte++[]>++`. + +== Inbound Metadata + +The MQTT connector does not provide inbound metadata. + +== Failure Management + +If a message produced from a MQTT message is _nacked_, a failure +strategy is applied. The MQTT connector supports 3 strategies: + +* `fail` - fail the application, no more MQTT messages will be +processed. (default) The offset of the record that has not been +processed correctly is not committed. +* `ignore` - the failure is logged, but the processing continue. + +== Configuration Reference + +include::{generated-dir}/smallrye-mqtt-incoming.adoc[indent=0] + +The MQTT connector is based on the https://vertx.io/docs/vertx-mqtt/java/#_vert_x_mqtt_client[Vert.x MQTT +client]. So +you can pass any attribute supported by this client. + +[IMPORTANT] +==== + +A single instance of `MqttClient` and a single connection is used for +each `host` / `port` / `server-name` / `client-id`. This client is +reused for both the inbound and outbound connectors. + +==== + +[IMPORTANT] +==== + +Using `auto-clean-session=false` the MQTT Connector send Subscribe requests +to the broken only if a Persistent Session is not present (like on the first +connection). This means that if a Session is already present (maybe for a +previous run) and you add a new incoming channel, this will not be subscribed. +Beware to check always the subscription present on Broker when use +`auto-clean-session=false`. + +==== diff --git a/content/docs/smallrye-reactive-messaging/4/mqtt/sending-messages-to-mqtt.adoc b/content/docs/smallrye-reactive-messaging/4/mqtt/sending-messages-to-mqtt.adoc new file mode 100644 index 000000000..bc8017414 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/mqtt/sending-messages-to-mqtt.adoc @@ -0,0 +1,94 @@ += Sending messages to MQTT +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The MQTT Connector can write Reactive Messaging `Messages` as MQTT +Message. + +== Example + +Let’s imagine you have a MQTT server/broker running, and accessible +using the `mqtt:1883` address (by default it would use +`localhost:1883`). Configure your application to write the messages from +the `prices` channel into a MQTT Messages as follows: + +[source,properties] +---- +mp.messaging.outgoing.prices.type=smallrye-mqtt +mp.messaging.outgoing.prices.host=mqtt +mp.messaging.outgoing.prices.port=1883 +---- + +[arabic] +. Sets the connector for the `prices` channel +. Configure the broker/server host name. +. Configure the broker/server port. 1883 is the default. + +[NOTE] +==== + +You don’t need to set the MQTT topic. By default, it uses the channel +name (`prices`). You can configure the `topic` attribute to override it. +NOTE: It is generally recommended to set the `client-id`. By default, +the connector is generating a unique `client-id`. + +==== + +Then, your application must send `Message++<++Double++>++` to the `prices` +channel. It can use `double` payloads as in the following snippet: + +[source,java] +---- +include::{examples-dir}/mqtt/outbound/MqttPriceProducer.java[indent=0] +---- + +Or, you can send `Message++<++Double++>++`: + +[source,java] +---- +include::{examples-dir}/mqtt/outbound/MqttPriceMessageProducer.java[indent=0] +---- + +== Serialization + +The `Message` sent to MQTT can have various payload types: + +* https://vertx.io/docs/apidocs/io/vertx/core/json/JsonObject.html[`JsonObject`]: +JSON string encoded as `byte++[]++` +* https://vertx.io/docs/apidocs/io/vertx/core/json/JsonArray.html[`JsonArray`]: +JSON string encoded as `byte++[]++` +* `java.lang.String` and Java primitive types: `toString` encoded as +`byte++[]++` +* `byte++[]++` +* complex objects: The objects are encoded to JSON and passed as +`byte++[]++` + +== Outbound Metadata + +The MQTT connector does not provide outbound metadata. + +== Acknowledgement + +MQTT acknowledgement depends on the QoS level. The message is +acknowledged when the broker indicated the successful reception of the +message (or immediately if the level of QoS does not support +acknowledgment). + +If a MQTT message cannot be sent to the broker, the message is `nacked`. + +== Configuration Reference + +include::{generated-dir}/smallrye-mqtt-outgoing.adoc[indent=0] + +The MQTT connector is based on the https://vertx.io/docs/vertx-mqtt/java/#_vert_x_mqtt_client[Vert.x MQTT +client]. So +you can pass any attribute supported by this client. + +[IMPORTANT] +==== + +A single instance of `MqttClient` and a single connection is used for +each `host` / `port` / `server-name` / `client-id`. This client is +reused for both the inbound and outbound connectors. + +==== diff --git a/content/docs/smallrye-reactive-messaging/4/pulsar/client-configuration.adoc b/content/docs/smallrye-reactive-messaging/4/pulsar/client-configuration.adoc new file mode 100644 index 000000000..c0e021963 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/pulsar/client-configuration.adoc @@ -0,0 +1,98 @@ += Configuring Pulsar clients, consumers and producers +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Pulsar clients, consumers and producers are very customizable to configure how a Pulsar client application behaves. + +The Pulsar connector creates a Pulsar client and, a consumer or a producer per channel, each with sensible defaults to ease their configuration. +Although the creation is handled, all available configuration options remain configurable through Pulsar channels. + +While idiomatic way of creating `PulsarClient`, `PulsarConsumer` or `PulsarProducer` are through builder APIs, in its essence +those APIs build each time a configuration object, to pass onto the implementation. +Those are link:https://javadoc.io/doc/org.apache.pulsar/pulsar-client-original/{javadoc-version}/org/apache/pulsar/client/impl/conf/ClientConfigurationData.html[`ClientConfigurationData`], +link:https://javadoc.io/doc/org.apache.pulsar/pulsar-client-original/{javadoc-version}/org/apache/pulsar/client/impl/conf/ConsumerConfigurationData.html[`ConsumerConfigurationData`], +link:https://javadoc.io/doc/org.apache.pulsar/pulsar-client-original/{javadoc-version}/org/apache/pulsar/client/impl/conf/ProducerConfigurationData.html[`ProducerConfigurationData`]. + +Pulsar Connector allows receiving properties for those configuration objects directly. +For example, the broker authentication information for `PulsarClient` is received using `authPluginClassName` and `authParams` properties. +In order to configure the authentication for the incoming channel `data` : + +[source,properties] +---- +mp.messaging.incoming.data.connector=smallrye-pulsar +mp.messaging.incoming.data.serviceUrl=pulsar://localhost:6650 +mp.messaging.incoming.data.topic=topic +mp.messaging.incoming.data.subscriptionInitialPosition=Earliest +mp.messaging.incoming.data.schema=INT32 +mp.messaging.incoming.data.authPluginClassName=org.apache.pulsar.client.impl.auth.AuthenticationBasic +mp.messaging.incoming.data.authParams={"userId":"superuser","password":"admin"} +---- + +Note that the Pulsar consumer property `subscriptionInitialPosition` is also configured with the `Earliest` value which represents with enum value `SubscriptionInitialPosition.Earliest`. + +This approach covers most of the configuration cases. +However, non-serializable objects such as `CryptoKeyReader`, `ServiceUrlProvider` etc. cannot be configured this way. +The Pulsar Connector allows taking into account instances of Pulsar configuration data objects – +`ClientConfigurationData`, `ConsumerConfigurationData`, `ProducerConfigurationData`: + +[source,java] +---- +include::{examples-dir}/pulsar/configuration/PulsarClientConfigProducers.java[tags=consumer,indent=0] +---- + +This instance is retrieved and used to configure the client used by the connector. +You need to indicate the name of the client using the `client-configuration`, `consumer-configuration` or `producer-configuration` attributes: + +[source,properties] +---- +mp.messaging.incoming.prices.consumer-configuration=my-consumer-options +---- + +If no `++[++client++|++consumer++|++producer++]++-configuration` is configured, the connector will look for instances identified with the channel name: + +[source,java] +---- +include::{examples-dir}/pulsar/configuration/PulsarClientConfigProducers.java[tags=client,indent=0] +---- + +You also can provide a `Map++<++String, Object++>++` containing configuration values by key: + +[source,java] +---- +include::{examples-dir}/pulsar/configuration/PulsarClientConfigProducers.java[tags=producer,indent=0] +---- + +Different configuration sources are loaded in the following order of precedence, from the least important to the highest: + +[arabic] +. `Map++<++String, Object++>++` config map produced with default config identifier, `default-pulsar-client`, `default-pulsar-consumer`, `default-pulsar-producer`. +. `Map++<++String, Object++>++` config map produced with identifier in the configuration or channel name +. `++[++Client++|++Producer++|++Consuemr++]++ConfigurationData` object produced with identifier in the channel configuration or the channel name +. Channel configuration properties named with `++[++Client++|++Producer++|++Consuemr++]++ConfigurationData` field names. + +Following is the configuration reference for the `PulsarClient`. + +Corresponding sections list the xref:pulsar/receiving-pulsar-messages.adoc#configuration-reference[Consumer Configuration Reference] and +xref:pulsar/sending-messages-to-pulsar.adoc#configuration-reference[Producer Configuration Reference]. +Configuration properties not configurable in configuration files (non-serializable) is noted in the column `Config file`. + +== Shared Resources + +The Pulsar connector shares an event loop group and DNS resolver across all Pulsar clients using +link:https://javadoc.io/doc/org.apache.pulsar/pulsar-client-api/{javadoc-version}/org/apache/pulsar/client/api/PulsarClientSharedResources.html[`PulsarClientSharedResources`]. +This reduces thread usage and avoids creating separate thread pools per client. + +By default, the connector creates and manages a `PulsarClientSharedResources` instance with shared `EventLoopGroup` and `DnsResolver`. +To customize the shared resources, produce a CDI bean of type `PulsarClientSharedResources`: + +[source,java] +---- +include::{examples-dir}/pulsar/configuration/PulsarClientConfigProducers.java[tags=shared-resources,indent=0] +---- + +When a `PulsarClientSharedResources` bean is provided, the connector uses it for all Pulsar clients and does not manage its lifecycle. +The application is responsible for closing the shared resources when they are no longer needed. + +== `PulsarClient` Configuration Reference + +include::{partialsdir}/pulsar/config/smallrye-pulsar-client.adoc[] diff --git a/content/docs/smallrye-reactive-messaging/4/pulsar/client-service.adoc b/content/docs/smallrye-reactive-messaging/4/pulsar/client-service.adoc new file mode 100644 index 000000000..a50e7db91 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/pulsar/client-service.adoc @@ -0,0 +1,24 @@ += PulsarClientService +:version-root: ../ +include::{version-root}_attributes.adoc[] + +For advanced use cases, SmallRye Reactive Messaging provides a bean of +type `PulsarClientService` that you can inject: + +[source,java] +---- +@Inject +PulsarClientService pulsarClients; +---- + +From there, you can obtain +`org.apache.pulsar.client.api.Consumer`, +`org.apache.pulsar.client.api.Producer` and `org.apache.pulsar.client.api.PulsarClient` by channel name. + +[IMPORTANT] +==== + +Note that the https://pulsar.apache.org/docs/3.0.x/admin-api-get-started/[`PulsarAdmin`] is a separate library and the Pulsar connector doesn't provide it out-of-the-box. +You'd need to explicitly add the dependency, i.e. `org.apache.pulsar:pulsar-client-admin-original` artifact. + +==== diff --git a/content/docs/smallrye-reactive-messaging/4/pulsar/health.adoc b/content/docs/smallrye-reactive-messaging/4/pulsar/health.adoc new file mode 100644 index 000000000..090f18c9c --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/pulsar/health.adoc @@ -0,0 +1,24 @@ += Health reporting +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The Pulsar connector reports the startup, readiness and liveness of each channel managed by the connector: + +* *Startup & Readiness* For both inbound and outbound, probes report _OK_ when the +connection with the broker is established. +* *Liveness* For both inbound and outbound, the liveness check verifies that the +connection is established *AND* that no failures have been caught. + +[NOTE] +==== + +To disable health reporting, set the `health-enabled` attribute for the +channel to `false`. + +==== + +Note that a message processing failures _nacks_ the message which is +then handled by the failure-strategy. It is the responsibility of the +failure-strategy to report the failure and influence the outcome of the +liveness checks. The `fail` failure strategy reports the failure and so +the liveness check will report the failure. diff --git a/content/docs/smallrye-reactive-messaging/4/pulsar/pulsar.adoc b/content/docs/smallrye-reactive-messaging/4/pulsar/pulsar.adoc new file mode 100644 index 000000000..29e31c8b8 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/pulsar/pulsar.adoc @@ -0,0 +1,64 @@ += Apache Pulsar Connector +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The Pulsar connector adds support for Apache Pulsar to Reactive Messaging. With +it you can consume Pulsar Messages as well as produce _message_ into Pulsar. + +https://pulsar.apache.org/[Apache Pulsar] an open-source, distributed messaging +and streaming platform built for the cloud. For more details about Pulsar, check the +https://pulsar.apache.org/docs/next/concepts-overview/[documentation]. + +Pulsar implements the publish-subscribe pattern. +Producers publish _messages_ to __topic__s. +Consumers create __subscription__s to those topics to receive and process incoming messages, +and send _acknowledgments_ to the broker when processing is finished. + +When a _subscription_ is created, Pulsar retains all messages, even if the consumer is disconnected. +The retained messages are discarded only when a consumer acknowledges that all these messages are processed successfully. + +A Pulsar _message_ consists of + +* *value*, payload data that message contains, while Pulsar messages contain payloads as unstructured byte array, +a *schema* is applied to write and read with an enforced data structure +* *key* of type string, used for partitioning +* *properties*, optional key/value map +* *topic*, the topic that the message is published to +* *producer name*, the name of the producer who produces the message +* *message ID*, the ID assigned by bookies to a message as soon as the message is persistently stored +* *sequence ID*, the ID assigned by producers, indicating its order in that sequence +* *publish time*, timestamp automatically added by the producer +* *event time*, optional timestamp added by the application + +A Pulsar cluster consists of +- One or more **broker**s, which are stateless components +- A *metadata store* for maintaining topic metadata, schema, coordination and cluster configuration. +By default, a Zookeeper cluster is used, except the standalone mode +- A ensemble of *bookies* used for persistent storage of messages + +== Using the Pulsar Connector + +To use the Pulsar Connector, add the following dependency to your +project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.reactive + smallrye-reactive-messaging-pulsar + {project-version} + +---- + +The connector name is: `smallrye-pulsar`. + +So, to indicate that a channel is managed by this connector you need: + +[source,properties] +---- +# Inbound +mp.messaging.incoming.[channel-name].connector=smallrye-pulsar + +# Outbound +mp.messaging.outgoing.[channel-name].connector=smallrye-pulsar +---- diff --git a/content/docs/smallrye-reactive-messaging/4/pulsar/receiving-pulsar-messages.adoc b/content/docs/smallrye-reactive-messaging/4/pulsar/receiving-pulsar-messages.adoc new file mode 100644 index 000000000..bafa43e0b --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/pulsar/receiving-pulsar-messages.adoc @@ -0,0 +1,196 @@ += Receiving messages from Pulsar +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The Pulsar Connector connects to a Pulsar broker using a Pulsar client and creates consumers to +receive messages from Pulsar brokers and it maps each of them into Reactive Messaging `Messages`. + +== Example + +Let’s imagine you have a Pulsar broker running, and accessible +using the `pulsar:6650` address (by default it would use +`localhost:6650`). Configure your application to receive Pulsar messages +on the `prices` channel as follows: + +[source,properties] +---- +mp.messaging.incoming.prices.connector=smallrye-pulsar # <1> +mp.messaging.incoming.prices.serviceUrl=pulsar://pulsar:6650 # <2> +mp.messaging.incoming.prices.schema=DOUBLE # <3> +mp.messaging.incoming.prices.subscriptionInitialPosition=Earliest # <4> +---- + +[arabic] +. Sets the connector for the `prices` channel +. Configure the Pulsar broker service url. +. Configure the schema to consume prices as Double. +. Make sure consumer subscription starts receiving messages from the `Earliest` position. + +[NOTE] +==== + +You don’t need to set the Pulsar topic, nor the consumer name. +By default, the connector uses the channel name (`prices`). +You can configure the `topic` and `consumerName` attributes to override them. + +==== + +[NOTE] +==== + +In Pulsar, consumers need to provide a `subscriptionName` for topic subscriptions. +If not provided the connector is generating a unique *subscription name*. + +==== + +Then, your application can receive the `double` payload directly: + +[source,java] +---- +include::{examples-dir}/pulsar/inbound/PulsarPriceConsumer.java[indent=0] +---- + +Or, you can retrieve the `Message++<++Double++>++`: + +[source,java] +---- +include::{examples-dir}/pulsar/inbound/PulsarPriceMessageConsumer.java[indent=0] +---- + +== Consumer Configuration + +The Pulsar Connector allows flexibly configuring the underlying Pulsar consumer. +One of the ways is to set consumer properties directly on the channel configuration. +The list of available configuration properties are listed in xref:#configuration-reference[Configuration Reference]. + +See the xref:pulsar/client-configuration.adoc[Configuring Pulsar consumers, producers and clients] for more information. + +== Deserialization and Pulsar Schema + +The Pulsar Connector allows configuring Schema configuration for the underlying Pulsar consumer. +See the xref:pulsar/schema-configuration.adoc[Configuring the schema used for Pulsar channels] for more information. + +== Inbound Metadata + +The incoming Pulsar messages include an instance of `PulsarIncomingMessageMetadata` in the metadata. +It provides the key, topic, partitions, headers and so on: + +[source,java] +---- +include::{examples-dir}/pulsar/inbound/PulsarMetadataExample.java[tags=code,indent=0] +---- + +== Acknowledgement + +When a message produced from a Pulsar Message is _acknowledged_, the connector sends an https://pulsar.apache.org/docs/3.0.x/concepts-messaging/#acknowledgment[acknowledgement request] to the Pulsar broker. +All Reactive Messaging messages need to be _acknowledged_, which is handled automatically in most cases. +Acknowledgement requests can be sent to the Pulsar broker using the following two strategies: + +* *Individual acknowledgement* is the default strategy, an acknowledgement request is to the broker for each message. +* *Cumulative acknowledgement*, configured using `ack-strategy=cumulative`, the consumer only acknowledges the last message it received. +All messages in the stream up to (and including) the provided message are not redelivered to that consumer. + +== Failure Management + +If a message produced from a Pulsar message is _nacked_, a failure strategy is applied. +The Pulsar connector supports 4 strategies: + +* `nack` _(default)_ sends https://pulsar.apache.org/docs/3.0.x/concepts-messaging/#negative-acknowledgment[negative acknowledgment] to the broker, triggering the broker to redeliver this message to the consumer. +The negative acknowledgment can be further configured using `negativeAckRedeliveryDelayMicros` and `negativeAck.redeliveryBackoff` properties. +* `fail` fail the application, no more messages will be processed. +* `ignore` the failure is logged, but the acknowledgement strategy will be applied and the processing will continue. +* `continue` the failure is logged, but processing continues without applying acknowledgement or negative acknowledgement. This strategy can be used with xref:#acknowledgement-timeout[acknowledgement timeout] configuration. +* `reconsume-later` sends the message to the https://pulsar.apache.org/docs/3.0.x/concepts-messaging/#retry-letter-topic[retry letter topic] using the `reconsumeLater` API to be reconsumed with a delay. +The delay can be configured using the `reconsumeLater.delay` property and defaults to 3 seconds. +Custom delay or properties per message can be configured by adding an instance of link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/io/smallrye/reactive/messaging/pulsar/PulsarReconsumeLaterMetadata.html[`PulsarReconsumeLaterMetadata`] to the failure metadata. + +For example the following configuration for the incoming channel `data` uses `reconsumer-later` failure strategy with default delays of 60 seconds: + +[source,properties] +---- +mp.messaging.incoming.data.connector=smallrye-pulsar +mp.messaging.incoming.data.serviceUrl=pulsar://localhost:6650 +mp.messaging.incoming.data.topic=data +mp.messaging.incoming.data.schema=INT32 +mp.messaging.incoming.data.failure-strategy=reconsume-later +mp.messaging.incoming.data.retryEnable=true +mp.messaging.incoming.data.reconsumeLater.delay=60 // in seconds +mp.messaging.incoming.data.deadLetterPolicy.retryLetterTopic=data-retry +mp.messaging.incoming.data.deadLetterPolicy.maxRedeliverCount=2 +---- + +=== Acknowledgement timeout + +Similar to the negative acknowledgement, with the https://pulsar.apache.org/docs/3.0.x/concepts-messaging/#acknowledgment-timeout[acknowledgment timeout] mechanism, the Pulsar client tracks the unacknowledged messages, +for the given _ackTimeout_ period and sends _redeliver unacknowledged messages request_ to the broker, thus the broker resends the unacknowledged messages to the consumer. + +To configure the timeout and redelivery backoff mechanism you can set `ackTimeoutMillis` and `ackTimeout.redeliveryBackoff` properties. +The `ackTimeout.redeliveryBackoff` value accepts comma separated values of min delay in milliseconds, max delay in milliseconds and multiplier respectively: + +[source,properties] +---- +mp.messaging.incoming.data.connector=smallrye-pulsar +mp.messaging.incoming.data.failure-strategy=continue +mp.messaging.incoming.data.ackTimeoutMillis=10000 +mp.messaging.incoming.data.ackTimeout.redeliveryBackoff=1000,60000,2 +---- + +=== Dead-letter topic + +The https://pulsar.apache.org/docs/3.0.x/concepts-messaging/#dead-letter-topic[dead letter topic] pushes messages that are not consumed successfully to a dead letter topic an continue message consumption. +Note that dead letter topic can be used in different message redelivery methods, such as acknowledgment timeout, negative acknowledgment or retry letter topic. + +[source,properties] +---- +mp.messaging.incoming.data.connector=smallrye-pulsar +mp.messaging.incoming.data.failure-strategy=nack +mp.messaging.incoming.data.deadLetterPolicy.maxRedeliverCount=2 +mp.messaging.incoming.data.deadLetterPolicy.deadLetterTopic=my-dead-letter-topic +mp.messaging.incoming.data.deadLetterPolicy.initialSubscriptionName=my-dlq-subscription +mp.messaging.incoming.data.subscriptionType=Shared +---- + +[IMPORTANT] +==== + +_Negative acknowledgment_ or _acknowledgment timeout_ methods for redelivery will redeliver the whole batch of messages containing at least an unprocessed message. +See xref:pulsar/sending-messages-to-pulsar.adoc#producer-batching[producer batching] for more information. + +==== + +== Receiving Pulsar Messages in Batches + +By default, incoming methods receive each Pulsar message individually. +You can enable batch mode using `batchReceive=true` property, or setting a `batchReceivePolicy` in consumer configuration. + +[source,java] +---- +include::{examples-dir}/pulsar/inbound/PulsarMessageBatchExample.java[tags=code,indent=0] +---- + +Or you can directly receive the list of payloads to the consume method: + +[source,java] +---- +include::{examples-dir}/pulsar/inbound/PulsarMessageBatchPayloadExample.java[tags=code,indent=0] +---- + +=== Accessing metadata of batch records + +When receiving records in batch mode, the metadata of each record is accessible through the `PulsarIncomingBatchMessageMetadata` : + +[source,java] +---- +include::{examples-dir}/pulsar/inbound/PulsarMessageBatchExample.java[tags=batch,indent=0] +---- + +Like in this example, this can be useful to propagate the tracing information of each record. + +== Configuration Reference + +include::{generated-dir}/smallrye-pulsar-incoming.adoc[indent=0] + +In addition to the configuration properties provided by the connector, +following Pulsar consumer properties can also be set on the channel: + +include::{partialsdir}/pulsar/config/smallrye-pulsar-consumer.adoc[] diff --git a/content/docs/smallrye-reactive-messaging/4/pulsar/schema-configuration.adoc b/content/docs/smallrye-reactive-messaging/4/pulsar/schema-configuration.adoc new file mode 100644 index 000000000..67aaf89d5 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/pulsar/schema-configuration.adoc @@ -0,0 +1,57 @@ += Configuring the schema used for Pulsar channels +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Pulsar messages are stored with payloads as unstructured byte array. +A Pulsar *schema* defines how to serialize structured data to the raw message bytes +The *schema* is applied in producers and consumers to write and read with an enforced data structure. +It serializes data into raw bytes before they are published to a topic and deserializes the raw bytes before they are delivered to consumers. + +Pulsar uses a schema registry as a central repository to store the registered schema information, +which enables producers/consumers to coordinate the schema of a topic's messages through brokers. +By default the Apache BookKeeper is used to store schemas. + +Pulsar API provides built-in schema information for a number of +https://pulsar.apache.org/docs/3.0.x/schema-understand#primitive-type[primitive types] +and https://pulsar.apache.org/docs/3.0.x/schema-understand#complex-type[complex types] such as Key/Value, Avro and Protobuf. + +The Pulsar Connector allows specifying the schema as a primitive type using the `schema` property: + +[source,properties] +---- +mp.messaging.incoming.prices.connector=smallrye-pulsar +mp.messaging.incoming.prices.schema=INT32 + +mp.messaging.outgoing.prices-out.connector=smallrye-pulsar +mp.messaging.outgoing.prices-out.schema=DOUBLE +---- + +If the value for the `schema` property matches a https://javadoc.io/doc/org.apache.pulsar/pulsar-client-api/latest/org/apache/pulsar/common/schema/SchemaType.html[Schema Type] +a simple schema will be created with that type and will be used for that channel. + +The Pulsar Connector allows configuring complex schema types by providing `Schema` beans through CDI, identified with the `@Identifier` qualifier. + +For example the following bean provides an Avro schema and a Key/Value schema: + +[source,java] +---- +include::{examples-dir}/pulsar/configuration/PulsarSchemaProvider.java[indent=0] +---- + +To configure the incoming channel `users` with defined schema, you need to set the `schema` property to the identifier of the schema `user-schema`: + +[source,properties] +---- +mp.messaging.incoming.users.connector=smallrye-pulsar +mp.messaging.incoming.users.schema=user-schema +---- + +If no `schema` property is found, the connector looks for `Schema` beans identified with the channel name. +For example, the outgoing channel `a-channel` will use the key/value schema. + +[source,properties] +---- +mp.messaging.outgoing.a-channel.connector=smallrye-pulsar +---- + +If no schema information is provided incoming channels will use `Schema.AUTO++_++CONSUME()`, whereas outgoing channels will use `Schema.AUTO++_++PRODUCE++_++BYTES()` schemas. diff --git a/content/docs/smallrye-reactive-messaging/4/pulsar/sending-messages-to-pulsar.adoc b/content/docs/smallrye-reactive-messaging/4/pulsar/sending-messages-to-pulsar.adoc new file mode 100644 index 000000000..f3561a08a --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/pulsar/sending-messages-to-pulsar.adoc @@ -0,0 +1,160 @@ += Sending messages to Pulsar +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The Pulsar Connector can write Reactive Messaging `Message`s as Pulsar Message. + +== Example + +Let’s imagine you have a Pulsar broker running, and accessible +using the `pulsar:6650` address (by default it would use +`localhost:1883`). Configure your application to write the messages from +the `prices` channel into a Pulsar Messages as follows: + +[source,properties] +---- +mp.messaging.outgoing.prices.connector=smallrye-pulsar # <1> +mp.messaging.outgoing.prices.serviceUrl=pulsar://pulsar:6650 # <2> +mp.messaging.outgoing.prices.schema=DOUBLE # <3> +---- + +[arabic] +. Sets the connector for the `prices` channel +. Configure the Pulsar broker service url. +. Configure the schema to consume prices as Double. + +[NOTE] +==== + +You don’t need to set the Pulsar topic, nor the producer name. +By default, the connector uses the channel name (`prices`). +You can configure the `topic` and `producerName` attributes to override them. + +==== + +Then, your application must send `Message++<++Double++>++` to the `prices` +channel. It can use `double` payloads as in the following snippet: + +[source,java] +---- +include::{examples-dir}/pulsar/outbound/PulsarPriceProducer.java[indent=0] +---- + +Or, you can send `Message++<++Double++>++`: + +[source,java] +---- +include::{examples-dir}/pulsar/outbound/PulsarPriceMessageProducer.java[indent=0] +---- + +== Producer Configuration + +The Pulsar Connector allows flexibly configuring the underlying Pulsar producer. +One of the ways is to set producer properties directly on the channel configuration. +The list of available configuration properties are listed in xref:#configuration-reference[Configuration Reference]. + +See the xref:pulsar/client-configuration.adoc[Configuring Pulsar consumers, producers and clients] for more information. + +== Serialization and Pulsar Schema + +The Pulsar Connector allows configuring Schema configuration for the underlying Pulsar producer. +See the xref:pulsar/schema-configuration.adoc[Configuring the schema used for Pulsar channels] for more information. + +== Sending key/value pairs + +In order to send Kev/Value pairs to Pulsar, you can configure the Pulsar producer Schema with a +link:https://javadoc.io/doc/org.apache.pulsar/pulsar-client-api/{javadoc-version}/org/apache/pulsar/common/schema/KeyValue.html[`org.apache.pulsar.common.schema.KeyValue`] type: + +[source,java] +---- +include::{examples-dir}/pulsar/outbound/PulsarKeyValueExample.java[tags=code,indent=0] +---- + +If you need more control on the written records, use +`PulsarOutgoingMessageMetadata`. + +== Outbound Metadata + +When sending `Message`s, you can add an instance of +link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-pulsar/{javadoc-version}/io/smallrye/reactive/messaging/pulsar/PulsarOutgoingMessageMetadata.html[`PulsarOutgoingMessageMetadata`] +to influence how the message is going to be written to Pulsar. +For example, configure the record key, and set message properties: + +[source,java] +---- +include::{examples-dir}/pulsar/outbound/PulsarOutboundMetadataExample.java[tags=code,indent=0] +---- + +== OutgoingMessage + +Using link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-pulsar/{javadoc-version}/io/smallrye/reactive/messaging/pulsar/OutgoingMessage.html[`OutgoingMessage`], +is an easy way of customizing the Pulsar message to be published when dealing with payloads and not `Message`s. + +You can create an `OutgoingMessage` with key and value, or from an incoming Pulsar Message: + +[source,java] +---- +include::{examples-dir}/pulsar/outbound/PulsarOutgoingMessageExample.java[tags=code,indent=0] +---- + +== Acknowledgement + +Upon receiving a message from a Producer, a Pulsar broker assigns a `MessageId` to the message and sends it back to the producer, +confirming that the message is published. + +By default, the connector does wait for Pulsar to acknowledge the record +to continue the processing (acknowledging the received `Message`). +You can disable this by setting the `waitForWriteCompletion` attribute to `false`. + +If a record cannot be written, the message is `nacked`. + +[IMPORTANT] +==== + +The Pulsar client automatically retries sending messages in case of failure, until the _send timeout_ is reached. +The _send timeout_ is configurable with `sendTimeoutMs` attribute and by default is is 30 seconds. + +==== + +== Back-pressure and inflight records + +The Pulsar outbound connector handles back-pressure monitoring the number +of pending messages waiting to be written to the Pulsar broker. +The number of pending messages is configured using the +`max-inflight-messages` attribute and defaults to 1000. + +The connector only sends that amount of messages concurrently. No other +messages will be sent until at least one pending message gets +acknowledged by the broker. Then, the connector writes a new message to +Pulsar when one of the broker’s pending messages get acknowledged. + +The Pulsar producer also has a limit on the number of pending messages controlled by +`maxPendingMessages` and for partitioned topics `maxPendingMessagesAcrossPartitions` producer configuration properties. +The default value for the both is `0` which means the limit is enforced by the connector and not the underlying producer. +If a `maxPendingMessagesAcrossPartitions` is set and subscribed topic is partitioned, the connector will set the `max-inflight-messages` use the producer's limit. + +[WARNING] +.Deprecation +==== + +Previously a single connector attribute `maxPendingMessages` controlled both the connector back-pressure and the Pulsar producer configuration. +The `max-inflight-messages` attribute is not the same as the Pulsar producer `maxPendingMessages` configuration property. + +==== + +== Producer Batching + +By default, the Pulsar producer batches individual messages together to be published to the broker. +You can configure batching parameters using `batchingMaxPublishDelayMicros`, `batchingPartitionSwitchFrequencyByPublishDelay`, +`batchingMaxMessages`, `batchingMaxBytes` configuration properties, or disable it completely with `batchingEnabled=false`. + +When using `Key++_++Shared` consumer subscriptions, the `batcherBuilder` can be configured to `BatcherBuilder.KEY++_++BASED`. + +== Configuration Reference + +include::{generated-dir}/smallrye-pulsar-outgoing.adoc[indent=0] + +In addition to the configuration properties provided by the connector, +following Pulsar producer properties can also be set on the channel: + +include::{partialsdir}/pulsar/config/smallrye-pulsar-producer.adoc[] diff --git a/content/docs/smallrye-reactive-messaging/4/pulsar/transactions.adoc b/content/docs/smallrye-reactive-messaging/4/pulsar/transactions.adoc new file mode 100644 index 000000000..d46f4e6ba --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/pulsar/transactions.adoc @@ -0,0 +1,89 @@ += Pulsar Transactions and Exactly-Once Processing +:version-root: ../ +include::{version-root}_attributes.adoc[] + +https://pulsar.apache.org/docs/3.0.x/txn-why/[Pulsar transactions] enable event streaming applications to consume, process, and produce messages in one atomic operation. + +Transactions allow one or multiple producers to send batch of messages to multiple topics where all messages in the batch are eventually visible to any consumer, or none is ever visible to consumers. + +In order to be used, transaction support needs to be activated on the broker configuration: + +[source,properties] +---- +# used to enable transaction coordinator +transactionCoordinatorEnabled=true + +# used to create systemTopic used for transaction buffer snapshot +systemTopicEnabled=true +---- + +On the client side, the transaction support also needs to be enabled on `PulsarClient` configuration: + +[source,properties] +---- +mp.messaging.outgoing.tx-producer.enableTransaction=true +---- + +Pulsar connector provides `PulsarTransactions` custom emitter for writing records inside a transaction. + +It can be used as a regular emitter `@Channel`: + +[source,java] +---- +include::{examples-dir}/pulsar/outbound/PulsarTransactionalProducer.java[indent=0] +---- + +The function given to the `withTransaction` method receives a `TransactionalEmitter` for producing records, and returns a `Uni` that provides the result of the transaction. +If the processing completes successfully, the producer is flushed and the transaction is committed. +If the processing throws an exception, returns a failing `Uni`, or marks the `TransactionalEmitter` for abort, the transaction is aborted. + +[NOTE] +==== + +Multiple transactional producers can participate in a single transaction. +This ensures all messages are sent using the started transaction and before the transaction is committed, all participating producers are flushed. + +==== + +If this method is called on a Vert.x context, the processing function is also called on that context. +Otherwise, it is called on the sending thread of the producer. + +== Exactly-Once Processing + +Pulsar Transactions API also allows managing consumer offsets inside a transaction, together with produced messages. +This in turn enables coupling a consumer with a transactional producer in a consume-transform-produce pattern, +also known as exactly-once processing. +It means that an application consumes messages, processes them, publishes the results to a topic, and commits offsets of the consumed messages in a transaction. + +The `PulsarTransactions` emitter also provides a way to apply exactly-once processing to an incoming Pulsar message inside a transaction. + +The following example includes a batch of Pulsar messages inside a transaction. + +[source,properties] +---- + mp.messaging.outgoing.tx-out-example.enableTransaction=true + # ... + mp.messaging.incoming.in-channel.enableTransaction=true + mp.messaging.incoming.in-channel.batchReceive=true +---- + +[source,java] +---- +include::{examples-dir}/pulsar/outbound/PulsarExactlyOnceProcessor.java[indent=0] +---- + +If the processing completes successfully, the message is acknowledged inside the transaction and the transaction is committed. + +[IMPORTANT] +==== + +When using exactly-once processing, messages can only be acked individually rather than cumulatively. + +==== + +If the processing needs to abort, the message is nack'ed. One of the failure strategies can be employed in order to retry the processing or simply fail-stop. +Note that the `Uni` returned from the `#withTransaction` will yield a failure if the transaction fails and is aborted. + +The application can choose to handle the error case, but for the message consumption to continue, `Uni` returned from the `@Incoming` method must not result in failure. +`PulsarTransactions#withTransactionAndAck` method will ack and nack the message but will not stop the reactive stream. +Ignoring the failure simply resets the consumer to the last committed offsets and resumes the processing from there. diff --git a/content/docs/smallrye-reactive-messaging/4/rabbitmq/rabbitmq-client-customization.adoc b/content/docs/smallrye-reactive-messaging/4/rabbitmq/rabbitmq-client-customization.adoc new file mode 100644 index 000000000..467767813 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/rabbitmq/rabbitmq-client-customization.adoc @@ -0,0 +1,49 @@ += Customizing the underlying RabbitMQ client +:version-root: ../ +include::{version-root}_attributes.adoc[] + +You can customize the underlying RabbitMQ Client configuration by +_producing_ an instance of +https://vertx.io/docs/apidocs/io/vertx/rabbitmq/RabbitMQOptions.html[`RabbitMQOptions`]: + +[source,java] +---- +include::{examples-dir}/rabbitmq/customization/RabbitMQProducers.java[tags=named,indent=0] +---- + +This instance is retrieved and used to configure the client used by the +connector. You need to indicate the name of the client using the +`client-options-name` attribute: + +.... +mp.messaging.incoming.prices.client-options-name=my-named-options +.... + +== Shared connections + +By default, each channel opens its own connection to the RabbitMQ +broker. If your application has multiple channels connecting to the same +broker, you can configure them to share a single underlying connection +using the `shared-connection-name` attribute: + +[source,properties] +---- +mp.messaging.incoming.orders.connector=smallrye-rabbitmq +mp.messaging.incoming.orders.shared-connection-name=my-connection + +mp.messaging.outgoing.confirmations.connector=smallrye-rabbitmq +mp.messaging.outgoing.confirmations.shared-connection-name=my-connection +---- + +In the above example, the `orders` incoming channel and the +`confirmations` outgoing channel share the same RabbitMQ connection. + +All channels sharing a connection name *must* use identical connection +options (host, port, credentials, SSL settings, virtual host, etc.). +If two channels declare the same `shared-connection-name` but have +different connection options, the connector throws an +`IllegalStateException` at startup. + +Shared connections are useful when your application has many channels +connecting to the same broker and you want to reduce the number of TCP +connections, or when the broker imposes connection limits. diff --git a/content/docs/smallrye-reactive-messaging/4/rabbitmq/rabbitmq-cloud.adoc b/content/docs/smallrye-reactive-messaging/4/rabbitmq/rabbitmq-cloud.adoc new file mode 100644 index 000000000..b1df1aed5 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/rabbitmq/rabbitmq-cloud.adoc @@ -0,0 +1,52 @@ += Connecting to managed instances +:version-root: ../ +include::{version-root}_attributes.adoc[] + +This section describes the connector configuration to use managed +RabbitMQ instances (hosted on the Cloud). + +== Cloud AMQP + +To connect to an instance of RabbitMQ hosted on https://www.cloudamqp.com/[Cloud +AMQP], use the following configuration: + +[source,properties] +---- +rabbitmq-host=host-name +rabbitmq-port=5671 +rabbitmq-username=user-name +rabbitmq-password=password +rabbitmq-virtual-host=user-name +rabbitmq-ssl=true +---- + +You can extract the values from the `AMQPS` url displayed on the +administration portal: + +.... +amqps://user-name:password@host/user-name +.... + +== Amazon MQ + +https://aws.amazon.com/amazon-mq/[Amazon MQ] can host RabbitMQ brokers +(as well as AMQP 1.0 brokers). To connect to a RabbitMQ instance hosted +on Amazon MQ, use the following configuration: + +[source,properties] +---- +rabbitmq-host=host-name +rabbitmq-port=5671 +rabbitmq-username=user-name +rabbitmq-password=password +rabbitmq-ssl=true +---- + +You can extract the host value from the `AMQPS` url displayed on the +administration console: + +.... +amqps://foobarbaz.mq.us-east-2.amazonaws.com:5671 +.... + +The username and password are configured during the broker creation. diff --git a/content/docs/smallrye-reactive-messaging/4/rabbitmq/rabbitmq-health.adoc b/content/docs/smallrye-reactive-messaging/4/rabbitmq/rabbitmq-health.adoc new file mode 100644 index 000000000..3d2be2654 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/rabbitmq/rabbitmq-health.adoc @@ -0,0 +1,27 @@ += Health reporting +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The RabbitMQ connector reports the readiness and liveness of each +channel managed by the connector. + +On the inbound side (receiving messages from RabbitMQ), the check +verifies that the receiver is connected to the broker. + +On the outbound side (sending records to RabbitMQ), the check verifies +that the sender is not disconnected from the broker; the sender _may_ +still be in an initialized state (connection not yet attempted), but +this is regarded as live/ready. + +You can disable health reporting by setting the `health-enabled` attribute of the channel to `false`. +It disables both liveness and readiness. +You can disable readiness reporting by setting the `health-readiness-enabled` attribute of the channel to `false`. + +== @Channel and lazy subscription + +When you inject a channel using `@Channel` annotation, you are responsible for subscribing to the channel. +Until the subscription happens, the channel is not connected to the broker and thus cannot receive messages. +The default health check will fail in this case. + +To handle this use case, you need to configure the `health-lazy-subscription` attribute of the channel to `true`. +It configures the health check to not fail if there are no subscription yet. diff --git a/content/docs/smallrye-reactive-messaging/4/rabbitmq/rabbitmq.adoc b/content/docs/smallrye-reactive-messaging/4/rabbitmq/rabbitmq.adoc new file mode 100644 index 000000000..35bb40e7c --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/rabbitmq/rabbitmq.adoc @@ -0,0 +1,57 @@ += RabbitMQ Connector +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The RabbitMQ Connector adds support for RabbitMQ to Reactive Messaging, +based on the AMQP 0-9-1 Protocol Specification. + +Advanced Message Queuing Protocol 0-9-1 (https://www.rabbitmq.com/resources/specs/amqp0-9-1.pdf[AMQP +0-9-1]) is an +open standard for passing business messages between applications or +organizations. + +With this connector, your application can: + +* receive messages from a RabbitMQ queue +* send messages to a RabbitMQ exchange + +The RabbitMQ connector is based on the https://vertx.io/docs/vertx-rabbitmq-client/java/[Vert.x RabbitMQ +Client]. + +[IMPORTANT] +==== + +The *AMQP connector* supports the AMQP 1.0 protocol, which is very +different from AMQP 0-9-1. You _can_ use the AMQP connector with +RabbitMQ provided that the latter has the https://github.com/rabbitmq/rabbitmq-amqp1.0/blob/v3.7.x/README.md[AMQP 1.0 +Plugin] +installed, albeit with reduced functionality. + +==== + +== Using the RabbitMQ connector + +To use the RabbitMQ Connector, add the following dependency to your +project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.reactive + smallrye-reactive-messaging-rabbitmq + {project-version} + +---- + +The connector name is: `smallrye-rabbitmq`. + +So, to indicate that a channel is managed by this connector you need: + +[source,properties] +---- +# Inbound +mp.messaging.incoming.[channel-name].connector=smallrye-rabbitmq + +# Outbound +mp.messaging.outgoing.[channel-name].connector=smallrye-rabbitmq +---- diff --git a/content/docs/smallrye-reactive-messaging/4/rabbitmq/receiving-messages-from-rabbitmq.adoc b/content/docs/smallrye-reactive-messaging/4/rabbitmq/receiving-messages-from-rabbitmq.adoc new file mode 100644 index 000000000..4cb450d37 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/rabbitmq/receiving-messages-from-rabbitmq.adoc @@ -0,0 +1,324 @@ += Receiving messages from RabbitMQ +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The RabbitMQ connector lets you retrieve messages from a https://www.rabbitmq.com/[RabbitMQ +broker]. The RabbitMQ connector retrieves +_RabbitMQ Messages_ and maps each of them into Reactive Messaging +`Messages`. + +[NOTE] +==== + +In this context, the reactive messaging concept of a _Channel_ is +realised as a https://www.rabbitmq.com/queues.html[RabbitMQ Queue]. + +==== + +== Example + +Let’s imagine you have a RabbitMQ broker running, and accessible using +the `rabbitmq:5672` address (by default it would use `localhost:5672`). +Configure your application to receive RabbitMQ Messages on the `prices` +channel as follows: + +[source,properties] +---- +rabbitmq-host=rabbitmq # <1> +rabbitmq-port=5672 # <2> +rabbitmq-username=my-username # <3> +rabbitmq-password=my-password # <4> + +mp.messaging.incoming.prices.connector=smallrye-rabbitmq # <5> +mp.messaging.incoming.prices.queue.name=my-queue # <6> +mp.messaging.incoming.prices.routing-keys=urgent # <7> +---- + +[arabic] +. Configures the broker/router host name. You can do it per channel +(using the `host` attribute) or globally using `rabbitmq-host`. +. Configures the broker/router port. You can do it per channel (using +the `port` attribute) or globally using `rabbitmq-port`. The default +is 5672. +. Configures the broker/router username if required. You can do it per +channel (using the `username` attribute) or globally using +`rabbitmq-username`. +. Configures the broker/router password if required. You can do it per +channel (using the `password` attribute) or globally using +`rabbitmq-password`. +. Instructs the `prices` channel to be managed by the RabbitMQ +connector. +. Configures the RabbitMQ queue to read messages from. +. Configures the binding between the RabbitMQ exchange and the +RabbitMQ queue using a routing key. The default is `#` (all messages +will be forwarded from the exchange to the queue) but in general +this can be a comma-separated list of one or more keys. + +Then, your application receives `Message++<++String++>++`. You can consume the +payload directly: + +[source,java] +---- +include::{examples-dir}/rabbitmq/inbound/RabbitMQPriceConsumer.java[indent=0] +---- + +Or, you can retrieve the `Message++<++String++>++`: + +[source,java] +---- +include::{examples-dir}/rabbitmq/inbound/RabbitMQPriceMessageConsumer.java[indent=0] +---- + +[NOTE] +==== + +Whether you need to explicitly acknowledge the message depends on the +`auto-acknowledgement` channel setting; if that is set to `true` then +your message will be automatically acknowledged on receipt. + +==== + +== Deserialization + +The connector converts incoming RabbitMQ Messages into Reactive +Messaging `Message++<++T++>++` instances. The payload type `T` depends on the +value of the RabbitMQ received message Envelope `content++_++type` and +`content++_++encoding` properties. + +[width="100%",cols="7%,6%,87%",options="header",] +|=== +|content++_++encoding |content++_++type |Type +|_Value present_ |_n/a_ |`io.vertx.core.buffer.Buffer` +|_No value_ |`text/plain` |`String` +|_No value_ |`application/json` |a JSON element which can be a https://vertx.io/docs/apidocs/io/vertx/core/json/JsonArray.html[`JsonArray`], https://vertx.io/docs/apidocs/io/vertx/core/json/JsonObject.html[`JsonObject`], `String`, ... if the buffer contains an array, object, string,... +|_No value_ |`application/json` |Anything else (if one of the `JsonMapping` modules like `smallrye-reactive-messaging-jackson`, `smallrye-reactive-messaging-jsonb` is present on the classpath) +|_No value_ |_Anything else_ |`io.vertx.core.buffer.Buffer` +|=== + +If you send objects with this RabbitMQ connector (outbound connector), +they are encoded as JSON and sent with `content++_++type` set to +`application/json`. You can receive this payload using (Vert.x) JSON +Objects, and then map it to the object class you want: + +[source,java] +---- +@ApplicationScoped +public static class Generator { + + @Outgoing("to-rabbitmq") + public Multi prices() { // <1> + AtomicInteger count = new AtomicInteger(); + return Multi.createFrom().ticks().every(Duration.ofMillis(1000)) + .map(l -> new Price().setPrice(count.incrementAndGet())) + .onOverflow().drop(); + } + +} + +@ApplicationScoped +public static class Consumer { + + List prices = new CopyOnWriteArrayList<>(); + + @Incoming("from-rabbitmq") + public void consume(JsonObject p) { // <2> + Price price = p.mapTo(Price.class); // <3> + prices.add(price); + } + + public List list() { + return prices; + } +} +---- + +[arabic] +. The `Price` instances are automatically encoded to JSON by the +connector +. You can receive it using a `JsonObject` +. Then, you can reconstruct the instance using the `mapTo` method + +== Inbound Metadata + +Messages coming from RabbitMQ contain an instance of link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-rabbitmq/{javadoc-version}/io/smallrye/reactive/messaging/rabbitmq/IncomingRabbitMQMetadata.html[`IncomingRabbitMQMetadata`] +in the metadata. + +RabbitMQ message headers can be accessed from the metadata either by +calling `getHeader(String header, Class++<++T++>++ type)` to retrieve a single +header value. or `getHeaders()` to get a map of all header values. + +[source,java] +---- +include::{examples-dir}/rabbitmq/inbound/RabbitMQMetadataExample.java[tags=code,indent=0] +---- + +The type `++<++T++>++` of the header value depends on the RabbitMQ type used for +the header: + +[cols=",",options="header",] +|=== +|RabbitMQ Header Type |T +|String |`String` +|Boolean |`Boolean` +|Number |`Number` +|List |`java.util.List` +|=== + +If you need to create `IncomingRabbitMQMetadata` e.g. for testing purposes, +you can create an `link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/io/smallrye/reactive/messaging/rabbitmq/OutgoingRabbitMQMetadata.html[`OutgoingRabbitMQMetadata`]` +using its builder and convert it using `OutgoingRabbitMQMetadata.toIncomingMetadata(String, boolean)` + +== Acknowledgement + +When a Reactive Messaging Message associated with a RabbitMQ Message is +acknowledged, it informs the broker that the message has been +_accepted_. + +Whether you need to explicitly acknowledge the message depends on the +`auto-acknowledgement` setting for the channel; if that is set to `true` +then your message will be automatically acknowledged on receipt. + +== Failure Management + +If a message produced from a RabbitMQ message is _nacked_, a failure +strategy is applied. The RabbitMQ connector supports three strategies, +controlled by the `failure-strategy` channel setting: + +* `fail` - fail the application; no more RabbitMQ messages will be +processed. The RabbitMQ message is marked as rejected. +* `accept` - this strategy marks the RabbitMQ message as accepted. The +processing continues ignoring the failure. +* `reject` - this strategy marks the RabbitMQ message as rejected +(default). The processing continues with the next message. +* `requeue` - this strategy marks the RabbitMQ message as rejected +with requeue flag to true. The processing continues with the next message, +but the requeued message will be redelivered to the consumer. + +When using `dead-letter-queue`, it is also possible to change some +metadata of the record that is sent to the dead letter topic. To do +that, use the `Message.nack(Throwable, Metadata)` method: + +The RabbitMQ reject `requeue` flag can be controlled on different failure strategies +using the link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/io/smallrye/reactive/messaging/rabbitmq/RabbitMQRejectMetadata.html[`RabbitMQRejectMetadata`]. +To do that, use the `Message.nack(Throwable, Metadata)` method by including the +`RabbitMQRejectMetadata` metadata with `requeue` to `true`. + +[source,java] +---- +include::{examples-dir}/rabbitmq/inbound/RabbitMQRejectMetadataExample.java[tags=code,indent=0] +---- + +[WARNING] +.Experimental +==== + +`RabbitMQFailureHandler` is experimental and APIs are subject to change in the future + +==== + +In addition, you can also provide your own failure strategy. +To provide a failure strategy implement a bean exposing the interface +link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/io/smallrye/reactive/messaging/rabbitmq/fault/RabbitMQFailureHandler.html[`RabbitMQFailureHandler`], +qualified with a `@Identifier`. +Set the name of the bean as the `failure-strategy` channel setting. + +== Configuration Reference + +include::{generated-dir}/smallrye-rabbitmq-incoming.adoc[indent=0] + +To use an existing _queue_, you need to configure the `queue.name` +attribute. + +For example, if you have a RabbitMQ broker already configured with a +queue called `peopleQueue` that you wish to read messages from, you need +the following configuration: + +[source,properties] +---- +mp.messaging.incoming.people.connector=smallrye-rabbitmq +mp.messaging.incoming.people.queue.name=peopleQueue +---- + +If you want RabbitMQ to create the queue for you but bind it to an +existing topic exchange `people`, you need the following configuration: + +[source,properties] +---- +mp.messaging.incoming.people.connector=smallrye-rabbitmq +mp.messaging.incoming.people.queue.name=peopleQueue +mp.messaging.incoming.people.queue.declare=true +---- + +[NOTE] +==== + +In the above the channel name `people` is implicitly assumed to be the +name of the exchange; if this is not the case you would need to name the +exchange explicitly using the `exchange.name` property. + +==== + +[NOTE] +==== + +The connector supports RabbitMQ's "Server-named Queues" feature to create +an exclusive, auto-deleting, non-durable and randomly named queue. To +enable this feature you set the queue name to exactly `(server.auto)`. +Using this name not only enables the server named queue feature but also +automatically makes ths queue exclusive, auto-deleting, and non-durable; +therefore ignoring any values provided for the `exclusive`, `auto-delete` +and `durable` options. + +==== + +If you want RabbitMQ to create the `people` exchange, queue and binding, +you need the following configuration: + +[source,properties] +---- +mp.messaging.incoming.people.connector=smallrye-rabbitmq +mp.messaging.incoming.people.exchange.declare=true +mp.messaging.incoming.people.queue.name=peopleQueue +mp.messaging.incoming.people.queue.declare=true +mp.messaging.incoming.people.queue.routing-keys=tall,short +---- + +In the above we have used an explicit list of routing keys rather than +the default (`#`). Each component of the list creates a separate binding +between the queue and the exchange, so in the case above we would have +two bindings; one based on a routing key of `tall`, the other based on +one of `short`. + +[NOTE] +==== + +The default value of `routing-keys` is `#` (indicating a match against +all possible routing keys) which is only appropriate for _topic_ +Exchanges. If you are using other types of exchange and/or need to +declare queue bindings, you’ll need to supply a valid value for the +exchange in question. + +==== + +== Custom arguments for Queue declaration + +When queue declaration is made by the Reactive Messaging channel, using the `queue.declare=true` configuration, +custom queue arguments can be specified using the `queue.arguments` attribute. +`queue.arguments` accepts the identifier (using the `@Identifier` qualifier) of a `Map++<++String, Object++>++` exposed as a CDI bean. +If no arguments has been configured, the default *rabbitmq-queue-arguments* identifier is looked for. + +The following CDI bean produces such a configuration identified with *my-arguments*: + +[source,java] +---- +include::{examples-dir}/rabbitmq/customization/ArgumentProducers.java[indent=0] +---- + +Then the channel can be configured to use those arguments in exchange declaration: + +[source,properties] +---- +mp.messaging.outgoing.data.queue.arguments=my-arguments +---- + +Similarly, the `dead-letter-queue.arguments` allows configuring custom arguments for dead letter queue when one is declared (`auto-bind-dlq=true`). diff --git a/content/docs/smallrye-reactive-messaging/4/rabbitmq/request-reply.adoc b/content/docs/smallrye-reactive-messaging/4/rabbitmq/request-reply.adoc new file mode 100644 index 000000000..094e929e0 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/rabbitmq/request-reply.adoc @@ -0,0 +1,139 @@ += RabbitMQ Request/Reply +:version-root: ../ +include::{version-root}_attributes.adoc[] + +[WARNING] +.Experimental +==== + +RabbitMQ Request Reply Emitter is an experimental feature. + +==== + +The RabbitMQ https://www.enterpriseintegrationpatterns.com/patterns/messaging/RequestReply.html[Request-Reply] pattern allows you to publish a message to a RabbitMQ address and then await for a reply message that responds to the initial request, which can be used to implement https://www.rabbitmq.com/tutorials/tutorial-six-java[RPC over RabbitMQ]. + +Given the following request/reply outgoing configuration example: + +[source,properties] +---- +mp.messaging.outgoing.my-request.connector=smallrye-rabbitmq +mp.messaging.outgoing.my-request.exchange.name=rpc +mp.messaging.outgoing.my-request.exchange.type=direct +mp.messaging.outgoing.my-request.default-routing-key=rpc-request +---- + +The `RabbitMQRequestReply` emitter implements the requestor (or the client) of the request-reply pattern for RabbitMQ outbound channels: + +[source,java] +---- +include::{examples-dir}/rabbitmq/outbound/RabbitMQRequestReplyEmitter.java[indent=0] +---- + +[NOTE] +==== + +While in this example we use a direct exchange, it is also possible to use a topic or fanout exchange as well as https://www.rabbitmq.com/docs/local-random-exchange[Local Random Exchanges] for request/reply. + +==== + +The `request` method publishes the request message to the configured target address of the outgoing channel leveraging RabbitMQ https://www.rabbitmq.com/docs/direct-reply-to[Direct Reply-To], +and waits for a reply. +When the reply is received the returned `Uni` is completed with the message payload. + +The request send operation generates a correlation id and sets the `correlationId` property, +which it expects to be sent back in the reply message. + +The replier (or the server) can be implemented using a Reactive Messaging processor: + +[source,properties] +---- +mp.messaging.incoming.request.connector=smallrye-rabbitmq +mp.messaging.incoming.request.exchange.name=rpc +mp.messaging.incoming.request.exchange.type=direct +mp.messaging.incoming.request.routing-keys=rpc-request + +mp.messaging.outgoing.reply.connector=smallrye-rabbitmq +mp.messaging.outgoing.reply.exchange.name="" +---- + +[source,java] +---- +include::{examples-dir}/rabbitmq/outbound/RabbitMQReplier.java[indent=0] +---- + +== Requesting with `Message` types + +Like the core Emitter's `send` methods, `request` method also can receive a `Message` type and return a message: + +[source,java] +---- +include::{examples-dir}/rabbitmq/outbound/RabbitMQRequestReplyMessageEmitter.java[indent=0] +---- + +[NOTE] +==== + +The ingested reply type of the `RabbitMQRequestReply` is discovered at runtime, +in order to configure a `MessageConverter` to be applied on the incoming message before returning the `Uni` result. + +==== + +== Requesting multiple replies + +You can use the `requestMulti` method to expect any number of replies represented by the `Multi` return type. + +For example this can be used to aggregate multiple replies to a single request. + +[source,java] +---- +include::{examples-dir}/rabbitmq/outbound/RabbitMQRequestReplyMultiEmitter.java[indent=0] +---- + +Like the other `request` you can also request `Message` types. + +[NOTE] +==== + +The channel attribute `reply.timeout` will be applied between each message, if reached the returned `Multi` will +fail. + +==== + +== Scaling Request/Reply + +If multiple requestor instances are configured on the same outgoing address, and the same reply address, +each requestor instance will receive replies of all instances. If an observed correlation id doesn't match +the id of any pending replies, the reply is simply discarded. +With the additional network traffic this allows scaling requestors, (and repliers) dynamically. + +== Pending replies and reply timeout + +By default, the `Uni` returned from the `request` method is configured to fail with timeout exception if no replies is received after 5 seconds. +This timeout is configurable with the channel attribute `reply.timeout`. + +A snapshot of the list of pending replies is available through the `RabbitMQRequestReply#getPendingReplies` method. + +== Correlation Ids + +The RabbitMQ Request/Reply allows configuring the correlation id mechanism completely through a `CorrelationIdHandler` implementation. +The default handler is based on randomly generated UUID strings. +The correlation id handler implementation can be configured using the `reply.correlation-id.handler` attribute. +As mentioned the default configuration is `uuid`. + +Custom handlers can be implemented by proposing a CDI-managed bean with `@Identifier` qualifier. + +== Reply Error Handling + +If the reply server produces an error and can or would like to propagate the error back to the requestor, failing the returned `Uni`. + +If configured using the `reply.failure.handler` channel attribute, +the `ReplyFailureHandler` implementations are discovered through CDI, matching the `@Identifier` qualifier. + +A sample reply error handler can lookup header values and return the error to be thrown by the reply: + +[source,java] +---- +include::{examples-dir}/rabbitmq/outbound/MyRabbitMQReplyFailureHandler.java[indent=0] +---- + +`null` return value indicates that no error has been found in the reply, and it can be delivered to the application. diff --git a/content/docs/smallrye-reactive-messaging/4/rabbitmq/sending-messages-to-rabbitmq.adoc b/content/docs/smallrye-reactive-messaging/4/rabbitmq/sending-messages-to-rabbitmq.adoc new file mode 100644 index 000000000..320a71d22 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/rabbitmq/sending-messages-to-rabbitmq.adoc @@ -0,0 +1,193 @@ += Sending messages to RabbitMQ +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The RabbitMQ connector can write Reactive Messaging `Messages` as +RabbitMQ Messages. + +[NOTE] +==== + +In this context, the reactive messaging concept of a _Channel_ is +realised as a https://www.rabbitmq.com/tutorials/amqp-concepts.html#exchanges[RabbitMQ +Exchange]. + +==== + +== Example + +Let’s imagine you have a RabbitMQ broker running, and accessible using +the `rabbitmq:5672` address (by default it would use `localhost:5672`). +Configure your application to send the messages from the `prices` +channel as a RabbitMQ Message as follows: + +.... +rabbitmq-host=rabbitmq # <1> +rabbitmq-port=5672 # <2> +rabbitmq-username=my-username # <3> +rabbitmq-password=my-password # <4> + +mp.messaging.outgoing.prices.connector=smallrye-rabbitmq # <5> +mp.messaging.outgoing.prices.default-routing-key=normal # <6> +.... + +[arabic] +. Configures the broker/router host name. You can do it per channel +(using the `host` attribute) or globally using `rabbitmq-host` +. Configures the broker/router port. You can do it per channel (using +the `port` attribute) or globally using `rabbitmq-port`. The default +is `5672`. +. Configures the broker/router username if required. You can do it per +channel (using the `username` attribute) or globally using +`rabbitmq-username`. +. Configures the broker/router password if required. You can do it per +channel (using the `password` attribute) or globally using +`rabbitmq-password`. +. Instructs the `prices` channel to be managed by the RabbitMQ +connector +. Supplies the default routing key to be included in outbound +messages; this will be if the "raw payload" form of message sending +is used (see below). + +[NOTE] +==== + +You don’t need to set the RabbitMQ exchange name. By default, it uses +the channel name (`prices`) as the name of the exchange to send messages +to. You can configure the `exchange.name` attribute to override it. + +==== + +Then, your application can send `Message++<++Double++>++` to the prices channel. +It can use `double` payloads as in the following snippet: + +[source,java] +---- +include::{examples-dir}/rabbitmq/outbound/RabbitMQPriceProducer.java[indent=0] +---- + +Or, you can send `Message++<++Double++>++`, which affords the opportunity to +explicitly specify metadata on the outgoing message: + +[source,java] +---- +include::{examples-dir}/rabbitmq/outbound/RabbitMQPriceMessageProducer.java[indent=0] +---- + +== Serialization + +When sending a `Message++<++T++>++`, the connector converts the message into a +RabbitMQ Message. The payload is converted to the RabbitMQ Message body. + +[width="100%",cols="59%,41%",options="header",] +|=== +|T |RabbitMQ Message Body +|primitive types or `UUID`/`String` |String value with `content++_++type` set to `text/plain` +|https://vertx.io/docs/apidocs/io/vertx/core/json/JsonObject.html[`JsonObject`] or https://vertx.io/docs/apidocs/io/vertx/core/json/JsonArray.html[`JsonArray`] |Serialized String payload with `content++_++type` set to `application/json` +|`io.vertx.mutiny.core.buffer.Buffer` |Binary content, with `content++_++type` set to `application/octet-stream` +|`byte++[]++` |Binary content, with content++_++type set to `application/octet-stream` +|Any other class |The payload is converted to JSON (using a Json Mapper) then serialized with `content++_++type` set to `application/json` +|=== + +If the message payload cannot be serialized to JSON, the message is +_nacked_. + +== Outbound Metadata + +When sending `Messages`, you can add an instance of link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-rabbitmq/{javadoc-version}/io/smallrye/reactive/messaging/rabbitmq/OutgoingRabbitMQMetadata.html[`OutgoingRabbitMQMetadata`] +to influence how the message is handled by RabbitMQ. For example, you +can configure the routing key, timestamp and headers: + +[source,java] +---- +include::{examples-dir}/rabbitmq/outbound/RabbitMQOutboundMetadataExample.java[tags=code,indent=0] +---- + +== Acknowledgement + +By default, the Reactive Messaging `Message` is acknowledged when the +broker acknowledges the message. + +== Configuration Reference + +include::{generated-dir}/smallrye-rabbitmq-outgoing.adoc[indent=0] + +== Using existing destinations + +To use an existing _exchange_, you may need to configure the +`exchange.name` attribute. + +For example, if you have a RabbitMQ broker already configured with an +exchange called `people` that you wish to send messages to, you need the +following configuration: + +[source,properties] +---- +mp.messaging.outgoing.people.connector=smallrye-rabbitmq +---- + +You would need to configure the `exchange.name` attribute, if the +exchange name were not the channel name: + +[source,properties] +---- +mp.messaging.outgoing.people-out.connector=smallrye-rabbitmq +mp.messaging.outgoing.people-out.exchange.name=people +---- + +If you want RabbitMQ to create the `people` exchange, you need the +following configuration: + +[source,properties] +---- +mp.messaging.outgoing.people-out.connector=smallrye-amqp +mp.messaging.outgoing.people-out.exchange.name=people +mp.messaging.outgoing.people-out.exchange.declare=true +---- + +[NOTE] +==== + +The above example will create a `topic` exchange and use an empty +default `routing-key` (unless overridden programatically using outgoing +metadata for the message). If you want to create a different type of +exchange or have a different default routing key, then the +`exchange.type` and `default-routing-key` properties need to be +explicitly specified. + +==== + +== Sending to specific queues via the default exchange + +To send a message to a specific queue (usually a reply queue), +you have to configure the default exchange as an outgoing channel and set the name of the queue as routing key in the message metadata. +The name of the exchange needs to be set to `""`. + +[source,properties] +---- +mp.messaging.outgoing.channel-name-for-default-exchange.connector=smallrye-rabbitmq +mp.messaging.outgoing.channel-name-for-default-exchange.exchange.name="" +---- + +== Custom arguments for Exchange declaration + +When exchange declaration is made by the Reactive Messaging channel, using the `exchange.declare=true` configuration, +custom exchange arguments can be specified using the `exchange.arguments` attribute. +`exchange.arguments` accepts the identifier (using the `@Identifier` qualifier) of a `Map++<++String, Object++>++` exposed as a CDI bean. +If no arguments has been configured, the default *rabbitmq-exchange-arguments* identifier is looked for. + +The following CDI bean produces such a configuration identified with *my-arguments*: + +[source,java] +---- +include::{examples-dir}/rabbitmq/customization/ArgumentProducers.java[indent=0] +---- + +Then the channel can be configured to use those arguments in exchange declaration: + +[source,properties] +---- +mp.messaging.outgoing.data.exchange.arguments=my-arguments +---- + +Similarly, the `dead-letter-exchange.arguments` allows configuring custom arguments for dead letter exchange when one is declared (`dlx.declare=true`). diff --git a/content/docs/smallrye-reactive-messaging/4/sns/sending-aws-sns-messages.adoc b/content/docs/smallrye-reactive-messaging/4/sns/sending-aws-sns-messages.adoc new file mode 100644 index 000000000..69282c824 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/sns/sending-aws-sns-messages.adoc @@ -0,0 +1,72 @@ += Sending AWS SNS Messages +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The AWS SNS connector allows you to send messages to an AWS SNS topic. + +== Sending messages + +Before you start, you need to have an AWS account and an SNS topic created. +To send messages to an SNS topic, you need to create a method that produces messages to the topic. + +Then, your application can send `Message++<++String++>++` to the prices channel. +It can use `String` payloads as in the following snippet: + +[source,java] +---- +include::{examples-dir}/sns/outbound/SnsProducer.java[indent=0] +---- + +Or, you can send `Message++<++Double++>++`, which affords the opportunity to +explicitly specify metadata on the outgoing message: + +[source,java] +---- +include::{examples-dir}/sns/outbound/SnsMessageProducer.java[indent=0] +---- + +== Sending messages in batch + +You can configure the outbound channel to send messages in batch of maximum 10 messages (AWS SNS limitation). + +You can customize the size of batches, `10` being the default batch size, and the delay to wait for new messages to be added to the batch, 3000ms being the default delay: + +[source,java] +---- +mp.messaging.outgoing.prices.connector=smallrye-sns +mp.messaging.outgoing.prices.topic=prices +mp.messaging.outgoing.prices.batch=true +mp.messaging.outgoing.prices.batch-size=5 +mp.messaging.outgoing.prices.batch-delay=3000 +---- + +== Serialization + +When sending a `Message++<++T++>++`, the connector converts the message into a AWS SNS Message. +How the message is converted depends on the payload type: + +* If the payload is of type `PublishRequest` it is sent as is. +* If the payload is of type `PublishRequest.Builder`, the topic url is set and sent. +* If the payload is of primitive types the payload is converted to String and the message attribute `++_++classname` is set to the class name of the payload. +* If the payload is of any other object type, the payload is serialized (using the `JsonMapping` implementation discovered) and the message attribute `++_++classname` is set to the class name of the payload. + +If the message payload cannot be serialized to JSON, the message is _nacked_. + +== Outbound Metadata + +When sending `Messages`, you can add an instance of link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-aws-sns/{javadoc-version}/io/smallrye/reactive/messaging/aws/sns/SnsOutboundMetadata.html[`SnsOutboundMetadata`] +to influence how the message is handled by AWS SNS. For example, you can configure the routing key, timestamp and headers: + +[source,java] +---- +include::{examples-dir}/sns/outbound/SnsOutboundMetadataExample.java[tags=code,indent=0] +---- + +== Acknowledgement + +By default, the Reactive Messaging `Message` is acknowledged when the +send message request is successful. If the message is not sent successfully, the message is _nacked_. + +== Configuration Reference + +include::{generated-dir}/smallrye-sns-outgoing.adoc[indent=0] diff --git a/content/docs/smallrye-reactive-messaging/4/sns/sns.adoc b/content/docs/smallrye-reactive-messaging/4/sns/sns.adoc new file mode 100644 index 000000000..b24bf08f7 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/sns/sns.adoc @@ -0,0 +1,59 @@ += AWS SNS Connector +:version-root: ../ +include::{version-root}_attributes.adoc[] + +[WARNING] +.Preview +==== + +The AWS SNS Connector is currently in *preview*. + +==== + +The AWS SNS Connector adds support for AWS SNS to Reactive Messaging. + +With this connector, your application can send messages to a AWS SNS topic. + +The AWS SNS connector is based on the https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/home.html[AWS SDK for Java V2]. + +== Using the AWS SNS connector + +To use the AWS SNS Connector, add the following dependency to your project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.reactive + smallrye-reactive-messaging-aws-sns + {project-version} + +---- + +The connector name is: `smallrye-sns`. + +So, to indicate that a channel is managed by this connector you need: + +[source,properties] +---- +# Inbound +mp.messaging.incoming.[channel-name].connector=smallrye-sns + +# Outbound +mp.messaging.outgoing.[channel-name].connector=smallrye-sns +---- + +== Configuration + +When available the AWS SNS connector discovers the SNS client as a CDI bean. +This allows using the connector with the https://docs.quarkiverse.io/quarkus-amazon-services/dev/amazon-sns.html[Quarkiverse AWS SNS extension]. +If the SNS client is not available as a CDI bean, the connector creates a new client using the provided configuration. + +* `topic` - The name of the SNS queue, defaults to channel name if not provided. +* `region` - The name of the SNS region. +* `endpoint-override` - The endpoint url override. +* `credentials-provider` - The fully qualified class name of the credential provider to be used in the client, if not provided the default provider chain is used. + +== Additional Resources + +* https://docs.aws.amazon.com/sns/index.html[AWS SNS Documentation] +* https://docs.quarkiverse.io/quarkus-amazon-services/dev/amazon-sns.html[Quarkiverse AWS SNS Extension] diff --git a/content/docs/smallrye-reactive-messaging/4/sqs/receiving-aws-sqs-messages.adoc b/content/docs/smallrye-reactive-messaging/4/sqs/receiving-aws-sqs-messages.adoc new file mode 100644 index 000000000..2b03c4832 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/sqs/receiving-aws-sqs-messages.adoc @@ -0,0 +1,140 @@ += Receiving AWS SQS Messages +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The AWS SQS connector allows you to receive messages from an AWS SQS queue. + +== Receiving messages + +Before you start, you need to have an AWS account and a SQS queue created. +To receive messages from an SQS queue, you need to create a method that consumes messages from the queue. + +[source,properties] +---- +mp.messaging.incoming.data.connector=smallrye-sqs +mp.messaging.incoming.data.queue=my-queue +---- + +Then, your application receives `Message++<++String++>++`. +You can consume the payload directly: + +[source,java] +---- +include::{examples-dir}/sqs/inbound/SqsStringConsumer.java[indent=0] +---- + +Or, you can retrieve the `Message++<++String++>++`: + +[source,java] +---- +include::{examples-dir}/sqs/inbound/SqsMessageStringConsumer.java[indent=0] +---- + +You also can directly consume the `software.amazon.awssdk.services.sqs.model.Message`: + +[source,java] +---- +include::{examples-dir}/sqs/inbound/SqsSdkMessageConsumer.java[indent=0] +---- + +=== Receive message request customizer + +The receive message requests sent to AWS SQS can be customized by providing a CDI bean implementation of +link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-aws-sqs/{javadoc-version}/io/smallrye/reactive/messaging/aws/sqs/SqsReceiveMessageRequestCustomizer.html[`SqsReceiveMessageRequestCustomizer`] +and configuring it's identifier using the `receive.request.customizer` connector attribute. + +[source,java] +---- +include::{examples-dir}/sqs/inbound/SqsReceiveMessageRequestCustomizerExample.java[indent=0] +---- + +[source,properties] +---- +mp.messaging.incoming.data.connector=smallrye-sqs +mp.messaging.incoming.data.queue=my-queue +mp.messaging.incoming.data.receive.request.customizer=my-customizer +---- + +Receive requests failed with retryable exceptions are retried automatically, by setting the failed request id. + +=== Receive message request pause and resume + +The AWS SQS connector fetches messages by continuously sending receive message requests. +If messages are not processed in a timely manner, the connector pauses fetching messages until queued messages are processed. + +The pause resume can be disabled using the `receive.request.pause.resume` connector attribute. + +[source,properties] +---- +mp.messaging.incoming.data.receive.request.pause.resume=false +---- + +== Deserialization + +The connector converts incoming SQS Messages into Reactive Messaging `Message++<++T++>++` instances. + +The payload type `T` is determined based on the value of the SQS message attribute `++_++classname`. + +If you send messages with the AWS SQS connector (outbound connector), +the `++_++classname` attribute is automatically added to the message. +The primitive types are transformed from the string representation to the corresponding Java type. +For objects, if one of the `JsonMapping` modules is present on the classpath, +the connector used that JSON module to deserialize the message body to objects. + +If the `++_++classname` attribute is not present, the payload is deserialized as a `String`. + +[source,java] +---- +include::{examples-dir}/sqs/json/SqsJsonMapping.java[tags=code,indent=0] +---- + +== Inbound Metadata + +Messages coming from SQS contain an instance of link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-aws-sqs/{javadoc-version}/io/smallrye/reactive/messaging/aws/sqs/SqsIncomingMetadata.html[`SqsIncomingMetadata`] +in the metadata. + +SQS message attributes can be accessed from the metadata either by name or by the `MessageAttributeValue` object. + +[source,java] +---- +include::{examples-dir}/sqs/inbound/SqsMetadataExample.java[indent=0] +---- + +== Acknowledgement Strategies + +The default strategy for acknowledging AWS SQS Message is to _delete_ the message from the queue. +You can set the `ack-strategy` attribute to `ignore` if you want to ignore the message. + +++[++NOTE++]++ Deprecated +`ack.delete` attribute is deprecated and will be removed in a future release. + +You can implement a custom strategy by implementing the link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-aws-sqs/{javadoc-version}/io/smallrye/reactive/messaging/aws/sqs/SqsAckHandler.html[`SqsAckHandler`], +interface with a `Factory` class and registering it as a CDI bean with an `@Identifier`. + +[source,java] +---- +include::{examples-dir}/sqs/inbound/SqsCustomAckStrategy.java[indent=0] +---- + +== Failure Strategies + +The default strategy for handling message processing failures is `ignore`. +It lets the visibility timeout of the message consumer to expire and reconsume the message. + +Other possible strategies are: + +* `fail`: the failure is logged and the channel fail-stops. +* `delete`: the message is removed from the queue. +* `visibility`: the message visibility timeout is reset to 0. + +You can implement a custom strategy by implementing the link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-aws-sqs/{javadoc-version}/io/smallrye/reactive/messaging/aws/sqs/SqsFailureHandler.html[`SqsFailureHandler`], +interface with a `Factory` class and registering it as a CDI bean with an `@Identifier`. + +[source,java] +---- +include::{examples-dir}/sqs/inbound/SqsCustomNackStrategy.java[indent=0] +---- + +== Configuration Reference + +include::{generated-dir}/smallrye-sqs-incoming.adoc[indent=0] diff --git a/content/docs/smallrye-reactive-messaging/4/sqs/sending-aws-sqs-messages.adoc b/content/docs/smallrye-reactive-messaging/4/sqs/sending-aws-sqs-messages.adoc new file mode 100644 index 000000000..94e92d1ce --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/sqs/sending-aws-sqs-messages.adoc @@ -0,0 +1,74 @@ += Sending AWS SQS Messages +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The AWS SQS connector allows you to send messages to an AWS SQS queue. + +== Sending messages + +Before you start, you need to have an AWS account and an SQS queue created. +To send messages to an SQS queue, you need to create a method that produces messages to the queue. + +Then, your application can send `Message++<++String++>++` to the prices channel. +It can use `String` payloads as in the following snippet: + +[source,java] +---- +include::{examples-dir}/sqs/outbound/SqsStringProducer.java[indent=0] +---- + +Or, you can send `Message++<++Double++>++`, which affords the opportunity to +explicitly specify metadata on the outgoing message: + +[source,java] +---- +include::{examples-dir}/sqs/outbound/SqsMessageStringProducer.java[indent=0] +---- + +== Sending messages in batch + +You can configure the outbound channel to send messages in batch of maximum 10 messages (AWS SQS limitation). + +You can customize the size of batches, `10` being the default batch size, and the delay to wait for new messages to be added to the batch, 3000ms being the default delay: + +[source,java] +---- +mp.messaging.outgoing.prices.connector=smallrye-sqs +mp.messaging.outgoing.prices.queue=prices +mp.messaging.outgoing.prices.batch=true +mp.messaging.outgoing.prices.batch-size=5 +mp.messaging.outgoing.prices.batch-delay=3000 +---- + +== Serialization + +When sending a `Message++<++T++>++`, the connector converts the message into a AWS SQS Message. +How the message is converted depends on the payload type: + +* If the payload is of type `SendMessageRequest` it is sent as is. +* If the payload is of type `SendMessageRequest.Builder`, the queue url is set and sent. +* If the payload is of type `software.amazon.awssdk.services.sqs.model.Message` it is usd to set the message body and attributes. +* If the payload is of primitive types the paylaod is converted to String and the message attribute `++_++classname` is set to the class name of the payload. +* If the payload is of any other object type, the payload is serialized (using the `JsonMapping` implementation discovered) and the message attribute `++_++classname` is set to the class name of the payload. + +If the message payload cannot be serialized to JSON, the message is _nacked_. + +== Outbound Metadata + +When sending `Messages`, you can add an instance of link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-aws-sqs/{javadoc-version}/io/smallrye/reactive/messaging/aws/sqs/SqsOutboundMetadata.html[`SqsOutboundMetadata`] +to influence how the message is handled by AWS SQS. For example, you +can configure the routing key, timestamp and headers: + +[source,java] +---- +include::{examples-dir}/sqs/outbound/SqsOutboundMetadataExample.java[tags=code,indent=0] +---- + +== Acknowledgement + +By default, the Reactive Messaging `Message` is acknowledged when the +send message request is successful. If the message is not sent successfully, the message is _nacked_. + +== Configuration Reference + +include::{generated-dir}/smallrye-sqs-outgoing.adoc[indent=0] diff --git a/content/docs/smallrye-reactive-messaging/4/sqs/sqs.adoc b/content/docs/smallrye-reactive-messaging/4/sqs/sqs.adoc new file mode 100644 index 000000000..83b664f6d --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/4/sqs/sqs.adoc @@ -0,0 +1,62 @@ += AWS SQS Connector +:version-root: ../ +include::{version-root}_attributes.adoc[] + +[WARNING] +.Preview +==== + +The AWS SQS Connector is currently in *preview*. + +==== + +The AWS SQS Connector adds support for AWS SQS to Reactive Messaging. + +With this connector, your application can: + +* receive messages from a RabbitMQ queue +* send messages to a RabbitMQ exchange + +The AWS SQS connector is based on the https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/home.html[AWS SDK for Java V2]. + +== Using the AWS SQS connector + +To use the AWS SQS Connector, add the following dependency to your project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.reactive + smallrye-reactive-messaging-aws-sqs + {project-version} + +---- + +The connector name is: `smallrye-sqs`. + +So, to indicate that a channel is managed by this connector you need: + +[source,properties] +---- +# Inbound +mp.messaging.incoming.[channel-name].connector=smallrye-sqs + +# Outbound +mp.messaging.outgoing.[channel-name].connector=smallrye-sqs +---- + +== Configuration + +When available the AWS SQS connector discovers the SQS client as a CDI bean. +This allows using the connector with the https://docs.quarkiverse.io/quarkus-amazon-services/dev/amazon-sqs.html[Quarkiverse AWS SQS extension]. +If the SQS client is not available as a CDI bean, the connector creates a new client using the provided configuration. + +* `queue` - The name of the SQS queue, defaults to channel name if not provided. +* `region` - The name of the SQS region. +* `endpoint-override` - The endpoint url override. +* `credentials-provider` - The fully qualified class name of the credential provider to be used in the client, if not provided the default provider chain is used. + +== Additional Resources + +* https://docs.aws.amazon.com/sqs/index.html[AWS SQS Documentation] +* https://docs.quarkiverse.io/quarkus-amazon-services/dev/amazon-sqs.html[Quarkiverse AWS SQS Extension] diff --git a/content/docs/smallrye-reactive-messaging/latest/_attributes.adoc b/content/docs/smallrye-reactive-messaging/latest/_attributes.adoc new file mode 100644 index 000000000..085815d5d --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_attributes.adoc @@ -0,0 +1,26 @@ +:project-version: 5.0.0-RC1 +// Attributes shared by every page. smallrye.io overrides the ones it needs when it publishes. +// Pages in subdirectories set :version-root: (../ per level) before including this file. +ifndef::version-root[:version-root:] +:idprefix: +:idseparator: - +:sectanchors: +:examples-dir: {version-root}_examples +:partialsdir: {version-root}_partials +:project-name: SmallRye Reactive Messaging +:javadoc-version: latest +:version: 5.0.0 +:generated-dir: {version-root}_generated + +// Added by smallrye.io (tools/docs/Docs.java sync) +:javadoc-version: 5.0.0-RC1 +:relfileprefix: /docs/smallrye-reactive-messaging/latest/ +:imagesdir: /docs/smallrye-reactive-messaging/latest +:docs-project: smallrye-reactive-messaging +:docs-version: latest +:docs-ref: 5.0.0-RC1 +:weld-version: 6.0.4.Final +:smallrye-config-version: 3.18.3 +:mutiny-version: 3.1.0 +:camel-version: 4.22.1 +:spec-version: 3.0.1 diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/ack/AutoNack.java b/content/docs/smallrye-reactive-messaging/latest/_examples/ack/AutoNack.java new file mode 100644 index 000000000..bdb5a371a --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/ack/AutoNack.java @@ -0,0 +1,55 @@ +package ack; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Uni; + +public class AutoNack { + + // tag::auto-nack[] + @Incoming("data") + @Outgoing("out") + public String process(String s) { + if (s.equalsIgnoreCase("b")) { + // Throwing an exception triggers a nack + throw new IllegalArgumentException("b"); + } + + if (s.equalsIgnoreCase("e")) { + // Returning null would skip the message (it will be acked) + return null; + } + + return s.toUpperCase(); + } + + @Incoming("data") + @Outgoing("out") + public Uni processAsync(String s) { + if (s.equalsIgnoreCase("a")) { + // Returning a failing Uni triggers a nack + return Uni.createFrom().failure(new Exception("a")); + } + + if (s.equalsIgnoreCase("b")) { + // Throwing an exception triggers a nack + throw new IllegalArgumentException("b"); + } + + if (s.equalsIgnoreCase("e")) { + // Returning null would skip the message (it will be acked not nacked) + return Uni.createFrom().nullItem(); + } + + if (s.equalsIgnoreCase("f")) { + // returning `null` is invalid for method returning Unis, the message is nacked + return null; + } + + return Uni.createFrom().item(s.toUpperCase()); + } + + // end::auto-nack[] + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/ack/MessageAckExamples.java b/content/docs/smallrye-reactive-messaging/latest/_examples/ack/MessageAckExamples.java new file mode 100644 index 000000000..5b4ddf459 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/ack/MessageAckExamples.java @@ -0,0 +1,52 @@ +package ack; + +import java.util.concurrent.CompletableFuture; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +public class MessageAckExamples { + + // tag::with-payload[] + @Incoming("in") + @Outgoing("out") + public Message process(Message in) { + // The acknowledgement is forwarded, when the consumer + // acknowledges the message, `in` will be acknowledged + return in.withPayload(in.getPayload() + 1); + } + // end::with-payload[] + + public void creation() { + // tag::message-creation[] + Message message = Message.of("hello", () -> { + // called when the consumer acknowledges the message + + // return a CompletionStage completed when the + // acknowledgment of the created message is + // completed. + // For immediate ack use: + return CompletableFuture.completedFuture(null); + + }); + // end::message-creation[] + } + + // tag::process[] + @Incoming("in") + @Outgoing("out") + public Message processAndProduceNewMessage(Message in) { + // The acknowledgement is forwarded, when the consumer + // acknowledges the message, `in` will be acknowledged + return Message.of(in.getPayload() + 1, + () -> { + // Called when the consumer acknowledges the message + // ... + // Don't forget to acknowledge the incoming message: + return in.ack(); + }); + } + // end::process[] + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/ack/NackOnEmitter.java b/content/docs/smallrye-reactive-messaging/latest/_examples/ack/NackOnEmitter.java new file mode 100644 index 000000000..ffdbc70bc --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/ack/NackOnEmitter.java @@ -0,0 +1,27 @@ +package ack; + +import java.util.concurrent.CompletionStage; + +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Emitter; + +public class NackOnEmitter { + + // tag::emitter[] + @Inject + @Channel("data") + Emitter emitter; + + public void emitPayload() { + CompletionStage completionStage = emitter.send("hello"); + completionStage.whenComplete((acked, nacked) -> { + if (nacked != null) { + // the processing has failed + } + }); + } + // end::emitter[] + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/ack/PreAckExamples.java b/content/docs/smallrye-reactive-messaging/latest/_examples/ack/PreAckExamples.java new file mode 100644 index 000000000..04b336bde --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/ack/PreAckExamples.java @@ -0,0 +1,20 @@ +package ack; + +import org.eclipse.microprofile.reactive.messaging.Acknowledgment; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +public class PreAckExamples { + + // tag::pre[] + @Incoming("in") + @Outgoing("out") + @Acknowledgment(Acknowledgment.Strategy.PRE_PROCESSING) + public String process(String input) { + // The message wrapping the payload is already acknowledged + // The default would have waited the produced message to be + // acknowledged + return input.toUpperCase(); + } + // end::pre[] +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/ack/StreamAckExamples.java b/content/docs/smallrye-reactive-messaging/latest/_examples/ack/StreamAckExamples.java new file mode 100644 index 000000000..ea47f34b4 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/ack/StreamAckExamples.java @@ -0,0 +1,42 @@ +package ack; + +import java.util.concurrent.Flow.Publisher; + +import org.eclipse.microprofile.reactive.messaging.Acknowledgment; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +public class StreamAckExamples { + + // tag::message[] + @Incoming("in") + @Outgoing("out") + public Publisher> transform(Multi> stream) { + return stream + .map(message -> message.withPayload(message.getPayload().toUpperCase())); + } + // end::message[] + + // tag::payload[] + @Incoming("in") + @Outgoing("out") + public Publisher transformPayload(Multi stream) { + return stream + // The incoming messages are already acknowledged + .map(String::toUpperCase); + } + // end::payload[] + + // tag::payload-to-multi[] + @Incoming("in") + @Outgoing("out") + // Defaults to pre-processing, but post-processing is also supported + @Acknowledgment(Acknowledgment.Strategy.POST_PROCESSING) + public Multi transformPayload(String one) { + return Multi.createFrom().items(one, one); + } + // end::payload-to-multi[] +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/amqp/customization/ClientProducers.java b/content/docs/smallrye-reactive-messaging/latest/_examples/amqp/customization/ClientProducers.java new file mode 100644 index 000000000..4dfa6c829 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/amqp/customization/ClientProducers.java @@ -0,0 +1,34 @@ +package amqp.customization; + +import jakarta.enterprise.inject.Produces; + +import io.smallrye.common.annotation.Identifier; +import io.vertx.amqp.AmqpClientOptions; +import io.vertx.core.net.PemKeyCertOptions; +import io.vertx.core.net.PemTrustOptions; + +public class ClientProducers { + + // tag::named[] + @Produces + @Identifier("my-options") + public AmqpClientOptions getNamedOptions() { + // You can use the produced options to configure the TLS connection + PemKeyCertOptions keycert = new PemKeyCertOptions() + .addCertPath("./tls/tls.crt") + .addKeyPath("./tls/tls.key"); + PemTrustOptions trust = new PemTrustOptions().addCertPath("./tlc/ca.crt"); + + return new AmqpClientOptions() + .setSsl(true) + .setKeyCertOptions(keycert) + .setTrustOptions(trust) + .addEnabledSaslMechanism("EXTERNAL") + .setHostnameVerificationAlgorithm("") // Disable hostname verification + .setConnectTimeout(30000) + .setReconnectInterval(5000) + .setContainerId("my-container"); + } + // end::named[] + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/amqp/customization/MyCbsTokenProvider.java b/content/docs/smallrye-reactive-messaging/latest/_examples/amqp/customization/MyCbsTokenProvider.java new file mode 100644 index 000000000..45346773f --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/amqp/customization/MyCbsTokenProvider.java @@ -0,0 +1,32 @@ +package amqp.customization; + +import java.time.OffsetDateTime; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.amqp.AmqpConnectorCommonConfiguration; +import io.smallrye.reactive.messaging.amqp.cbs.CbsToken; +import io.smallrye.reactive.messaging.amqp.cbs.CbsTokenProvider; +import io.smallrye.reactive.messaging.amqp.cbs.DefaultCbsToken; + +// tag::provider[] +@ApplicationScoped +public class MyCbsTokenProvider implements CbsTokenProvider { + + @Override + public Uni getToken(AmqpConnectorCommonConfiguration config) { + String token = fetchTokenFromAuthService(); + return Uni.createFrom().item(new DefaultCbsToken( + token, + "jwt", + OffsetDateTime.now().plusHours(1), + OffsetDateTime.now().plusMinutes(45))); + } + + private String fetchTokenFromAuthService() { + // Retrieve a token from your identity provider + return "my-jwt-token"; + } +} +// end::provider[] diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/amqp/customization/MySasTokenProvider.java b/content/docs/smallrye-reactive-messaging/latest/_examples/amqp/customization/MySasTokenProvider.java new file mode 100644 index 000000000..0440f43b5 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/amqp/customization/MySasTokenProvider.java @@ -0,0 +1,29 @@ +package amqp.customization; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.amqp.AmqpConnectorCommonConfiguration; +import io.smallrye.reactive.messaging.amqp.cbs.CbsToken; +import io.smallrye.reactive.messaging.amqp.cbs.CbsTokenProvider; +import io.smallrye.reactive.messaging.amqp.cbs.DefaultCbsToken; + +// tag::sas-provider[] +@ApplicationScoped +public class MySasTokenProvider implements CbsTokenProvider { + + @Override + public Uni getToken(AmqpConnectorCommonConfiguration config) { + String sasToken = generateSasToken(config.getHost()); + return Uni.createFrom().item(new DefaultCbsToken( + sasToken, + "servicebus.windows.net:sastoken", + 3600)); // valid for 1 hour + } + + private String generateSasToken(String host) { + // Build a SAS token for Azure Service Bus + return "SharedAccessSignature sr=..."; + } +} +// end::sas-provider[] diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/amqp/inbound/AmqpMetadataExample.java b/content/docs/smallrye-reactive-messaging/latest/_examples/amqp/inbound/AmqpMetadataExample.java new file mode 100644 index 000000000..8683ccbb7 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/amqp/inbound/AmqpMetadataExample.java @@ -0,0 +1,27 @@ +package amqp.inbound; + +import java.util.Optional; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.amqp.IncomingAmqpMetadata; +import io.vertx.core.json.JsonObject; + +public class AmqpMetadataExample { + + public void metadata() { + Message incoming = Message.of(12.0); + // tag::code[] + Optional metadata = incoming.getMetadata(IncomingAmqpMetadata.class); + metadata.ifPresent(meta -> { + String address = meta.getAddress(); + String subject = meta.getSubject(); + boolean durable = meta.isDurable(); + // Use io.vertx.core.json.JsonObject + JsonObject properties = meta.getProperties(); + // ... + }); + // end::code[] + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/amqp/inbound/AmqpPriceConsumer.java b/content/docs/smallrye-reactive-messaging/latest/_examples/amqp/inbound/AmqpPriceConsumer.java new file mode 100644 index 000000000..ac877fc2b --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/amqp/inbound/AmqpPriceConsumer.java @@ -0,0 +1,15 @@ +package amqp.inbound; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; + +@ApplicationScoped +public class AmqpPriceConsumer { + + @Incoming("prices") + public void consume(double price) { + // process your price. + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/amqp/inbound/AmqpPriceMessageConsumer.java b/content/docs/smallrye-reactive-messaging/latest/_examples/amqp/inbound/AmqpPriceMessageConsumer.java new file mode 100644 index 000000000..1611ebf7d --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/amqp/inbound/AmqpPriceMessageConsumer.java @@ -0,0 +1,21 @@ +package amqp.inbound; + +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; + +@ApplicationScoped +public class AmqpPriceMessageConsumer { + + @Incoming("prices") + public CompletionStage consume(Message price) { + // process your price. + + // Acknowledge the incoming message, marking the AMQP message as `accepted`. + return price.ack(); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/amqp/outbound/AmqpCloudEventMultipleProcessors.java b/content/docs/smallrye-reactive-messaging/latest/_examples/amqp/outbound/AmqpCloudEventMultipleProcessors.java new file mode 100644 index 000000000..f87f08a15 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/amqp/outbound/AmqpCloudEventMultipleProcessors.java @@ -0,0 +1,39 @@ +package amqp.outbound; + +import java.net.URI; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.reactive.messaging.ce.OutgoingCloudEventMetadata; + +@ApplicationScoped +public class AmqpCloudEventMultipleProcessors { + + @Incoming("source") + @Outgoing("processed") + public Message process(Message in) { + return in.addMetadata(OutgoingCloudEventMetadata.builder() + .withId("id-" + in.getPayload()) + .withType("greeting") + .build()); + } + + @SuppressWarnings("unchecked") + @Incoming("processed") + @Outgoing("cloud-events") + public Message process2(Message in) { + OutgoingCloudEventMetadata metadata = in + .getMetadata(OutgoingCloudEventMetadata.class) + .orElseGet(() -> OutgoingCloudEventMetadata.builder().build()); + + return in.addMetadata(OutgoingCloudEventMetadata.from(metadata) + .withSource(URI.create("source://me")) + .withSubject("test") + .build()); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/amqp/outbound/AmqpCloudEventProcessor.java b/content/docs/smallrye-reactive-messaging/latest/_examples/amqp/outbound/AmqpCloudEventProcessor.java new file mode 100644 index 000000000..3b7a981f8 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/amqp/outbound/AmqpCloudEventProcessor.java @@ -0,0 +1,25 @@ +package amqp.outbound; + +import java.net.URI; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.reactive.messaging.ce.OutgoingCloudEventMetadata; + +@ApplicationScoped +public class AmqpCloudEventProcessor { + + @Outgoing("cloud-events") + public Message toCloudEvents(Message in) { + return in.addMetadata(OutgoingCloudEventMetadata.builder() + .withId("id-" + in.getPayload()) + .withType("greetings") + .withSource(URI.create("http://example.com")) + .withSubject("greeting-message") + .build()); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/amqp/outbound/AmqpOutboundDynamicAddressExample.java b/content/docs/smallrye-reactive-messaging/latest/_examples/amqp/outbound/AmqpOutboundDynamicAddressExample.java new file mode 100644 index 000000000..35f848796 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/amqp/outbound/AmqpOutboundDynamicAddressExample.java @@ -0,0 +1,26 @@ +package amqp.outbound; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.amqp.OutgoingAmqpMetadata; + +public class AmqpOutboundDynamicAddressExample { + public Message metadata(Message incoming) { + + // tag::code[] + String addressName = selectAddressFromIncommingMessage(incoming); + OutgoingAmqpMetadata metadata = OutgoingAmqpMetadata.builder() + .withAddress(addressName) + .withDurable(true) + .build(); + + // Create a new message from the `incoming` message + // Add `metadata` to the metadata from the `incoming` message. + return incoming.addMetadata(metadata); + // end::code[] + } + + private String selectAddressFromIncommingMessage(Message incoming) { + return "fake"; + } +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/amqp/outbound/AmqpOutboundMetadataExample.java b/content/docs/smallrye-reactive-messaging/latest/_examples/amqp/outbound/AmqpOutboundMetadataExample.java new file mode 100644 index 000000000..3eef8b590 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/amqp/outbound/AmqpOutboundMetadataExample.java @@ -0,0 +1,23 @@ +package amqp.outbound; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.amqp.OutgoingAmqpMetadata; + +public class AmqpOutboundMetadataExample { + + public Message metadata(Message incoming) { + + // tag::code[] + OutgoingAmqpMetadata metadata = OutgoingAmqpMetadata.builder() + .withDurable(true) + .withSubject("my-subject") + .build(); + + // Create a new message from the `incoming` message + // Add `metadata` to the metadata from the `incoming` message. + return incoming.addMetadata(metadata); + // end::code[] + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/amqp/outbound/AmqpPriceMessageProducer.java b/content/docs/smallrye-reactive-messaging/latest/_examples/amqp/outbound/AmqpPriceMessageProducer.java new file mode 100644 index 000000000..418a901b3 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/amqp/outbound/AmqpPriceMessageProducer.java @@ -0,0 +1,26 @@ +package amqp.outbound; + +import java.time.Duration; +import java.util.Random; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +@ApplicationScoped +public class AmqpPriceMessageProducer { + + private Random random = new Random(); + + @Outgoing("prices") + public Multi> generate() { + // Build an infinite stream of random prices + // It emits a price every second + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .map(x -> Message.of(random.nextDouble())); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/amqp/outbound/AmqpPriceProducer.java b/content/docs/smallrye-reactive-messaging/latest/_examples/amqp/outbound/AmqpPriceProducer.java new file mode 100644 index 000000000..0e760b71f --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/amqp/outbound/AmqpPriceProducer.java @@ -0,0 +1,25 @@ +package amqp.outbound; + +import java.time.Duration; +import java.util.Random; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +@ApplicationScoped +public class AmqpPriceProducer { + + private Random random = new Random(); + + @Outgoing("prices") + public Multi generate() { + // Build an infinite stream of random prices + // It emits a price every second + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .map(x -> random.nextDouble()); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/amqp/outbound/AmqpReplier.java b/content/docs/smallrye-reactive-messaging/latest/_examples/amqp/outbound/AmqpReplier.java new file mode 100644 index 000000000..7f5236aba --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/amqp/outbound/AmqpReplier.java @@ -0,0 +1,20 @@ +package amqp.outbound; + +import java.util.Random; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +@ApplicationScoped +public class AmqpReplier { + + Random rand = new Random(); + + @Incoming("request") + @Outgoing("reply") + String handleRequest(String request) { + return request + "-" + rand.nextInt(100); + } +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/amqp/outbound/AmqpReplierWithMetadata.java b/content/docs/smallrye-reactive-messaging/latest/_examples/amqp/outbound/AmqpReplierWithMetadata.java new file mode 100644 index 000000000..c798cd136 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/amqp/outbound/AmqpReplierWithMetadata.java @@ -0,0 +1,31 @@ +package amqp.outbound; + +import java.util.Random; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.reactive.messaging.amqp.IncomingAmqpMetadata; +import io.smallrye.reactive.messaging.amqp.OutgoingAmqpMetadata; + +@ApplicationScoped +public class AmqpReplierWithMetadata { + + Random rand = new Random(); + + @Incoming("request") + @Outgoing("reply") + Message handleRequest(Message request) { + IncomingAmqpMetadata incoming = request.getMetadata(IncomingAmqpMetadata.class) + .orElseThrow(); + OutgoingAmqpMetadata outMeta = OutgoingAmqpMetadata.builder() + .withAddress(incoming.getReplyTo()) + .withCorrelationId(incoming.getId()) + .build(); + String reply = request.getPayload() + "-" + rand.nextInt(100); + return request.withPayload(reply).addMetadata(outMeta); + } +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/amqp/outbound/AmqpRequestReplyEmitter.java b/content/docs/smallrye-reactive-messaging/latest/_examples/amqp/outbound/AmqpRequestReplyEmitter.java new file mode 100644 index 000000000..f1d5285c8 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/amqp/outbound/AmqpRequestReplyEmitter.java @@ -0,0 +1,21 @@ +package amqp.outbound; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; + +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.amqp.reply.AmqpRequestReply; + +@ApplicationScoped +public class AmqpRequestReplyEmitter { + + @Inject + @Channel("my-request") + AmqpRequestReply quoteRequest; + + public Uni requestQuote(String request) { + return quoteRequest.request(request); + } +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/amqp/outbound/AmqpRequestReplyMessageEmitter.java b/content/docs/smallrye-reactive-messaging/latest/_examples/amqp/outbound/AmqpRequestReplyMessageEmitter.java new file mode 100644 index 000000000..de5f1bbaf --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/amqp/outbound/AmqpRequestReplyMessageEmitter.java @@ -0,0 +1,22 @@ +package amqp.outbound; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.amqp.reply.AmqpRequestReply; + +@ApplicationScoped +public class AmqpRequestReplyMessageEmitter { + + @Inject + @Channel("my-request") + AmqpRequestReply quoteRequest; + + public Uni> requestMessage(String request) { + return quoteRequest.request(Message.of(request)); + } +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/amqp/outbound/AmqpRequestReplyMultiEmitter.java b/content/docs/smallrye-reactive-messaging/latest/_examples/amqp/outbound/AmqpRequestReplyMultiEmitter.java new file mode 100644 index 000000000..25d776606 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/amqp/outbound/AmqpRequestReplyMultiEmitter.java @@ -0,0 +1,21 @@ +package amqp.outbound; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; + +import io.smallrye.mutiny.Multi; +import io.smallrye.reactive.messaging.amqp.reply.AmqpRequestReply; + +@ApplicationScoped +public class AmqpRequestReplyMultiEmitter { + + @Inject + @Channel("my-request") + AmqpRequestReply quoteRequest; + + public Multi requestQuote(String request) { + return quoteRequest.requestMulti(request).select().first(5); + } +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/amqp/outbound/MyAmqpReplyFailureHandler.java b/content/docs/smallrye-reactive-messaging/latest/_examples/amqp/outbound/MyAmqpReplyFailureHandler.java new file mode 100644 index 000000000..77b1d2d14 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/amqp/outbound/MyAmqpReplyFailureHandler.java @@ -0,0 +1,24 @@ +package amqp.outbound; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.smallrye.common.annotation.Identifier; +import io.smallrye.reactive.messaging.amqp.AmqpMessage; +import io.smallrye.reactive.messaging.amqp.reply.ReplyFailureHandler; + +@ApplicationScoped +@Identifier("my-reply-error") +public class MyAmqpReplyFailureHandler implements ReplyFailureHandler { + + @Override + public Throwable handleReply(AmqpMessage replyMessage) { + var properties = replyMessage.getApplicationProperties(); + if (properties != null) { + String error = properties.getString("REPLY_ERROR"); + if (error != null) { + return new IllegalArgumentException(error); + } + } + return null; + } +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/beans/Chain.java b/content/docs/smallrye-reactive-messaging/latest/_examples/beans/Chain.java new file mode 100644 index 000000000..8a44b6d55 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/beans/Chain.java @@ -0,0 +1,27 @@ +package beans; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +public class Chain { + + // tag::chain[] + @Outgoing("source") + public Multi generate() { + return Multi.createFrom().items("Hello", "from", "reactive", "messaging"); + } + + @Incoming("source") + @Outgoing("sink") + public String process(String in) { + return in.toUpperCase(); + } + + @Incoming("sink") + public void consume(String processed) { + System.out.println(processed); + } + // end::chain[] +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/beans/MessageProcessingBean.java b/content/docs/smallrye-reactive-messaging/latest/_examples/beans/MessageProcessingBean.java new file mode 100644 index 000000000..edfbf0df4 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/beans/MessageProcessingBean.java @@ -0,0 +1,20 @@ +package beans; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +@ApplicationScoped +public class MessageProcessingBean { + + @Incoming("consumed-channel") + @Outgoing("populated-channel") + public Message process(Message in) { + // Process the payload + String payload = in.getPayload().toUpperCase(); + // Create a new message from `in` and just update the payload + return in.withPayload(payload); + } +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/beans/PayloadProcessingBean.java b/content/docs/smallrye-reactive-messaging/latest/_examples/beans/PayloadProcessingBean.java new file mode 100644 index 000000000..b7fa6b0ae --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/beans/PayloadProcessingBean.java @@ -0,0 +1,16 @@ +package beans; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +@ApplicationScoped +public class PayloadProcessingBean { + + @Incoming("consumed-channel") + @Outgoing("populated-channel") + public String process(String in) { + return in.toUpperCase(); + } +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/broadcast/BroadcastExamples.java b/content/docs/smallrye-reactive-messaging/latest/_examples/broadcast/BroadcastExamples.java new file mode 100644 index 000000000..53a8bb160 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/broadcast/BroadcastExamples.java @@ -0,0 +1,29 @@ +package broadcast; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.reactive.messaging.annotations.Broadcast; + +public class BroadcastExamples { + + // tag::chain[] + @Incoming("in") + @Outgoing("out") + @Broadcast + public int increment(int i) { + return i + 1; + } + + @Incoming("out") + public void consume1(int i) { + //... + } + + @Incoming("out") + public void consume2(int i) { + //... + } + // end::chain[] + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/broadcast/BroadcastWithCountExamples.java b/content/docs/smallrye-reactive-messaging/latest/_examples/broadcast/BroadcastWithCountExamples.java new file mode 100644 index 000000000..e227d7747 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/broadcast/BroadcastWithCountExamples.java @@ -0,0 +1,29 @@ +package broadcast; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.reactive.messaging.annotations.Broadcast; + +public class BroadcastWithCountExamples { + + // tag::chain[] + @Incoming("in") + @Outgoing("out") + @Broadcast(2) + public int increment(int i) { + return i + 1; + } + + @Incoming("out") + public void consume1(int i) { + //... + } + + @Incoming("out") + public void consume2(int i) { + //... + } + // end::chain[] + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/camel/api/CamelApi.java b/content/docs/smallrye-reactive-messaging/latest/_examples/camel/api/CamelApi.java new file mode 100644 index 000000000..f96087599 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/camel/api/CamelApi.java @@ -0,0 +1,94 @@ +package camel.api; + +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +import org.apache.camel.BindToRegistry; +import org.apache.camel.CamelContext; +import org.apache.camel.Exchange; +import org.apache.camel.builder.LambdaRouteBuilder; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.component.reactive.streams.api.CamelReactiveStreamsService; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; + +public class CamelApi { + + // tag::reactive[] + @Inject + CamelReactiveStreamsService reactiveStreamsService; + // end::reactive[] + + // tag::source[] + @Outgoing("camel") + public Publisher retrieveDataFromCamelRoute() { + return reactiveStreamsService.from("seda:camel"); + } + // end::source[] + + // tag::source-route-builder[] + @Singleton + static class MyRouteBuilder extends RouteBuilder { + @Inject + CamelReactiveStreamsService reactiveStreamsService; + + @Outgoing("sink") + public Publisher getDataFromCamelRoute() { + return reactiveStreamsService.fromStream("my-stream", String.class); + } + + @Override + public void configure() { + from("seda:camel").process( + exchange -> exchange.getMessage().setBody(exchange.getIn().getBody(String.class).toUpperCase())) + .to("reactive-streams:my-stream"); + } + } + // end::source-route-builder[] + + // tag::source-lambda-route-builder[] + @ApplicationScoped + static class MyLambdaRouteBuilder { + @Inject + CamelReactiveStreamsService reactiveStreamsService; + + @Outgoing("sink") + public Publisher getDataFromCamelRoute() { + return reactiveStreamsService.fromStream("my-stream", String.class); + } + + @Produces + @BindToRegistry + public LambdaRouteBuilder route() { + return rb -> rb.from("seda:camel").process( + exchange -> exchange.getMessage().setBody(exchange.getIn().getBody(String.class).toUpperCase())) + .to("reactive-streams:my-stream"); + } + } + // end::source-lambda-route-builder[] + + // tag::sink[] + @Incoming("to-camel") + public Subscriber sendDataToCamelRoute() { + return reactiveStreamsService.subscriber("file:./target?fileName=values.txt&fileExist=append", + String.class); + } + // end::sink[] + + // tag::producer[] + @Inject + CamelContext camel; + + @Incoming("to-camel") + public CompletionStage sink(String value) { + return camel.createProducerTemplate() + .asyncSendBody("file:./target?fileName=values.txt&fileExist=append", value).thenApply(x -> null); + } + // end::producer[] +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/camel/inbound/CamelFileConsumer.java b/content/docs/smallrye-reactive-messaging/latest/_examples/camel/inbound/CamelFileConsumer.java new file mode 100644 index 000000000..21c206129 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/camel/inbound/CamelFileConsumer.java @@ -0,0 +1,20 @@ +package camel.inbound; + +import java.io.File; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.apache.camel.component.file.GenericFile; +import org.eclipse.microprofile.reactive.messaging.Incoming; + +@ApplicationScoped +public class CamelFileConsumer { + + @Incoming("files") + public void consume(GenericFile gf) { + File file = gf.getFile(); + // process the file + + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/camel/inbound/CamelFileMessageConsumer.java b/content/docs/smallrye-reactive-messaging/latest/_examples/camel/inbound/CamelFileMessageConsumer.java new file mode 100644 index 000000000..56cbc8559 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/camel/inbound/CamelFileMessageConsumer.java @@ -0,0 +1,23 @@ +package camel.inbound; + +import java.io.File; +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.apache.camel.component.file.GenericFile; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; + +@ApplicationScoped +public class CamelFileMessageConsumer { + + @Incoming("files") + public CompletionStage consume(Message> msg) { + File file = msg.getPayload().getFile(); + // process the file + + return msg.ack(); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/camel/inbound/IncomingCamelMetadataExample.java b/content/docs/smallrye-reactive-messaging/latest/_examples/camel/inbound/IncomingCamelMetadataExample.java new file mode 100644 index 000000000..63f0a4f3d --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/camel/inbound/IncomingCamelMetadataExample.java @@ -0,0 +1,29 @@ +package camel.inbound; + +import java.io.File; +import java.util.Optional; +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.apache.camel.Exchange; +import org.apache.camel.component.file.GenericFile; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.camel.IncomingExchangeMetadata; + +@ApplicationScoped +public class IncomingCamelMetadataExample { + + @Incoming("files") + public CompletionStage consume(Message> msg) { + Optional metadata = msg.getMetadata(IncomingExchangeMetadata.class); + if (metadata.isPresent()) { + // Retrieve the camel exchange: + Exchange exchange = metadata.get().getExchange(); + } + return msg.ack(); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/camel/outbound/CamelOutboundMetadataExample.java b/content/docs/smallrye-reactive-messaging/latest/_examples/camel/outbound/CamelOutboundMetadataExample.java new file mode 100644 index 000000000..3261e7371 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/camel/outbound/CamelOutboundMetadataExample.java @@ -0,0 +1,31 @@ +package camel.outbound; + +import java.time.Duration; +import java.util.Random; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; +import io.smallrye.reactive.messaging.camel.OutgoingExchangeMetadata; + +@ApplicationScoped +public class CamelOutboundMetadataExample { + + private Random random = new Random(); + + @Outgoing("prices") + public Multi> generate() { + // tag::code[] + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .map(x -> random.nextDouble()) + .map(p -> Double.toString(p)) + .map(s -> Message.of(s) + .addMetadata(new OutgoingExchangeMetadata() + .putProperty("my-property", "my-value"))); + // end::code[] + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/camel/outbound/CamelPriceMessageProducer.java b/content/docs/smallrye-reactive-messaging/latest/_examples/camel/outbound/CamelPriceMessageProducer.java new file mode 100644 index 000000000..5a0202608 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/camel/outbound/CamelPriceMessageProducer.java @@ -0,0 +1,27 @@ +package camel.outbound; + +import java.time.Duration; +import java.util.Random; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +@ApplicationScoped +public class CamelPriceMessageProducer { + + private Random random = new Random(); + + @Outgoing("prices") + public Multi> generate() { + // Build an infinite stream of random prices + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .map(x -> random.nextDouble()) + .map(p -> Double.toString(p)) + .map(Message::of); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/camel/outbound/CamelPriceProducer.java b/content/docs/smallrye-reactive-messaging/latest/_examples/camel/outbound/CamelPriceProducer.java new file mode 100644 index 000000000..30392e08e --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/camel/outbound/CamelPriceProducer.java @@ -0,0 +1,26 @@ +package camel.outbound; + +import java.time.Duration; +import java.util.Random; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +@ApplicationScoped +public class CamelPriceProducer { + + private Random random = new Random(); + + @Outgoing("prices") + public Multi generate() { + // Build an infinite stream of random prices + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .onOverflow().drop() + .map(x -> random.nextDouble()) + .map(p -> Double.toString(p)); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/camel/processor/CamelProcessor.java b/content/docs/smallrye-reactive-messaging/latest/_examples/camel/processor/CamelProcessor.java new file mode 100644 index 000000000..434b12911 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/camel/processor/CamelProcessor.java @@ -0,0 +1,18 @@ +package camel.processor; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +@ApplicationScoped +public class CamelProcessor { + + @Incoming("mynatssubject") + @Outgoing("mykafkatopic") + public byte[] process(byte[] message) { + // do some logic + return message; + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/connectors/MyAckHandler.java b/content/docs/smallrye-reactive-messaging/latest/_examples/connectors/MyAckHandler.java new file mode 100644 index 000000000..dc1d1d146 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/connectors/MyAckHandler.java @@ -0,0 +1,25 @@ +package connectors; + +import java.util.concurrent.CompletionStage; + +import connectors.api.BrokerClient; +import io.smallrye.mutiny.Uni; + +public class MyAckHandler { + + private final BrokerClient client; + + static MyAckHandler create(BrokerClient client) { + return new MyAckHandler(client); + } + + public MyAckHandler(BrokerClient client) { + this.client = client; + } + + public CompletionStage handle(MyMessage msg) { + return Uni.createFrom().completionStage(client.ack(msg.getConsumedMessage())) + .emitOn(msg::runOnMessageContext) + .subscribeAsCompletionStage(); + } +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/connectors/MyConnector.java b/content/docs/smallrye-reactive-messaging/latest/_examples/connectors/MyConnector.java new file mode 100644 index 000000000..9dd75beb8 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/connectors/MyConnector.java @@ -0,0 +1,75 @@ +package connectors; + +import static io.smallrye.reactive.messaging.annotations.ConnectorAttribute.Direction.INCOMING; +import static io.smallrye.reactive.messaging.annotations.ConnectorAttribute.Direction.INCOMING_AND_OUTGOING; +import static io.smallrye.reactive.messaging.annotations.ConnectorAttribute.Direction.OUTGOING; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Flow; + +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.spi.Connector; + +import connectors.api.BrokerClient; +import io.smallrye.reactive.messaging.annotations.ConnectorAttribute; +import io.smallrye.reactive.messaging.connector.InboundConnector; +import io.smallrye.reactive.messaging.connector.OutboundConnector; +import io.smallrye.reactive.messaging.providers.connectors.ExecutionHolder; +import io.vertx.mutiny.core.Vertx; + +@ApplicationScoped +@Connector(MyConnector.CONNECTOR_NAME) +@ConnectorAttribute(name = "client-id", type = "string", direction = INCOMING_AND_OUTGOING, description = "The client id ", mandatory = true) +@ConnectorAttribute(name = "buffer-size", type = "int", direction = INCOMING, description = "The size buffer of incoming messages waiting to be processed", defaultValue = "128") +@ConnectorAttribute(name = "topic", type = "string", direction = OUTGOING, description = "The default topic to send the messages, defaults to channel name if not set") +@ConnectorAttribute(name = "maxPendingMessages", type = "int", direction = OUTGOING, description = "The maximum size of a queue holding pending messages", defaultValue = "1000") +@ConnectorAttribute(name = "waitForWriteCompletion", type = "boolean", direction = OUTGOING, description = "Whether the outgoing channel waits for the write completion", defaultValue = "true") +public class MyConnector implements InboundConnector, OutboundConnector { + + public static final String CONNECTOR_NAME = "smallrye-my-connector"; + + @Inject + ExecutionHolder executionHolder; + + Vertx vertx; + + List incomingChannels = new CopyOnWriteArrayList<>(); + List outgoingChannels = new CopyOnWriteArrayList<>(); + + @PostConstruct + void init() { + this.vertx = executionHolder.vertx(); + } + + @Override + public Flow.Publisher> getPublisher(Config config) { + MyConnectorIncomingConfiguration ic = new MyConnectorIncomingConfiguration(config); + String channelName = ic.getChannel(); + String clientId = ic.getClientId(); + int bufferSize = ic.getBufferSize(); + // ... + BrokerClient client = BrokerClient.create(clientId); + MyIncomingChannel channel = new MyIncomingChannel(vertx, ic, client); + incomingChannels.add(channel); + return channel.getStream(); + } + + @Override + public Flow.Subscriber> getSubscriber(Config config) { + MyConnectorOutgoingConfiguration oc = new MyConnectorOutgoingConfiguration(config); + String channelName = oc.getChannel(); + String clientId = oc.getClientId(); + int pendingMessages = oc.getMaxPendingMessages(); + // ... + BrokerClient client = BrokerClient.create(clientId); + MyOutgoingChannel channel = new MyOutgoingChannel(vertx, oc, client); + outgoingChannels.add(channel); + return channel.getSubscriber(); + } +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/connectors/MyConnectorWithPartials.java b/content/docs/smallrye-reactive-messaging/latest/_examples/connectors/MyConnectorWithPartials.java new file mode 100644 index 000000000..55d0cc2fa --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/connectors/MyConnectorWithPartials.java @@ -0,0 +1,106 @@ +package connectors; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Flow; + +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.spi.Connector; + +import connectors.api.BrokerClient; +import io.smallrye.reactive.messaging.connector.InboundConnector; +import io.smallrye.reactive.messaging.connector.OutboundConnector; +import io.smallrye.reactive.messaging.health.HealthReport; +import io.smallrye.reactive.messaging.health.HealthReporter; +import io.smallrye.reactive.messaging.providers.connectors.ExecutionHolder; +import io.vertx.mutiny.core.Vertx; + +// tag::health-report[] +@ApplicationScoped +@Connector(MyConnectorWithPartials.CONNECTOR_NAME) +public class MyConnectorWithPartials implements InboundConnector, OutboundConnector, HealthReporter { + + public static final String CONNECTOR_NAME = "smallrye-my-connector"; + + List incomingChannels = new CopyOnWriteArrayList<>(); + List outgoingChannels = new CopyOnWriteArrayList<>(); + + @Override + public HealthReport getReadiness() { + HealthReport.HealthReportBuilder builder = HealthReport.builder(); + for (MyIncomingChannel channel : incomingChannels) { + builder.add(channel.getChannel(), true); + } + for (MyOutgoingChannel channel : outgoingChannels) { + builder.add(channel.getChannel(), true); + } + return builder.build(); + } + + @Override + public HealthReport getLiveness() { + HealthReport.HealthReportBuilder builder = HealthReport.builder(); + for (MyIncomingChannel channel : incomingChannels) { + builder.add(channel.getChannel(), true); + } + for (MyOutgoingChannel channel : outgoingChannels) { + builder.add(channel.getChannel(), true); + } + return builder.build(); + } + + @Override + public HealthReport getStartup() { + HealthReport.HealthReportBuilder builder = HealthReport.builder(); + for (MyIncomingChannel channel : incomingChannels) { + builder.add(channel.getChannel(), true); + } + for (MyOutgoingChannel channel : outgoingChannels) { + builder.add(channel.getChannel(), true); + } + return builder.build(); + } + + // end::health-report[] + + @Inject + ExecutionHolder executionHolder; + + Vertx vertx; + + @PostConstruct + void init() { + this.vertx = executionHolder.vertx(); + } + + @Override + public Flow.Publisher> getPublisher(Config config) { + MyConnectorIncomingConfiguration ic = new MyConnectorIncomingConfiguration(config); + String channelName = ic.getChannel(); + String clientId = ic.getClientId(); + int bufferSize = ic.getBufferSize(); + // ... + BrokerClient client = BrokerClient.create(clientId); + MyIncomingChannel channel = new MyIncomingChannel(vertx, ic, client); + incomingChannels.add(channel); + return channel.getStream(); + } + + @Override + public Flow.Subscriber> getSubscriber(Config config) { + MyConnectorOutgoingConfiguration oc = new MyConnectorOutgoingConfiguration(config); + String channelName = oc.getChannel(); + String clientId = oc.getClientId(); + int pendingMessages = oc.getMaxPendingMessages(); + // ... + BrokerClient client = BrokerClient.create(clientId); + MyOutgoingChannel channel = new MyOutgoingChannel(vertx, oc, client); + outgoingChannels.add(channel); + return channel.getSubscriber(); + } +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/connectors/MyFailureHandler.java b/content/docs/smallrye-reactive-messaging/latest/_examples/connectors/MyFailureHandler.java new file mode 100644 index 000000000..f41b78055 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/connectors/MyFailureHandler.java @@ -0,0 +1,27 @@ +package connectors; + +import java.util.concurrent.CompletionStage; + +import org.eclipse.microprofile.reactive.messaging.Metadata; + +import connectors.api.BrokerClient; +import io.smallrye.mutiny.Uni; + +public class MyFailureHandler { + + private final BrokerClient client; + + static MyFailureHandler create(BrokerClient client) { + return new MyFailureHandler(client); + } + + public MyFailureHandler(BrokerClient client) { + this.client = client; + } + + public CompletionStage handle(MyMessage msg, Throwable reason, Metadata metadata) { + return Uni.createFrom().completionStage(() -> client.reject(msg.getConsumedMessage(), reason.getMessage())) + .emitOn(msg::runOnMessageContext) + .subscribeAsCompletionStage(); + } +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/connectors/MyIncomingChannel.java b/content/docs/smallrye-reactive-messaging/latest/_examples/connectors/MyIncomingChannel.java new file mode 100644 index 000000000..a7db74ccc --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/connectors/MyIncomingChannel.java @@ -0,0 +1,57 @@ +package connectors; + +import java.util.concurrent.Flow; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import connectors.api.BrokerClient; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.health.HealthReport; +import io.vertx.core.internal.VertxInternal; +import io.vertx.mutiny.core.Context; +import io.vertx.mutiny.core.Vertx; + +public class MyIncomingChannel { + + private final String channel; + private final BrokerClient client; + private final Context context; + private final MyAckHandler ackHandler; + private final MyFailureHandler failureHandler; + private final AtomicBoolean closed = new AtomicBoolean(false); + private Flow.Publisher> stream; + + public MyIncomingChannel(Vertx vertx, MyConnectorIncomingConfiguration cfg, BrokerClient client) { + // create and configure the client with MyConnectorIncomingConfiguration + this.channel = cfg.getChannel(); + this.client = client; + this.context = Context.newInstance(((VertxInternal) vertx.getDelegate()).createEventLoopContext()); + this.ackHandler = MyAckHandler.create(this.client); + this.failureHandler = MyFailureHandler.create(this.client); + this.stream = Multi.createBy().repeating() + .uni(() -> Uni.createFrom().completionStage(this.client.poll())) + .until(__ -> closed.get()) + .emitOn(context::runOnContext) + .map(consumed -> new MyMessage<>(consumed, ackHandler, failureHandler)); + } + + public String getChannel() { + return channel; + } + + public Flow.Publisher> getStream() { + return this.stream; + } + + public void close() { + closed.compareAndSet(false, true); + client.close(); + } + + void isReady(HealthReport.HealthReportBuilder healthReportBuilder) { + + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/connectors/MyIncomingChannelWithPartials.java b/content/docs/smallrye-reactive-messaging/latest/_examples/connectors/MyIncomingChannelWithPartials.java new file mode 100644 index 000000000..fb8417101 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/connectors/MyIncomingChannelWithPartials.java @@ -0,0 +1,79 @@ +package connectors; + +import java.util.concurrent.Flow; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import connectors.api.BrokerClient; +import connectors.api.ConsumedMessage; +import connectors.tracing.MyOpenTelemetryInstrumenter; +import connectors.tracing.MyTrace; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.health.HealthReport; +import io.smallrye.reactive.messaging.tracing.TracingUtils; +import io.vertx.core.internal.VertxInternal; +import io.vertx.mutiny.core.Context; +import io.vertx.mutiny.core.Vertx; + +public class MyIncomingChannelWithPartials { + + private final String channel; + private final BrokerClient client; + private final Context context; + private final MyAckHandler ackHandler; + private final MyFailureHandler failureHandler; + private final AtomicBoolean closed = new AtomicBoolean(false); + private final boolean tracingEnabled; + private Flow.Publisher> stream; + + public MyIncomingChannelWithPartials(Vertx vertx, MyConnectorIncomingConfiguration cfg, BrokerClient client) { + // create and configure the client with MyConnectorIncomingConfiguration + this.channel = cfg.getChannel(); + this.client = client; + this.context = Context.newInstance(((VertxInternal) vertx.getDelegate()).createEventLoopContext()); + this.ackHandler = MyAckHandler.create(this.client); + this.failureHandler = MyFailureHandler.create(this.client); + this.tracingEnabled = true; + // tag::incoming-tracing[] + Multi> receiveMulti = Multi.createBy().repeating() + .uni(() -> Uni.createFrom().completionStage(this.client.poll())) + .until(__ -> closed.get()) + .emitOn(context::runOnContext) + .map(consumed -> new MyMessage<>(consumed, ackHandler, failureHandler)); + + Instrumenter instrumenter = MyOpenTelemetryInstrumenter.createInstrumenter(true); + if (tracingEnabled) { + receiveMulti = receiveMulti.map(message -> { + ConsumedMessage consumedMessage = message.getMetadata(MyIncomingMetadata.class).get().getCustomMessage(); + return TracingUtils.traceIncoming(instrumenter, message, new MyTrace.Builder() + .withClientId(consumedMessage.clientId()) + .withTopic(consumedMessage.topic()) + .withProperties(consumedMessage.properties()) + .build()); + }); + } + // end::incoming-tracing[] + this.stream = receiveMulti; + } + + public String getChannel() { + return channel; + } + + public Flow.Publisher> getStream() { + return this.stream; + } + + public void close() { + closed.compareAndSet(false, true); + client.close(); + } + + void isReady(HealthReport.HealthReportBuilder healthReportBuilder) { + + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/connectors/MyIncomingMetadata.java b/content/docs/smallrye-reactive-messaging/latest/_examples/connectors/MyIncomingMetadata.java new file mode 100644 index 000000000..c288085be --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/connectors/MyIncomingMetadata.java @@ -0,0 +1,34 @@ +package connectors; + +import java.util.Map; + +import connectors.api.ConsumedMessage; + +public class MyIncomingMetadata { + + private final ConsumedMessage msg; + + public MyIncomingMetadata(ConsumedMessage msg) { + this.msg = msg; + } + + public ConsumedMessage getCustomMessage() { + return msg; + } + + public T getBody() { + return msg.body(); + } + + public String getKey() { + return msg.key(); + } + + public long getTimestamp() { + return msg.timestamp(); + } + + public Map getProperties() { + return msg.properties(); + } +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/connectors/MyMessage.java b/content/docs/smallrye-reactive-messaging/latest/_examples/connectors/MyMessage.java new file mode 100644 index 000000000..cd432f0f3 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/connectors/MyMessage.java @@ -0,0 +1,51 @@ +package connectors; + +import java.util.concurrent.CompletionStage; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Metadata; + +import connectors.api.ConsumedMessage; +import io.smallrye.reactive.messaging.providers.locals.ContextAwareMessage; + +public class MyMessage implements Message, ContextAwareMessage { + + private final T payload; + private final Metadata metadata; + private final MyAckHandler ackHandler; + + private final MyFailureHandler nackHandler; + private ConsumedMessage consumed; + + public MyMessage(ConsumedMessage message, MyAckHandler ackHandler, MyFailureHandler nackHandler) { + this.consumed = message; + this.payload = message.body(); + this.ackHandler = ackHandler; + this.nackHandler = nackHandler; + this.metadata = ContextAwareMessage.captureContextMetadata(new MyIncomingMetadata<>(message)); + } + + public ConsumedMessage getConsumedMessage() { + return consumed; + } + + @Override + public T getPayload() { + return payload; + } + + @Override + public Metadata getMetadata() { + return metadata; + } + + @Override + public CompletionStage ack() { + return ackHandler.handle(this); + } + + @Override + public CompletionStage nack(Throwable reason, Metadata nackMetadata) { + return nackHandler.handle(this, reason, nackMetadata); + } +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/connectors/MyMessageConverter.java b/content/docs/smallrye-reactive-messaging/latest/_examples/connectors/MyMessageConverter.java new file mode 100644 index 000000000..6f9f180e9 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/connectors/MyMessageConverter.java @@ -0,0 +1,27 @@ +package connectors; + +import java.lang.reflect.Type; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import connectors.api.ConsumedMessage; +import io.smallrye.reactive.messaging.MessageConverter; +import io.smallrye.reactive.messaging.providers.helpers.TypeUtils; + +@ApplicationScoped +public class MyMessageConverter implements MessageConverter { + @Override + public boolean canConvert(Message in, Type target) { + return TypeUtils.isAssignable(target, ConsumedMessage.class) + && in.getMetadata(MyIncomingMetadata.class).isPresent(); + } + + @Override + public Message convert(Message in, Type target) { + return in.withPayload(in.getMetadata(MyIncomingMetadata.class) + .map(MyIncomingMetadata::getCustomMessage) + .orElse(null)); + } +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/connectors/MyOutgoingChannel.java b/content/docs/smallrye-reactive-messaging/latest/_examples/connectors/MyOutgoingChannel.java new file mode 100644 index 000000000..013defda1 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/connectors/MyOutgoingChannel.java @@ -0,0 +1,49 @@ +package connectors; + +import java.util.concurrent.Flow; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import connectors.api.BrokerClient; +import connectors.api.SendMessage; +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.providers.helpers.MultiUtils; +import io.vertx.mutiny.core.Vertx; + +public class MyOutgoingChannel { + private final String channel; + private Flow.Subscriber> subscriber; + private final BrokerClient client; + private final String topic; + + public MyOutgoingChannel(Vertx vertx, MyConnectorOutgoingConfiguration oc, BrokerClient client) { + this.channel = oc.getChannel(); + this.client = client; + this.topic = oc.getTopic().orElse(oc.getChannel()); + this.subscriber = MultiUtils.via(multi -> multi.call(m -> publishMessage(this.client, m))); + } + + private Uni publishMessage(BrokerClient client, Message m) { + // construct the outgoing message + SendMessage sendMessage = new SendMessage(); + Object payload = m.getPayload(); + sendMessage.setPayload(payload); + sendMessage.setTopic(topic); + m.getMetadata(MyOutgoingMetadata.class).ifPresent(out -> { + sendMessage.setTopic(out.getTopic()); + sendMessage.setKey(out.getKey()); + //... + }); + return Uni.createFrom().completionStage(() -> client.send(sendMessage)) + .onItem().transformToUni(receipt -> Uni.createFrom().completionStage(m.ack())) + .onFailure().recoverWithUni(t -> Uni.createFrom().completionStage(m.nack(t))); + } + + public Flow.Subscriber> getSubscriber() { + return this.subscriber; + } + + public String getChannel() { + return this.channel; + } +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/connectors/MyOutgoingChannelWithPartials.java b/content/docs/smallrye-reactive-messaging/latest/_examples/connectors/MyOutgoingChannelWithPartials.java new file mode 100644 index 000000000..2ba3d9e59 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/connectors/MyOutgoingChannelWithPartials.java @@ -0,0 +1,105 @@ +package connectors; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.Flow; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import connectors.api.BrokerClient; +import connectors.api.SendMessage; +import connectors.tracing.MyOpenTelemetryInstrumenter; +import connectors.tracing.MyTrace; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.providers.helpers.MultiUtils; +import io.smallrye.reactive.messaging.providers.helpers.SenderProcessor; +import io.smallrye.reactive.messaging.tracing.TracingUtils; +import io.vertx.mutiny.core.Vertx; + +public class MyOutgoingChannelWithPartials { + private SenderProcessor processor; + private Flow.Subscriber> subscriber; + private final BrokerClient client; + private final String topic; + private final boolean tracingEnabled; + + private Instrumenter instrumenter; + + public MyOutgoingChannelWithPartials(Vertx vertx, MyConnectorOutgoingConfiguration oc, BrokerClient client) { + this.client = client; + this.topic = oc.getTopic().orElse(oc.getChannel()); + this.tracingEnabled = true; + // tag::sender-processor[] + long requests = oc.getMaxPendingMessages(); + boolean waitForWriteCompletion = oc.getWaitForWriteCompletion(); + if (requests <= 0) { + requests = Long.MAX_VALUE; + } + this.processor = new SenderProcessor(requests, waitForWriteCompletion, m -> publishMessage(client, m)); + this.subscriber = MultiUtils.via(processor, m -> m.onFailure().invoke(f -> { + // log the failure + })); + // end::sender-processor[] + instrumenter = MyOpenTelemetryInstrumenter.createInstrumenter(false); + + } + + // tag::send-message[] + private Uni publishMessage(BrokerClient client, Message message) { + // construct the outgoing message + SendMessage sendMessage; + Object payload = message.getPayload(); + if (payload instanceof SendMessage) { + sendMessage = (SendMessage) message.getPayload(); + } else { + sendMessage = new SendMessage(); + sendMessage.setPayload(payload); + sendMessage.setTopic(topic); + message.getMetadata(MyOutgoingMetadata.class).ifPresent(out -> { + sendMessage.setTopic(out.getTopic()); + sendMessage.setKey(out.getKey()); + //... + }); + } + return Uni.createFrom().completionStage(() -> client.send(sendMessage)) + .onItem().transformToUni(receipt -> Uni.createFrom().completionStage(message.ack())) + .onFailure().recoverWithUni(t -> Uni.createFrom().completionStage(message.nack(t))); + } + // end::send-message[] + + // tag::outgoing-tracing[] + private Uni publishMessageWithTracing(BrokerClient client, Message message) { + // construct the outgoing message + SendMessage sendMessage; + Object payload = message.getPayload(); + if (payload instanceof SendMessage) { + sendMessage = (SendMessage) message.getPayload(); + } else { + sendMessage = new SendMessage(); + sendMessage.setPayload(payload); + sendMessage.setTopic(topic); + message.getMetadata(MyOutgoingMetadata.class).ifPresent(out -> { + sendMessage.setTopic(out.getTopic()); + sendMessage.setKey(out.getKey()); + //... + }); + } + if (tracingEnabled) { + Map properties = new HashMap<>(); + TracingUtils.traceOutgoing(instrumenter, message, new MyTrace.Builder() + .withProperties(properties) + .withTopic(sendMessage.getTopic()) + .build()); + sendMessage.setProperties(properties); + } + return Uni.createFrom().completionStage(() -> client.send(sendMessage)) + .onItem().transformToUni(receipt -> Uni.createFrom().completionStage(message.ack())) + .onFailure().recoverWithUni(t -> Uni.createFrom().completionStage(message.nack(t))); + } + // end::outgoing-tracing[] + + public Flow.Subscriber> getSubscriber() { + return this.subscriber; + } +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/connectors/MyOutgoingMessage.java b/content/docs/smallrye-reactive-messaging/latest/_examples/connectors/MyOutgoingMessage.java new file mode 100644 index 000000000..86c8a3636 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/connectors/MyOutgoingMessage.java @@ -0,0 +1,70 @@ +package connectors; + +import java.util.concurrent.CompletionStage; +import java.util.function.Function; +import java.util.function.Supplier; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Metadata; + +import io.smallrye.reactive.messaging.providers.locals.ContextAwareMessage; + +public class MyOutgoingMessage implements Message, ContextAwareMessage { + + private final T payload; + private final Metadata metadata; + + private final Supplier> ack; + private final Function> nack; + + public static MyOutgoingMessage from(Message message) { + return new MyOutgoingMessage<>(message.getPayload(), message.getMetadata(), message.getAck(), message.getNack()); + } + + public MyOutgoingMessage(T payload, Metadata metadata, + Supplier> ack, + Function> nack) { + this.payload = payload; + this.metadata = metadata; + this.ack = ack; + this.nack = nack; + } + + public MyOutgoingMessage(T payload, String key, String topic, + Supplier> ack, + Function> nack) { + this(payload, Metadata.of(new MyOutgoingMetadata(topic, key)), ack, nack); + } + + @Override + public T getPayload() { + return payload; + } + + @Override + public Metadata getMetadata() { + return metadata; + } + + @Override + public Supplier> getAck() { + return this.ack; + } + + @Override + public Function> getNack() { + return this.nack; + } + + public MyOutgoingMessage withKey(String key) { + this.metadata.with(this.metadata.get(MyOutgoingMetadata.class) + .map(m -> MyOutgoingMetadata.builder(m).withKey(key).build())); + return this; + } + + public MyOutgoingMessage withTopic(String topic) { + this.metadata.with(this.metadata.get(MyOutgoingMetadata.class) + .map(m -> MyOutgoingMetadata.builder(m).withTopic(topic).build())); + return this; + } +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/connectors/MyOutgoingMetadata.java b/content/docs/smallrye-reactive-messaging/latest/_examples/connectors/MyOutgoingMetadata.java new file mode 100644 index 000000000..15c47cc1b --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/connectors/MyOutgoingMetadata.java @@ -0,0 +1,56 @@ +package connectors; + +public class MyOutgoingMetadata { + private String topic; + private String key; + + public static MyOutgoingMetadataBuilder builder() { + return new MyOutgoingMetadataBuilder(); + } + + public static MyOutgoingMetadataBuilder builder(MyOutgoingMetadata metadata) { + return new MyOutgoingMetadataBuilder(metadata); + } + + public MyOutgoingMetadata(String topic, String key) { + this.topic = topic; + this.key = key; + } + + public String getTopic() { + return topic; + } + + public String getKey() { + return key; + } + + public static class MyOutgoingMetadataBuilder { + private String topic; + private String key; + + public MyOutgoingMetadataBuilder() { + + } + + public MyOutgoingMetadataBuilder(MyOutgoingMetadata metadata) { + this.key = metadata.getKey(); + this.topic = metadata.getTopic(); + } + + public MyOutgoingMetadataBuilder withTopic(String topic) { + this.topic = topic; + return this; + } + + public MyOutgoingMetadataBuilder withKey(String key) { + this.key = key; + return this; + } + + public MyOutgoingMetadata build() { + return new MyOutgoingMetadata(topic, key); + } + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/connectors/test/MyConnectorTest.java b/content/docs/smallrye-reactive-messaging/latest/_examples/connectors/test/MyConnectorTest.java new file mode 100644 index 000000000..718aac41f --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/connectors/test/MyConnectorTest.java @@ -0,0 +1,52 @@ +package connectors.test; + +import static org.awaitility.Awaitility.await; + +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CopyOnWriteArrayList; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.junit.jupiter.api.Test; + +import connectors.MyConnector; +import io.smallrye.reactive.messaging.test.common.config.MapBasedConfig; + +public class MyConnectorTest extends WeldTestBase { + + @Test + void incomingChannel() { + String host = ""; + int port = 0; + String myTopic = UUID.randomUUID().toString(); + MapBasedConfig config = new MapBasedConfig() + .with("mp.messaging.incoming.data.topic", myTopic) + .with("mp.messaging.incoming.data.host", host) + .with("mp.messaging.incoming.data.port", port) + .with("mp.messaging.incoming.data.connector", MyConnector.CONNECTOR_NAME); + MyApp app = runApplication(config, MyApp.class); + + int expected = 10; + // produce expected number of messages to myTopic + + // wait until app received + await().until(() -> app.received().size() == expected); + } + + @ApplicationScoped + public static class MyApp { + + List received = new CopyOnWriteArrayList<>(); + + @Incoming("data") + void consume(String msg) { + received.add(msg); + } + + public List received() { + return received; + } + } +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/connectors/test/WeldTestBase.java b/content/docs/smallrye-reactive-messaging/latest/_examples/connectors/test/WeldTestBase.java new file mode 100644 index 000000000..35cf9555b --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/connectors/test/WeldTestBase.java @@ -0,0 +1,140 @@ +package connectors.test; + +import jakarta.enterprise.inject.spi.BeanManager; + +import org.eclipse.microprofile.reactive.messaging.spi.ConnectorLiteral; +import org.jboss.weld.environment.se.Weld; +import org.jboss.weld.environment.se.WeldContainer; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; + +import connectors.MyConnector; +import connectors.MyMessageConverter; +import io.smallrye.config.inject.ConfigExtension; +import io.smallrye.reactive.messaging.providers.MediatorFactory; +import io.smallrye.reactive.messaging.providers.connectors.ExecutionHolder; +import io.smallrye.reactive.messaging.providers.connectors.WorkerPoolRegistry; +import io.smallrye.reactive.messaging.providers.extension.ChannelProducer; +import io.smallrye.reactive.messaging.providers.extension.EmitterFactoryImpl; +import io.smallrye.reactive.messaging.providers.extension.HealthCenter; +import io.smallrye.reactive.messaging.providers.extension.LegacyEmitterFactoryImpl; +import io.smallrye.reactive.messaging.providers.extension.MediatorManager; +import io.smallrye.reactive.messaging.providers.extension.MutinyEmitterFactoryImpl; +import io.smallrye.reactive.messaging.providers.extension.ReactiveMessagingExtension; +import io.smallrye.reactive.messaging.providers.impl.ConfiguredChannelFactory; +import io.smallrye.reactive.messaging.providers.impl.ConnectorFactories; +import io.smallrye.reactive.messaging.providers.impl.InternalChannelRegistry; +import io.smallrye.reactive.messaging.providers.metrics.MetricDecorator; +import io.smallrye.reactive.messaging.providers.metrics.MicrometerDecorator; +import io.smallrye.reactive.messaging.providers.wiring.Wiring; +import io.smallrye.reactive.messaging.test.common.config.MapBasedConfig; +import io.smallrye.reactive.messaging.test.common.config.SmallRyeConfigTestUtil; + +public class WeldTestBase { + + protected Weld weld; + protected WeldContainer container; + + @BeforeEach + public void initWeld() { + weld = new Weld(); + + // SmallRye config + ConfigExtension extension = new ConfigExtension(); + weld.addExtension(extension); + + weld.addBeanClass(MediatorFactory.class); + weld.addBeanClass(MediatorManager.class); + weld.addBeanClass(InternalChannelRegistry.class); + weld.addBeanClass(ConnectorFactories.class); + weld.addBeanClass(ConfiguredChannelFactory.class); + weld.addBeanClass(ChannelProducer.class); + weld.addBeanClass(ExecutionHolder.class); + weld.addBeanClass(WorkerPoolRegistry.class); + weld.addBeanClass(HealthCenter.class); + weld.addBeanClass(Wiring.class); + weld.addExtension(new ReactiveMessagingExtension()); + + weld.addBeanClass(EmitterFactoryImpl.class); + weld.addBeanClass(MutinyEmitterFactoryImpl.class); + weld.addBeanClass(LegacyEmitterFactoryImpl.class); + + weld.addBeanClass(MyConnector.class); + weld.addBeanClass(MyMessageConverter.class); + weld.addBeanClass(MetricDecorator.class); + weld.addBeanClass(MicrometerDecorator.class); + weld.disableDiscovery(); + } + + @AfterEach + public void stopContainer() { + if (container != null) { + // TODO Explicitly close the connector + getBeanManager().createInstance() + .select(MyConnector.class, ConnectorLiteral.of(MyConnector.CONNECTOR_NAME)).get(); + container.close(); + } + // Release the config objects + SmallRyeConfigTestUtil.releaseConfig(); + } + + public BeanManager getBeanManager() { + if (container == null) { + runApplication(new MapBasedConfig()); + } + return container.getBeanManager(); + } + + public void addBeans(Class... clazzes) { + weld.addBeanClasses(clazzes); + } + + public T get(Class clazz) { + return getBeanManager().createInstance().select(clazz).get(); + } + + public T runApplication(MapBasedConfig config, Class clazz) { + weld.addBeanClass(clazz); + runApplication(config); + return get(clazz); + } + + public void runApplication(MapBasedConfig config) { + if (config != null) { + config.write(); + } else { + MapBasedConfig.cleanup(); + } + + SmallRyeConfigTestUtil.installConfig(); + container = weld.initialize(); + } + + public static void addConfig(MapBasedConfig config) { + if (config != null) { + config.write(); + } else { + MapBasedConfig.cleanup(); + } + } + + public HealthCenter getHealth() { + if (container == null) { + throw new IllegalStateException("Application not started"); + } + return container.getBeanManager().createInstance().select(HealthCenter.class).get(); + } + + public boolean isStarted() { + return getHealth().getStartup().isOk(); + } + + public boolean isReady() { + return getHealth().getReadiness().isOk(); + } + + public boolean isAlive() { + return getHealth().getLiveness().isOk(); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/connectors/tracing/MyOpenTelemetryInstrumenter.java b/content/docs/smallrye-reactive-messaging/latest/_examples/connectors/tracing/MyOpenTelemetryInstrumenter.java new file mode 100644 index 000000000..9454e254f --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/connectors/tracing/MyOpenTelemetryInstrumenter.java @@ -0,0 +1,64 @@ +package connectors.tracing; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessageOperation; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingSpanNameExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; +import io.smallrye.reactive.messaging.tracing.TracingUtils; + +/** + * Encapsulates the OpenTelemetry instrumentation API so that those classes are only needed if + * users explicitly enable tracing. + */ +public class MyOpenTelemetryInstrumenter { + + private final Instrumenter instrumenter; + + private MyOpenTelemetryInstrumenter(Instrumenter instrumenter) { + this.instrumenter = instrumenter; + } + + public static MyOpenTelemetryInstrumenter createIncoming() { + return new MyOpenTelemetryInstrumenter(createInstrumenter(true)); + } + + public static MyOpenTelemetryInstrumenter createOutgoing() { + return new MyOpenTelemetryInstrumenter(createInstrumenter(false)); + } + + // tag::create-instrumenter[] + public static Instrumenter createInstrumenter(boolean incoming) { + MessageOperation messageOperation = incoming ? MessageOperation.RECEIVE : MessageOperation.PUBLISH; + + MyAttributesExtractor myExtractor = new MyAttributesExtractor(); + MessagingAttributesGetter attributesGetter = myExtractor.getMessagingAttributesGetter(); + var spanNameExtractor = MessagingSpanNameExtractor.create(attributesGetter, messageOperation); + InstrumenterBuilder builder = Instrumenter.builder(GlobalOpenTelemetry.get(), + "io.smallrye.reactive.messaging", spanNameExtractor); + var attributesExtractor = MessagingAttributesExtractor.create(attributesGetter, messageOperation); + + builder + .addAttributesExtractor(attributesExtractor) + .addAttributesExtractor(myExtractor); + + if (incoming) { + return builder.buildConsumerInstrumenter(MyTraceTextMapGetter.INSTANCE); + } else { + return builder.buildProducerInstrumenter(MyTraceTextMapSetter.INSTANCE); + } + } + // + + public Message traceIncoming(Message message, MyTrace myTrace, boolean makeCurrent) { + return TracingUtils.traceIncoming(instrumenter, message, myTrace, makeCurrent); + } + + public void traceOutgoing(Message message, MyTrace myTrace) { + TracingUtils.traceOutgoing(instrumenter, message, myTrace); + } +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/consumption/ConsumptionExamples.java b/content/docs/smallrye-reactive-messaging/latest/_examples/consumption/ConsumptionExamples.java new file mode 100644 index 000000000..2094888b6 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/consumption/ConsumptionExamples.java @@ -0,0 +1,70 @@ +package consumption; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.mutiny.Uni; + +public class ConsumptionExamples { + + //tag::message-cs[] + @Incoming("my-channel") + public CompletionStage consumeMessage(Message message) { + handle(message.getPayload()); + return message.ack(); + } + //end::message-cs[] + + //tag::payload-sync[] + @Incoming("my-channel") + public void consumePayload(Price payload) { + // do something + } + //end::payload-sync[] + + //tag::payload-cs[] + @Incoming("my-channel") + public CompletionStage consumePayloadCS(Price payload) { + CompletionStage cs = handleAsync(payload); + return cs; + } + //end::payload-cs[] + + //tag::payload-uni[] + @Incoming("my-channel") + public Uni consumePayloadUni(Price payload) { + return Uni.createFrom().item(payload) + .onItem().invoke(this::handle) + .onItem().ignore().andContinueWithNull(); + } + //end::payload-uni[] + + //tag::message-uni[] + @Incoming("my-channel") + public Uni consumeMessageUni(Message message) { + return Uni.createFrom().item(message) + .onItem().invoke(m -> handle(m.getPayload())) + .onItem().transformToUni(x -> Uni.createFrom().completionStage(message.ack())); + } + //end::message-uni[] + + private CompletionStage handleAsync(Price price) { + return CompletableFuture.completedFuture(null); + } + + @SuppressWarnings("UnusedReturnValue") + private Price handle(Price price) { + return price; + } + + public static class Price { + final double price; + + public Price(double p) { + this.price = p; + } + } +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/converters/ConverterExample.java b/content/docs/smallrye-reactive-messaging/latest/_examples/converters/ConverterExample.java new file mode 100644 index 000000000..a9d3b661e --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/converters/ConverterExample.java @@ -0,0 +1,24 @@ +package converters; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +public class ConverterExample { + + // tag::code[] + @Outgoing("persons") + public Multi source() { + return Multi.createFrom().items("Neo", "Morpheus", "Trinity"); + } + + // The messages need to be converted as they are emitted as Message + // and consumed as Message + @Incoming("persons") + public void consume(Person p) { + // ... + } + // end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/converters/MyConverter.java b/content/docs/smallrye-reactive-messaging/latest/_examples/converters/MyConverter.java new file mode 100644 index 000000000..3f517ece0 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/converters/MyConverter.java @@ -0,0 +1,31 @@ +package converters; + +import java.lang.reflect.Type; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.MessageConverter; + +// tag::code[] +@ApplicationScoped +public class MyConverter implements MessageConverter { + @Override + public boolean canConvert(Message in, Type target) { + // Checks whether this converter can be used to convert + // the incoming message into a message containing a payload + // of the type `target`. + return in.getPayload().getClass().equals(String.class) + && target.equals(Person.class); + } + + @Override + public Message convert(Message in, Type target) { + // Convert the incoming message into the new message. + // It's important to build the new message **from** + // the received one. + return in.withPayload(new Person((String) in.getPayload())); + } +} +// end::code[] diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/customizers/MyClientCustomizer.java b/content/docs/smallrye-reactive-messaging/latest/_examples/customizers/MyClientCustomizer.java new file mode 100644 index 000000000..9e07288be --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/customizers/MyClientCustomizer.java @@ -0,0 +1,17 @@ +package customizers; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.config.Config; + +import io.smallrye.reactive.messaging.ClientCustomizer; + +@ApplicationScoped +public class MyClientCustomizer implements ClientCustomizer { + @Override + public ClientConfig customize(String channel, Config channelConfig, ClientConfig config) { + // customize the client configuration + return config; + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/decorators/ConsumedMessageDecorator.java b/content/docs/smallrye-reactive-messaging/latest/_examples/decorators/ConsumedMessageDecorator.java new file mode 100644 index 000000000..d1b403fde --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/decorators/ConsumedMessageDecorator.java @@ -0,0 +1,41 @@ +package decorators; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.mutiny.Multi; +import io.smallrye.reactive.messaging.PublisherDecorator; + +// tag::code[] +@ApplicationScoped +public class ConsumedMessageDecorator implements PublisherDecorator { + + private final Map counters = new HashMap<>(); + + @Override + public Multi> decorate(Multi> publisher, String channelName, + boolean isConnector) { + if (isConnector) { + AtomicLong counter = new AtomicLong(); + counters.put(channelName, counter); + return publisher.onItem().invoke(counter::incrementAndGet); + } else { + return publisher; + } + } + + @Override + public int getPriority() { + return 10; + } + + public long getMessageCount(String channel) { + return counters.get(channel).get(); + } +} +// end::code[] diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/emitter/ChannelExamples.java b/content/docs/smallrye-reactive-messaging/latest/_examples/emitter/ChannelExamples.java new file mode 100644 index 000000000..1b2f021ba --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/emitter/ChannelExamples.java @@ -0,0 +1,31 @@ +package emitter; + +import java.util.concurrent.Flow.Publisher; + +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.mutiny.Multi; + +public class ChannelExamples { + + // tag::channel[] + @Inject + @Channel("my-channel") + Multi streamOfPayloads; + + @Inject + @Channel("my-channel") + Multi> streamOfMessages; + + @Inject + @Channel("my-channel") + Publisher publisherOfPayloads; + + @Inject + @Channel("my-channel") + Publisher> publisherOfMessages; + // end::channel[] +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/emitter/EmitterExamples.java b/content/docs/smallrye-reactive-messaging/latest/_examples/emitter/EmitterExamples.java new file mode 100644 index 000000000..4c5c4ed99 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/emitter/EmitterExamples.java @@ -0,0 +1,119 @@ +package emitter; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.*; + +import io.smallrye.reactive.messaging.annotations.Broadcast; +import messages.MyMetadata; + +public class EmitterExamples { + + // tag::payload[] + @Inject + @Channel("prices") + Emitter emitterForPrices; + + public void send(double d) { + emitterForPrices.send(d); + } + // end::payload[] + + // tag::cs[] + public void sendAndAwaitAcknowledgement(double d) { + CompletionStage acked = emitterForPrices.send(d); + // sending a payload returns a CompletionStage completed + // when the message is acknowledged + acked.toCompletableFuture().join(); + } + // end::cs[] + + // tag::message[] + public void sendAsMessage(double d) { + emitterForPrices.send(Message.of(d)); + } + // end::message[] + + // tag::message-ack[] + public void sendAsMessageWithAck(double d) { + emitterForPrices.send(Message.of(d, () -> { + // Called when the message is acknowledged. + return CompletableFuture.completedFuture(null); + }, + reason -> { + // Called when the message is acknowledged negatively. + return CompletableFuture.completedFuture(null); + })); + } + // end::message-ack[] + + // tag::message-meta[] + public void sendAsMessageWithAckAndMetadata(double d) { + MyMetadata metadata = new MyMetadata(); + emitterForPrices.send(Message.of(d, Metadata.of(metadata), + () -> { + // Called when the message is acknowledged. + return CompletableFuture.completedFuture(null); + }, + reason -> { + // Called when the message is acknowledged negatively. + return CompletableFuture.completedFuture(null); + })); + } + // end::message-meta[] + + // tag::broadcast[] + @Inject + @Broadcast + @Channel("prices") + Emitter emitter; + + public void emit(double d) { + emitter.send(d); + } + + @Incoming("prices") + public void handle(double d) { + // Handle the new price + } + + @Incoming("prices") + public void audit(double d) { + // Audit the price change + } + // end::broadcast[] + + static class OverflowExample { + // tag::overflow[] + + // Set the max size to 10 and fail if reached + @OnOverflow(value = OnOverflow.Strategy.BUFFER, bufferSize = 10) + @Inject + @Channel("channel") + Emitter emitterWithBuffer; + + // [DANGER ZONE] no limit + @OnOverflow(OnOverflow.Strategy.UNBOUNDED_BUFFER) + @Inject + @Channel("channel") + Emitter danger; + + // Drop the new messages if the size is reached + @OnOverflow(OnOverflow.Strategy.DROP) + @Inject + @Channel("channel") + Emitter dropping; + + // Drop the previously sent messages if the size is reached + @OnOverflow(OnOverflow.Strategy.LATEST) + @Inject + @Channel("channel") + Emitter dropOldMessages; + + // end::overflow[] + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/emitter/EmitterFactoryExamples.java b/content/docs/smallrye-reactive-messaging/latest/_examples/emitter/EmitterFactoryExamples.java new file mode 100644 index 000000000..d9884a773 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/emitter/EmitterFactoryExamples.java @@ -0,0 +1,88 @@ +package emitter; + +import java.util.concurrent.Flow.Publisher; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; +import jakarta.enterprise.inject.spi.InjectionPoint; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.ChannelRegistry; +import io.smallrye.reactive.messaging.EmitterConfiguration; +import io.smallrye.reactive.messaging.EmitterFactory; +import io.smallrye.reactive.messaging.EmitterType; +import io.smallrye.reactive.messaging.MessagePublisherProvider; +import io.smallrye.reactive.messaging.annotations.EmitterFactoryFor; +import io.smallrye.reactive.messaging.providers.extension.ChannelProducer; + +public class EmitterFactoryExamples { + + // tag::custom-emitter-declaration[] + + public interface CustomEmitter extends EmitterType { + + > void sendAndForget(M msg); + + } + + public static class CustomEmitterImpl implements CustomEmitter, MessagePublisherProvider { + + Publisher> publisher; + + public CustomEmitterImpl(EmitterConfiguration configuration, long defaultBufferSize) { + //... initialize emitter with configuration + } + + @Override + public Publisher> getPublisher() { + return publisher; + } + + @Override + public > void sendAndForget(M msg) { + //... send to stream + } + } + // end::custom-emitter-declaration[] + + // tag::custom-emitter-factory[] + @EmitterFactoryFor(CustomEmitter.class) + @ApplicationScoped + public static class CustomEmitterFactory implements EmitterFactory> { + + @Inject + ChannelRegistry channelRegistry; + + @Override + public CustomEmitterImpl createEmitter(EmitterConfiguration configuration, long defaultBufferSize) { + return new CustomEmitterImpl<>(configuration, defaultBufferSize); + } + + @Produces + @Channel("") // Stream name is ignored during type-safe resolution + CustomEmitter produce(InjectionPoint injectionPoint) { + String channelName = ChannelProducer.getChannelName(injectionPoint); + return channelRegistry.getEmitter(channelName, CustomEmitter.class); + } + } + // end::custom-emitter-factory[] + + // tag::custom-emitter-usage[] + + @Inject + @Channel("custom-emitter-channel") + CustomEmitter customEmitter; + + //... + + public void emitMessage() { + customEmitter.sendAndForget(Message.of("a")); + customEmitter.sendAndForget(Message.of("b")); + customEmitter.sendAndForget(Message.of("c")); + } + // end::custom-emitter-usage[] + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/emitter/MutinyExamples.java b/content/docs/smallrye-reactive-messaging/latest/_examples/emitter/MutinyExamples.java new file mode 100644 index 000000000..d6d2321b5 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/emitter/MutinyExamples.java @@ -0,0 +1,33 @@ +package emitter; + +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; + +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.subscription.Cancellable; +import io.smallrye.reactive.messaging.MutinyEmitter; + +public class MutinyExamples { + // tag::uni[] + @Inject + @Channel("prices") + MutinyEmitter emitter; + + public Uni send(double d) { + return emitter.send(d); + } + // end::uni[] + + // tag::uni-await[] + public void sendAwait(double d) { + emitter.sendAndAwait(d); + } + // end::uni-await[] + + // tag::uni-forget[] + public Cancellable sendForget(double d) { + return emitter.sendAndForget(d); + } + // end::uni-forget[] +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/emitter/MyImperativeBean.java b/content/docs/smallrye-reactive-messaging/latest/_examples/emitter/MyImperativeBean.java new file mode 100644 index 000000000..a4619004f --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/emitter/MyImperativeBean.java @@ -0,0 +1,24 @@ +package emitter; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +// tag::intro[] +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Emitter; + +@ApplicationScoped +public class MyImperativeBean { + + @Inject + @Channel("prices") + Emitter emitter; + + // ... + + public void send(double d) { + emitter.send(d); + } +} + +// end::intro[] diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/generation/GenerationExamples.java b/content/docs/smallrye-reactive-messaging/latest/_examples/generation/GenerationExamples.java new file mode 100644 index 000000000..c4c0afdc5 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/generation/GenerationExamples.java @@ -0,0 +1,95 @@ +package generation; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.Flow; +import java.util.concurrent.atomic.AtomicInteger; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; + +public class GenerationExamples { + + AtomicInteger counter = new AtomicInteger(); + + // tag::message-sync[] + @Outgoing("my-channel") + public Message generateMessagesSynchronously() { + return Message.of(counter.getAndIncrement()); + } + // end::message-sync[] + + // tag::message-cs[] + @Outgoing("my-channel") + public CompletionStage> generateMessagesAsCompletionStage() { + return asyncClient.poll() + .thenApply(Message::of); + } + // end::message-cs[] + + public static class Price { + final double price; + + public Price(double p) { + this.price = p; + } + } + + private Pollable asyncClient = () -> CompletableFuture.completedFuture(new Price(10.0)); + + private interface Pollable { + CompletionStage poll(); + } + + // tag::message-uni[] + @Outgoing("my-channel") + public Uni> generateMessagesAsync() { + return Uni.createFrom().item(() -> Message.of(counter.getAndIncrement())); + } + // end::message-uni[] + + // tag::message-stream[] + public Flow.Publisher> generateMessageStream() { + Multi multi = reactiveClient.getStream(); + return multi.map(Message::of); + } + // end::message-stream[] + + private ReactiveClient reactiveClient = () -> Multi.createFrom().items("a", "b", "c"); + + private interface ReactiveClient { + Multi getStream(); + } + + // tag::payload-sync[] + @Outgoing("my-channel") + public Integer generatePayloadsSynchronously() { + return counter.getAndIncrement(); + } + // end::payload-sync[] + + // tag::payload-cs[] + @Outgoing("my-channel") + public CompletionStage generatePayloadsAsCompletionStage() { + return asyncClient.poll(); + } + // end::payload-cs[] + + // tag::payload-uni[] + @Outgoing("my-channel") + public Uni generatePayloadsAsync() { + return Uni.createFrom().item(() -> counter.getAndIncrement()); + } + // end::payload-uni[] + + // tag::payload-stream[] + @Outgoing("my-channel") + public Multi generatePayloadsStream() { + Multi multi = reactiveClient.getStream(); + return multi; + } + // end::payload-stream[] +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/genericpayload/GenericPayloadExample.java b/content/docs/smallrye-reactive-messaging/latest/_examples/genericpayload/GenericPayloadExample.java new file mode 100644 index 000000000..583cfcec0 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/genericpayload/GenericPayloadExample.java @@ -0,0 +1,49 @@ +package genericpayload; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Metadata; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; +import io.smallrye.reactive.messaging.GenericPayload; +import io.smallrye.reactive.messaging.MutinyEmitter; +import messages.MyMetadata; + +@ApplicationScoped +public class GenericPayloadExample { + + // tag::code[] + @Outgoing("out") + Multi> produce() { + return Multi.createFrom().range(0, 100) + .map(i -> GenericPayload.of(">> " + i, Metadata.of(new MyMetadata()))); + } + // end::code[] + + // tag::injection[] + @Incoming("in") + @Outgoing("out") + GenericPayload process(int payload, MyMetadata metadata) { + // use the injected metadata + String id = metadata.getId(); + return GenericPayload.of(">> " + payload + " " + id, + Metadata.of(metadata, new MyMetadata("Bob", "Alice"))); + } + // end::injection[] + + // tag::emitter[] + @Inject + @Channel("out") + MutinyEmitter> emitter; + + public void emitWithMetadata(String payload) { + emitter.sendAndForget( + GenericPayload.of(payload, Metadata.of(new MyMetadata()))); + } + // end::emitter[] + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/incomings/IncomingsExamples.java b/content/docs/smallrye-reactive-messaging/latest/_examples/incomings/IncomingsExamples.java new file mode 100644 index 000000000..f1d727df7 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/incomings/IncomingsExamples.java @@ -0,0 +1,16 @@ +package incomings; + +import org.eclipse.microprofile.reactive.messaging.Incoming; + +public class IncomingsExamples { + + //tag::code[] + @Incoming("channel-1") + @Incoming("channel-2") + public String process(String s) { + // get messages from channel-1 and channel-2 + return s.toUpperCase(); + } + //end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/incomings/MetadataInjectionExample.java b/content/docs/smallrye-reactive-messaging/latest/_examples/incomings/MetadataInjectionExample.java new file mode 100644 index 000000000..79c3cc3ef --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/incomings/MetadataInjectionExample.java @@ -0,0 +1,44 @@ +package incomings; + +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Uni; +import jdk.jfr.Period; +import messages.MyMetadata; + +public class MetadataInjectionExample { + + //tag::code[] + @Incoming("in") + @Outgoing("out") + public String process(String payload, MyMetadata metadata) { + // ... + return payload.toUpperCase(); + } + + @Incoming("in") + @Outgoing("out") + public Uni process2(String payload, MyMetadata metadata) { + // ... + return Uni.createFrom().item(payload.toUpperCase()); + } + + // ... + @Incoming("in") + public void consume(String payload, Optional metadata, Period metadata2) { + // ... + } + + @Incoming("in") + public CompletionStage consume2(String payload, Optional metadata, Period metadata2) { + // ... + return CompletableFuture.completedFuture(null); + } + //end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/interceptors/MyIncomingInterceptor.java b/content/docs/smallrye-reactive-messaging/latest/_examples/interceptors/MyIncomingInterceptor.java new file mode 100644 index 000000000..c322a7553 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/interceptors/MyIncomingInterceptor.java @@ -0,0 +1,28 @@ +package interceptors; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.common.annotation.Identifier; +import io.smallrye.reactive.messaging.IncomingInterceptor; + +@Identifier("channel-a") +@ApplicationScoped +public class MyIncomingInterceptor implements IncomingInterceptor { + + @Override + public Message afterMessageReceive(Message message) { + return message.withPayload("changed " + message.getPayload()); + } + + @Override + public void onMessageAck(Message message) { + // Called after message ack + } + + @Override + public void onMessageNack(Message message, Throwable failure) { + // Called after message nack + } +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/interceptors/MyOutgoingInterceptor.java b/content/docs/smallrye-reactive-messaging/latest/_examples/interceptors/MyOutgoingInterceptor.java new file mode 100644 index 000000000..6efdc8835 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/interceptors/MyOutgoingInterceptor.java @@ -0,0 +1,30 @@ +package interceptors; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.common.annotation.Identifier; +import io.smallrye.reactive.messaging.OutgoingInterceptor; +import io.smallrye.reactive.messaging.OutgoingMessageMetadata; + +@Identifier("channel-a") +@ApplicationScoped +public class MyOutgoingInterceptor implements OutgoingInterceptor { + + @Override + public Message onMessage(Message message) { + return message.withPayload("changed " + message.getPayload()); + } + + @Override + public void onMessageAck(Message message) { + message.getMetadata(OutgoingMessageMetadata.class) + .ifPresent(m -> m.getResult()); + } + + @Override + public void onMessageNack(Message message, Throwable failure) { + + } +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/jms/inbound/JmsMetadataExample.java b/content/docs/smallrye-reactive-messaging/latest/_examples/jms/inbound/JmsMetadataExample.java new file mode 100644 index 000000000..adb2a0a65 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/jms/inbound/JmsMetadataExample.java @@ -0,0 +1,25 @@ +package jms.inbound; + +import java.util.Optional; + +import jakarta.jms.Destination; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.jms.IncomingJmsMessageMetadata; + +public class JmsMetadataExample { + + public void metadata() { + Message incoming = Message.of(12.0); + // tag::code[] + Optional metadata = incoming.getMetadata(IncomingJmsMessageMetadata.class); + metadata.ifPresent(meta -> { + long expiration = meta.getExpiration(); + Destination destination = meta.getDestination(); + String value = meta.getStringProperty("my-property"); + }); + // end::code[] + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/jms/inbound/JmsPriceConsumer.java b/content/docs/smallrye-reactive-messaging/latest/_examples/jms/inbound/JmsPriceConsumer.java new file mode 100644 index 000000000..ac4cdd774 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/jms/inbound/JmsPriceConsumer.java @@ -0,0 +1,15 @@ +package jms.inbound; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; + +@ApplicationScoped +public class JmsPriceConsumer { + + @Incoming("prices") + public void consume(double price) { + // process your price. + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/jms/inbound/JmsPriceMessageConsumer.java b/content/docs/smallrye-reactive-messaging/latest/_examples/jms/inbound/JmsPriceMessageConsumer.java new file mode 100644 index 000000000..6dfdf485e --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/jms/inbound/JmsPriceMessageConsumer.java @@ -0,0 +1,21 @@ +package jms.inbound; + +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; + +@ApplicationScoped +public class JmsPriceMessageConsumer { + + @Incoming("prices") + public CompletionStage consume(Message price) { + // process your price. + + // Acknowledge the incoming message + return price.ack(); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/jms/outbound/JmsOutboundMetadataExample.java b/content/docs/smallrye-reactive-messaging/latest/_examples/jms/outbound/JmsOutboundMetadataExample.java new file mode 100644 index 000000000..6142bccf7 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/jms/outbound/JmsOutboundMetadataExample.java @@ -0,0 +1,23 @@ +package jms.outbound; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.jms.JmsProperties; +import io.smallrye.reactive.messaging.jms.OutgoingJmsMessageMetadata; + +public class JmsOutboundMetadataExample { + + public Message metadata(Message incoming) { + + // tag::code[] + OutgoingJmsMessageMetadata metadata = OutgoingJmsMessageMetadata.builder() + .withProperties(JmsProperties.builder().with("some-property", "some-value").build()) + .build(); + + // Create a new message from the `incoming` message + // Add `metadata` to the metadata from the `incoming` message. + return incoming.addMetadata(metadata); + // end::code[] + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/jms/outbound/JmsPriceMessageProducer.java b/content/docs/smallrye-reactive-messaging/latest/_examples/jms/outbound/JmsPriceMessageProducer.java new file mode 100644 index 000000000..bd57c863b --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/jms/outbound/JmsPriceMessageProducer.java @@ -0,0 +1,26 @@ +package jms.outbound; + +import java.time.Duration; +import java.util.Random; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +@ApplicationScoped +public class JmsPriceMessageProducer { + + private Random random = new Random(); + + @Outgoing("prices") + public Multi> generate() { + // Build an infinite stream of random prices + // It emits a price every second + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .map(x -> Message.of(random.nextDouble())); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/jms/outbound/JmsPriceProducer.java b/content/docs/smallrye-reactive-messaging/latest/_examples/jms/outbound/JmsPriceProducer.java new file mode 100644 index 000000000..b693a91f7 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/jms/outbound/JmsPriceProducer.java @@ -0,0 +1,25 @@ +package jms.outbound; + +import java.time.Duration; +import java.util.Random; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +@ApplicationScoped +public class JmsPriceProducer { + + private Random random = new Random(); + + @Outgoing("prices") + public Multi generate() { + // Build an infinite stream of random prices + // It emits a price every second + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .map(x -> random.nextDouble()); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/companion/ConsumerTest.java b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/companion/ConsumerTest.java new file mode 100644 index 000000000..fe0e60637 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/companion/ConsumerTest.java @@ -0,0 +1,45 @@ +package kafka.companion; + +import static org.testcontainers.shaded.org.awaitility.Awaitility.await; + +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; + +import org.apache.kafka.common.TopicPartition; +import org.junit.jupiter.api.Test; + +import io.smallrye.reactive.messaging.kafka.companion.ConsumerBuilder; +import io.smallrye.reactive.messaging.kafka.companion.test.KafkaCompanionTestBase; + +public class ConsumerTest extends KafkaCompanionTestBase { + + @Test + public void fromTopics() { + // tag::topics[] + companion.consumeIntegers().fromTopics("topic1", "topic2"); + // end::topics[] + } + + @Test + public void fromOffset() { + // tag::offsets[] + Map offsets = new HashMap<>(); + offsets.put(new TopicPartition("topic1", 0), 100L); + offsets.put(new TopicPartition("topic2", 0), 100L); + companion.consumeIntegers().fromOffsets(offsets, Duration.ofSeconds(10)); + // end::offsets[] + } + + @Test + public void committed() { + // tag::committed[] + ConsumerBuilder consumer = companion.consumeIntegers() + .withAutoCommit(); + consumer.fromTopics("topic"); + // ... + await().untilAsserted(consumer::waitForAssignment); + consumer.committed(new TopicPartition("topic", 1)); + // end::committed[] + } +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/companion/KafkaTest.java b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/companion/KafkaTest.java new file mode 100644 index 000000000..e10717dc0 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/companion/KafkaTest.java @@ -0,0 +1,76 @@ +package kafka.companion; + +import java.time.Duration; + +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.apache.kafka.common.serialization.Deserializer; +import org.apache.kafka.common.serialization.Serializer; +import org.junit.jupiter.api.Assertions; + +import io.smallrye.reactive.messaging.kafka.companion.ConsumerBuilder; +import io.smallrye.reactive.messaging.kafka.companion.ConsumerTask; +import io.smallrye.reactive.messaging.kafka.companion.KafkaCompanion; +import io.smallrye.reactive.messaging.kafka.companion.ProducerBuilder; + +public class KafkaTest { + + public void newCompanion() { + // tag::companion[] + // Create companion with bootstrap servers and API timeout (default is 10 seconds) + KafkaCompanion companion = new KafkaCompanion("localhost:9092", Duration.ofSeconds(5)); + + // Create producer and start producer task + ProducerBuilder producer = companion.produceIntegers() + .withClientId("my-producer") + .withProp("max.block.ms", "5000"); + producer.usingGenerator(i -> new ProducerRecord<>("topic", i), 100); + + // Create consumer and start consumer task + ConsumerBuilder consumer = companion.consumeIntegers() + .withGroupId("my-group") + .withCommitAsyncWhen(record -> true); + ConsumerTask records = consumer.fromTopics("topic", Duration.ofSeconds(10)); + // Await completion and assert consumed record count + Assertions.assertEquals(records.awaitCompletion().count(), 100); + // end::companion[] + } + + public void serdes() { + // tag::serdes[] + KafkaCompanion companion = new KafkaCompanion("localhost:9092"); + + // Register serde to the companion + companion.registerSerde(Orders.class, new OrdersSerializer(), new OrdersDeserializer()); + + // Companion will configure consumer accordingly + ConsumerTask orders = companion.consume(Integer.class, Orders.class) + .fromTopics("orders", 1000).awaitCompletion(); + + for (ConsumerRecord order : orders) { + // ... check consumed records + } + // end::serdes[] + } + + public static class Orders { + + } + + public static class OrdersSerializer implements Serializer { + + @Override + public byte[] serialize(String s, Orders orders) { + return new byte[0]; + } + } + + public static class OrdersDeserializer implements Deserializer { + + @Override + public Orders deserialize(String s, byte[] bytes) { + return null; + } + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/companion/KafkaWithBaseTest.java b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/companion/KafkaWithBaseTest.java new file mode 100644 index 000000000..63bd8634a --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/companion/KafkaWithBaseTest.java @@ -0,0 +1,32 @@ +package kafka.companion; + +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import io.smallrye.reactive.messaging.kafka.companion.ConsumerTask; +import io.smallrye.reactive.messaging.kafka.companion.ProducerTask; +import io.smallrye.reactive.messaging.kafka.companion.test.KafkaCompanionTestBase; + +// tag::code[] +public class KafkaWithBaseTest extends KafkaCompanionTestBase { + + @Test + public void testWithBase() { + // companion is created by the base class + + // produce 100 records to messages topic + ProducerTask producerTask = companion.produceIntegers() + .usingGenerator(i -> new ProducerRecord<>("messages", i), 100); + long msgCount = producerTask.awaitCompletion().count(); + Assertions.assertEquals(msgCount, 100); + + // consume 100 records from messages topic + ConsumerTask consumerTask = companion.consumeIntegers() + .fromTopics("messages", 100); + ConsumerRecord lastRecord = consumerTask.awaitCompletion().getLastRecord(); + Assertions.assertEquals(lastRecord.value(), 99); + } +} +// end::code[] diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/companion/OffsetsTest.java b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/companion/OffsetsTest.java new file mode 100644 index 000000000..f7b00fa6b --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/companion/OffsetsTest.java @@ -0,0 +1,36 @@ +package kafka.companion; + +import java.util.Collection; + +import org.apache.kafka.clients.admin.ConsumerGroupListing; +import org.apache.kafka.clients.admin.OffsetSpec; +import org.apache.kafka.common.TopicPartition; +import org.junit.jupiter.api.Test; + +import io.smallrye.reactive.messaging.kafka.companion.KafkaCompanion; +import io.smallrye.reactive.messaging.kafka.companion.test.KafkaCompanionTestBase; + +public class OffsetsTest extends KafkaCompanionTestBase { + + @Test + public void offsets() { + // tag::offsets[] + TopicPartition topicPartition = KafkaCompanion.tp("topic", 1); + long latestOffset = companion.offsets().get(topicPartition, OffsetSpec.latest()).offset(); + // end::offsets[] + } + + @Test + public void consumerGroups() { + // tag::consumerGroups[] + Collection consumerGroups = companion.consumerGroups().list(); + for (ConsumerGroupListing consumerGroup : consumerGroups) { + // check consumer groups + } + + TopicPartition topicPartition = KafkaCompanion.tp("topic", 1); + companion.consumerGroups().resetOffsets("consumer-group", topicPartition); + // end::consumerGroups[] + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/companion/ProducerTest.java b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/companion/ProducerTest.java new file mode 100644 index 000000000..a60561829 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/companion/ProducerTest.java @@ -0,0 +1,34 @@ +package kafka.companion; + +import org.apache.kafka.clients.producer.ProducerRecord; +import org.junit.jupiter.api.Test; + +import io.smallrye.reactive.messaging.kafka.companion.test.KafkaCompanionTestBase; + +public class ProducerTest extends KafkaCompanionTestBase { + + @Test + public void fromRecords() { + // tag::records[] + companion.produce(byte[].class).fromRecords( + new ProducerRecord<>("topic1", "k1", "1".getBytes()), + new ProducerRecord<>("topic1", "k2", "2".getBytes()), + new ProducerRecord<>("topic1", "k3", "3".getBytes())); + // end::records[] + } + + @Test + public void fromGenerator() { + // tag::generator[] + companion.produceIntegers().usingGenerator(i -> new ProducerRecord<>("topic", i), 10); + // end::generator[] + } + + @Test + public void fromCsv() { + // tag::csv[] + companion.produceStrings().fromCsv("records.csv"); + // end::csv[] + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/companion/TopicsTest.java b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/companion/TopicsTest.java new file mode 100644 index 000000000..01e048611 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/companion/TopicsTest.java @@ -0,0 +1,20 @@ +package kafka.companion; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import io.smallrye.reactive.messaging.kafka.companion.test.KafkaCompanionTestBase; + +public class TopicsTest extends KafkaCompanionTestBase { + + @Test + public void topics() { + // tag::code[] + companion.topics().create("topic1", 1); + companion.topics().createAndWait("topic2", 3); + Assertions.assertEquals(companion.topics().list().size(), 2); + + companion.topics().delete("topic1"); + // end::code[] + } +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/inbound/Converters.java b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/inbound/Converters.java new file mode 100644 index 000000000..646c5ae54 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/inbound/Converters.java @@ -0,0 +1,30 @@ +package kafka.inbound; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.eclipse.microprofile.reactive.messaging.Incoming; + +import io.smallrye.reactive.messaging.kafka.Record; + +@ApplicationScoped +public class Converters { + + // tag::code[] + @Incoming("topic-a") + public void consume(Record record) { + String key = record.key(); // Can be `null` if the incoming record has no key + String value = record.value(); // Can be `null` if the incoming record has no value + } + + @Incoming("topic-b") + public void consume(ConsumerRecord record) { + String key = record.key(); // Can be `null` if the incoming record has no key + String value = record.value(); // Can be `null` if the incoming record has no value + String topic = record.topic(); + int partition = record.partition(); + // ... + } + // end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/inbound/KafkaCheckpointExample.java b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/inbound/KafkaCheckpointExample.java new file mode 100644 index 000000000..9ae1f1ecb --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/inbound/KafkaCheckpointExample.java @@ -0,0 +1,32 @@ +package kafka.inbound; + +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.kafka.commit.CheckpointMetadata; + +@Deprecated(forRemoval = true) +@ApplicationScoped +public class KafkaCheckpointExample { + + // tag::code[] + @Incoming("prices") + public CompletionStage consume(Message record) { + // Get the `CheckpointMetadata` from the incoming message + CheckpointMetadata checkpoint = CheckpointMetadata.fromMessage(record); + + // `CheckpointMetadata` allows transforming the processing state + // Applies the given function, starting from the value `0.0` when no previous state exists + checkpoint.transform(0.0, current -> current + record.getPayload(), /* persistOnAck */ true); + + // `persistOnAck` flag set to true, ack will persist the processing state + // associated with the latest offset (per partition). + return record.ack(); + } + // end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/inbound/KafkaCustomCommit.java b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/inbound/KafkaCustomCommit.java new file mode 100644 index 000000000..f57e3b87a --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/inbound/KafkaCustomCommit.java @@ -0,0 +1,60 @@ +package kafka.inbound; + +import java.util.Collection; +import java.util.function.BiConsumer; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.apache.kafka.common.TopicPartition; + +import io.smallrye.common.annotation.Identifier; +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.kafka.IncomingKafkaRecord; +import io.smallrye.reactive.messaging.kafka.KafkaConnectorIncomingConfiguration; +import io.smallrye.reactive.messaging.kafka.KafkaConsumer; +import io.smallrye.reactive.messaging.kafka.commit.KafkaCommitHandler; +import io.vertx.mutiny.core.Vertx; + +public class KafkaCustomCommit implements KafkaCommitHandler { + + @Override + public Uni handle(IncomingKafkaRecord record) { + // called on message ack + return Uni.createFrom().voidItem(); + } + + @Override + public Uni> received(IncomingKafkaRecord record) { + // called before message processing + return Uni.createFrom().item(record); + } + + @Override + public void terminate(boolean graceful) { + // called on channel shutdown + } + + @Override + public void partitionsAssigned(Collection partitions) { + // called on partitions assignment + } + + @Override + public void partitionsRevoked(Collection partitions) { + // called on partitions revoked + } + + @ApplicationScoped + @Identifier("custom") + public static class Factory implements KafkaCommitHandler.Factory { + + @Override + public KafkaCommitHandler create(KafkaConnectorIncomingConfiguration config, + Vertx vertx, + KafkaConsumer consumer, + BiConsumer reportFailure) { + return new KafkaCustomCommit(/* ... */); + } + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/inbound/KafkaDeadLetterExample.java b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/inbound/KafkaDeadLetterExample.java new file mode 100644 index 000000000..82cb5af95 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/inbound/KafkaDeadLetterExample.java @@ -0,0 +1,30 @@ +package kafka.inbound; + +import java.nio.charset.StandardCharsets; +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.apache.kafka.common.header.internals.RecordHeaders; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Metadata; + +import io.smallrye.reactive.messaging.kafka.api.OutgoingKafkaRecordMetadata; + +@ApplicationScoped +public class KafkaDeadLetterExample { + + // tag::code[] + @Incoming("in") + public CompletionStage consume(Message message) { + return message.nack(new Exception("Failed!"), Metadata.of( + OutgoingKafkaRecordMetadata.builder() + .withKey("failed-record") + .withHeaders(new RecordHeaders() + .add("my-header", "my-header-value".getBytes(StandardCharsets.UTF_8))) + .build())); + } + // end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/inbound/KafkaMetadataExample.java b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/inbound/KafkaMetadataExample.java new file mode 100644 index 000000000..8400fe0df --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/inbound/KafkaMetadataExample.java @@ -0,0 +1,36 @@ +package kafka.inbound; + +import java.time.Instant; + +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.kafka.api.IncomingKafkaRecordMetadata; + +public class KafkaMetadataExample { + + @SuppressWarnings("unchecked") + public void metadata() { + Message incoming = Message.of(12.0); + // tag::code[] + IncomingKafkaRecordMetadata metadata = incoming.getMetadata(IncomingKafkaRecordMetadata.class) + .orElse(null); + if (metadata != null) { + // The topic + String topic = metadata.getTopic(); + + // The key + String key = metadata.getKey(); + + // The timestamp + Instant timestamp = metadata.getTimestamp(); + + // The underlying record + ConsumerRecord record = metadata.getRecord(); + + // ... + } + // end::code[] + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/inbound/KafkaPriceConsumer.java b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/inbound/KafkaPriceConsumer.java new file mode 100644 index 000000000..7d6d28b94 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/inbound/KafkaPriceConsumer.java @@ -0,0 +1,15 @@ +package kafka.inbound; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; + +@ApplicationScoped +public class KafkaPriceConsumer { + + @Incoming("prices") + public void consume(double price) { + // process your price. + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/inbound/KafkaPriceMessageConsumer.java b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/inbound/KafkaPriceMessageConsumer.java new file mode 100644 index 000000000..324b7b94f --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/inbound/KafkaPriceMessageConsumer.java @@ -0,0 +1,21 @@ +package kafka.inbound; + +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; + +@ApplicationScoped +public class KafkaPriceMessageConsumer { + + @Incoming("prices") + public CompletionStage consume(Message price) { + // process your price. + + // Acknowledge the incoming message (commit the offset) + return price.ack(); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/inbound/KafkaRebalancedConsumer.java b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/inbound/KafkaRebalancedConsumer.java new file mode 100644 index 000000000..814461dc2 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/inbound/KafkaRebalancedConsumer.java @@ -0,0 +1,22 @@ +package kafka.inbound; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Acknowledgment; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; + +@ApplicationScoped +public class KafkaRebalancedConsumer { + + @Incoming("rebalanced-example") + @Acknowledgment(Acknowledgment.Strategy.NONE) + public CompletionStage consume(Message message) { + // We don't need to ACK messages because in this example we set offset during consumer re-balance + return CompletableFuture.completedFuture(null); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/inbound/KafkaRebalancedConsumerRebalanceListener.java b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/inbound/KafkaRebalancedConsumerRebalanceListener.java new file mode 100644 index 000000000..512b65117 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/inbound/KafkaRebalancedConsumerRebalanceListener.java @@ -0,0 +1,49 @@ +package kafka.inbound; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Logger; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.apache.kafka.clients.consumer.Consumer; +import org.apache.kafka.clients.consumer.OffsetAndTimestamp; + +import io.smallrye.common.annotation.Identifier; +import io.smallrye.reactive.messaging.kafka.KafkaConsumerRebalanceListener; + +@ApplicationScoped +@Identifier("rebalanced-example.rebalancer") +public class KafkaRebalancedConsumerRebalanceListener implements KafkaConsumerRebalanceListener { + + private static final Logger LOGGER = Logger.getLogger(KafkaRebalancedConsumerRebalanceListener.class.getName()); + + /** + * When receiving a list of partitions will search for the earliest offset within 10 minutes + * and seek the consumer to it. + * + * @param consumer underlying consumer + * @param partitions set of assigned topic partitions + */ + @Override + public void onPartitionsAssigned(Consumer consumer, + Collection partitions) { + long now = System.currentTimeMillis(); + long shouldStartAt = now - 600_000L; //10 minute ago + + Map request = new HashMap<>(); + for (org.apache.kafka.common.TopicPartition partition : partitions) { + LOGGER.info("Assigned " + partition); + request.put(partition, shouldStartAt); + } + Map offsets = consumer + .offsetsForTimes(request); + for (Map.Entry position : offsets.entrySet()) { + long target = position.getValue() == null ? 0L : position.getValue().offset(); + LOGGER.info("Seeking position " + target + " for " + position.getKey()); + consumer.seek(position.getKey(), target); + } + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/inbound/KafkaRecordBatchExample.java b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/inbound/KafkaRecordBatchExample.java new file mode 100644 index 000000000..85d329af6 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/inbound/KafkaRecordBatchExample.java @@ -0,0 +1,60 @@ +package kafka.inbound; + +import java.util.List; +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.clients.consumer.ConsumerRecords; +import org.apache.kafka.common.TopicPartition; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.TracingMetadata; +import io.smallrye.reactive.messaging.kafka.api.IncomingKafkaRecordBatchMetadata; + +@ApplicationScoped +public class KafkaRecordBatchExample { + + // tag::code[] + @Incoming("prices") + public CompletionStage consumeMessage(Message> msg) { + IncomingKafkaRecordBatchMetadata batchMetadata = msg.getMetadata(IncomingKafkaRecordBatchMetadata.class) + .get(); + for (ConsumerRecord record : batchMetadata.getRecords()) { + int partition = record.partition(); + long offset = record.offset(); + long timestamp = record.timestamp(); + } + // ack will commit the latest offsets (per partition) of the batch. + return msg.ack(); + } + + @Incoming("prices") + public void consumeRecords(ConsumerRecords records) { + for (TopicPartition partition : records.partitions()) { + for (ConsumerRecord record : records.records(partition)) { + //... process messages + } + } + } + // end::code[] + + // tag::batch[] + @Incoming("prices") + public void consumeRecords(ConsumerRecords records, + IncomingKafkaRecordBatchMetadata metadata) { + for (TopicPartition partition : records.partitions()) { + for (ConsumerRecord record : records.records(partition)) { + TracingMetadata tracing = metadata.getMetadataForRecord(record, TracingMetadata.class); + if (tracing != null) { + tracing.getCurrentContext().makeCurrent(); + } + //... process messages + } + } + } + // end::batch[] + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/inbound/KafkaRecordBatchPayloadExample.java b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/inbound/KafkaRecordBatchPayloadExample.java new file mode 100644 index 000000000..9a3d11100 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/inbound/KafkaRecordBatchPayloadExample.java @@ -0,0 +1,21 @@ +package kafka.inbound; + +import java.util.List; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; + +@ApplicationScoped +public class KafkaRecordBatchPayloadExample { + + // tag::code[] + @Incoming("prices") + public void consume(List prices) { + for (double price : prices) { + // process price + } + } + // end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/inbound/KafkaShareGroupAckExample.java b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/inbound/KafkaShareGroupAckExample.java new file mode 100644 index 000000000..2ee56bd59 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/inbound/KafkaShareGroupAckExample.java @@ -0,0 +1,29 @@ +package kafka.inbound; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; + +import io.smallrye.reactive.messaging.kafka.queues.ShareGroupAcknowledgement; + +@ApplicationScoped +public class KafkaShareGroupAckExample { + + // tag::code[] + @Incoming("queue") + public void consume(double price, ShareGroupAcknowledgement ack) { + if (price < 0) { + ack.reject(); // Permanently reject the record + } else if (needsRetry(price)) { + ack.release(); // Release for re-delivery to another consumer + } else { + ack.accept(); // Mark as successfully processed + } + } + // end::code[] + + private boolean needsRetry(double price) { + return false; + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/inbound/KafkaShareGroupBatchAckExample.java b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/inbound/KafkaShareGroupBatchAckExample.java new file mode 100644 index 000000000..383d4ec6c --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/inbound/KafkaShareGroupBatchAckExample.java @@ -0,0 +1,30 @@ +package kafka.inbound; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.clients.consumer.ConsumerRecords; +import org.eclipse.microprofile.reactive.messaging.Incoming; + +import io.smallrye.reactive.messaging.kafka.api.IncomingKafkaRecordBatchMetadata; +import io.smallrye.reactive.messaging.kafka.queues.ShareGroupAcknowledgement; + +@ApplicationScoped +public class KafkaShareGroupBatchAckExample { + + // tag::code[] + @Incoming("queue") + public void consume(ConsumerRecords records, + IncomingKafkaRecordBatchMetadata batchMetadata) { + for (ConsumerRecord record : records) { + ShareGroupAcknowledgement ack = batchMetadata.getMetadataForRecord(record, ShareGroupAcknowledgement.class); + if (record.value() < 0) { + ack.reject(); // Reject invalid records + } else { + ack.accept(); // Accept valid records + } + } + } + // end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/inbound/KafkaShareGroupConsumer.java b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/inbound/KafkaShareGroupConsumer.java new file mode 100644 index 000000000..5df915512 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/inbound/KafkaShareGroupConsumer.java @@ -0,0 +1,15 @@ +package kafka.inbound; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; + +@ApplicationScoped +public class KafkaShareGroupConsumer { + + @Incoming("queue") + public void consume(double price) { + // process your price. + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/inbound/KafkaShareGroupNackExample.java b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/inbound/KafkaShareGroupNackExample.java new file mode 100644 index 000000000..ffebef1c2 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/inbound/KafkaShareGroupNackExample.java @@ -0,0 +1,41 @@ +package kafka.inbound; + +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.apache.kafka.clients.consumer.AcknowledgeType; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Metadata; + +import io.smallrye.reactive.messaging.kafka.queues.ShareGroupAcknowledgement; + +@ApplicationScoped +public class KafkaShareGroupNackExample { + + // tag::code[] + @Incoming("queue") + public CompletionStage consume(Message msg) { + try { + process(msg.getPayload()); + return msg.ack(); + } catch (TransientException e) { + // Release for re-delivery + return msg.nack(e, + Metadata.of(ShareGroupAcknowledgement.from(AcknowledgeType.RELEASE))); + } catch (Exception e) { + // Reject permanently + return msg.nack(e, + Metadata.of(ShareGroupAcknowledgement.from(AcknowledgeType.REJECT))); + } + } + // end::code[] + + private void process(double price) { + } + + private static class TransientException extends RuntimeException { + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/inbound/MyCheckpointStateStore.java b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/inbound/MyCheckpointStateStore.java new file mode 100644 index 000000000..5c33ce0c1 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/inbound/MyCheckpointStateStore.java @@ -0,0 +1,69 @@ +package kafka.inbound; + +import java.util.Collection; +import java.util.Map; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.apache.kafka.clients.consumer.ConsumerConfig; +import org.apache.kafka.common.TopicPartition; + +import io.smallrye.common.annotation.Identifier; +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.kafka.KafkaConnectorIncomingConfiguration; +import io.smallrye.reactive.messaging.kafka.KafkaConsumer; +import io.smallrye.reactive.messaging.kafka.commit.CheckpointStateStore; +import io.smallrye.reactive.messaging.kafka.commit.ProcessingState; +import io.vertx.mutiny.core.Vertx; + +@Deprecated(forRemoval = true) +public class MyCheckpointStateStore implements CheckpointStateStore { + + private final String consumerGroupId; + private final Class stateType; + + public MyCheckpointStateStore(String consumerGroupId, Class stateType) { + this.consumerGroupId = consumerGroupId; + this.stateType = stateType; + } + + /** + * Can be used with + * {@code mp.reactive.messaging.incoming.my-channel.commit-strategy=checkpoint} + * {@code mp.reactive.messaging.incoming.my-channel.checkpoint.state-store=my-store} + */ + @ApplicationScoped + @Identifier("my-store") + public static class Factory implements CheckpointStateStore.Factory { + + @Override + public CheckpointStateStore create(KafkaConnectorIncomingConfiguration config, + Vertx vertx, + KafkaConsumer consumer, + Class stateType /* if configured, otherwise null */) { + String consumerGroupId = (String) consumer.configuration().get(ConsumerConfig.GROUP_ID_CONFIG); + return new MyCheckpointStateStore(consumerGroupId, stateType); + } + } + + @Override + public Uni>> fetchProcessingState(Collection partitions) { + // Called on Vert.x event loop + // Return a Uni completing with the map of topic-partition to processing state + // The Uni will be subscribed also on Vert.x event loop + return Uni.createFrom().nullItem(); + } + + @Override + public Uni persistProcessingState(Map> state) { + // Called on Vert.x event loop + // Return a Uni completing with void when the given states are persisted + // The Uni will be subscribed also on Vert.x event loop + return Uni.createFrom().voidItem(); + } + + @Override + public void close() { + /* Called when channel is closing, no-op by default */ + } +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/outbound/KafkaCloudEventMultipleProcessors.java b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/outbound/KafkaCloudEventMultipleProcessors.java new file mode 100644 index 000000000..17cea321d --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/outbound/KafkaCloudEventMultipleProcessors.java @@ -0,0 +1,39 @@ +package kafka.outbound; + +import java.net.URI; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.reactive.messaging.ce.OutgoingCloudEventMetadata; + +@ApplicationScoped +public class KafkaCloudEventMultipleProcessors { + + @Incoming("source") + @Outgoing("processed") + public Message process(Message in) { + return in.addMetadata(OutgoingCloudEventMetadata.builder() + .withId("id-" + in.getPayload()) + .withType("greeting") + .build()); + } + + @SuppressWarnings("unchecked") + @Incoming("processed") + @Outgoing("cloud-events") + public Message process2(Message in) { + OutgoingCloudEventMetadata metadata = in + .getMetadata(OutgoingCloudEventMetadata.class) + .orElseGet(() -> OutgoingCloudEventMetadata.builder().build()); + + return in.addMetadata(OutgoingCloudEventMetadata.from(metadata) + .withSource(URI.create("source://me")) + .withSubject("test") + .build()); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/outbound/KafkaCloudEventProcessor.java b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/outbound/KafkaCloudEventProcessor.java new file mode 100644 index 000000000..8b5d4d91c --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/outbound/KafkaCloudEventProcessor.java @@ -0,0 +1,25 @@ +package kafka.outbound; + +import java.net.URI; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.reactive.messaging.ce.OutgoingCloudEventMetadata; + +@ApplicationScoped +public class KafkaCloudEventProcessor { + + @Outgoing("cloud-events") + public Message toCloudEvents(Message in) { + return in.addMetadata(OutgoingCloudEventMetadata.builder() + .withId("id-" + in.getPayload()) + .withType("greetings") + .withSource(URI.create("http://example.com")) + .withSubject("greeting-message") + .build()); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/outbound/KafkaCustomHeaderReplier.java b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/outbound/KafkaCustomHeaderReplier.java new file mode 100644 index 000000000..9612a1c60 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/outbound/KafkaCustomHeaderReplier.java @@ -0,0 +1,39 @@ +package kafka.outbound; + +import java.util.Random; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.apache.kafka.common.header.Header; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.reactive.messaging.kafka.reply.KafkaRequestReply; + +@ApplicationScoped +public class KafkaCustomHeaderReplier { + + Random rand = new Random(); + + @Incoming("request") + @Outgoing("reply") + ProducerRecord process(ConsumerRecord request) { + Header topicHeader = request.headers().lastHeader("MY_TOPIC"); + if (topicHeader == null) { + // Skip + return null; + } + String myTopic = new String(topicHeader.value()); + int generateValue = rand.nextInt(100); + Header partitionHeader = request.headers().lastHeader("MY_PARTITION"); + if (partitionHeader == null) { + // Propagate incoming headers, including the correlation id header + return new ProducerRecord<>(myTopic, null, request.key(), generateValue, request.headers()); + } + // Send the replies to extracted myTopic-myPartition + int myPartition = KafkaRequestReply.replyPartitionFromBytes(partitionHeader.value()); + return new ProducerRecord<>(myTopic, myPartition, request.key(), generateValue, request.headers()); + } +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/outbound/KafkaExactlyOnceProcessor.java b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/outbound/KafkaExactlyOnceProcessor.java new file mode 100644 index 000000000..49b973232 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/outbound/KafkaExactlyOnceProcessor.java @@ -0,0 +1,34 @@ +package kafka.outbound; + +import java.util.List; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.kafka.KafkaRecord; +import io.smallrye.reactive.messaging.kafka.Record; +import io.smallrye.reactive.messaging.kafka.transactions.KafkaTransactions; + +@ApplicationScoped +public class KafkaExactlyOnceProcessor { + + @Inject + @Channel("tx-out-example") + KafkaTransactions txProducer; + + @Incoming("in-channel") + public Uni emitInTransaction(Message>> batch) { + return txProducer.withTransaction(batch, emitter -> { + for (Record record : batch.getPayload()) { + emitter.send(KafkaRecord.of(record.key(), record.value() + 1)); + } + return Uni.createFrom().voidItem(); + }); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/outbound/KafkaOutboundDynamicTopicExample.java b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/outbound/KafkaOutboundDynamicTopicExample.java new file mode 100644 index 000000000..cca404431 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/outbound/KafkaOutboundDynamicTopicExample.java @@ -0,0 +1,25 @@ +package kafka.outbound; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.kafka.api.OutgoingKafkaRecordMetadata; + +public class KafkaOutboundDynamicTopicExample { + + public Message metadata(Message incoming) { + // tag::code[] + String topicName = selectTopicFromIncommingMessage(incoming); + OutgoingKafkaRecordMetadata metadata = OutgoingKafkaRecordMetadata. builder() + .withTopic(topicName) + .build(); + + // Create a new message from the `incoming` message + // Add `metadata` to the metadata from the `incoming` message. + return incoming.addMetadata(metadata); + // end::code[] + } + + private String selectTopicFromIncommingMessage(Message incoming) { + return "fake"; + } +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/outbound/KafkaOutboundMetadataExample.java b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/outbound/KafkaOutboundMetadataExample.java new file mode 100644 index 000000000..645f290d2 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/outbound/KafkaOutboundMetadataExample.java @@ -0,0 +1,25 @@ +package kafka.outbound; + +import org.apache.kafka.common.header.internals.RecordHeaders; +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.kafka.api.OutgoingKafkaRecordMetadata; + +public class KafkaOutboundMetadataExample { + + public Message metadata(Message incoming) { + // tag::code[] + // Creates an OutgoingKafkaRecordMetadata + // The type parameter is the type of the record's key + OutgoingKafkaRecordMetadata metadata = OutgoingKafkaRecordMetadata. builder() + .withKey("my-key") + .withHeaders(new RecordHeaders().add("my-header", "value".getBytes())) + .build(); + + // Create a new message from the `incoming` message + // Add `metadata` to the metadata from the `incoming` message. + return incoming.addMetadata(metadata); + // end::code[] + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/outbound/KafkaPooledExactlyOnceProcessor.java b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/outbound/KafkaPooledExactlyOnceProcessor.java new file mode 100644 index 000000000..8bdf4142c --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/outbound/KafkaPooledExactlyOnceProcessor.java @@ -0,0 +1,32 @@ +package kafka.outbound; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Incoming; + +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.annotations.Blocking; +import io.smallrye.reactive.messaging.kafka.KafkaRecord; +import io.smallrye.reactive.messaging.kafka.api.IncomingKafkaRecordMetadata; +import io.smallrye.reactive.messaging.kafka.transactions.KafkaTransactions; + +@ApplicationScoped +public class KafkaPooledExactlyOnceProcessor { + + @Inject + @Channel("tx-out-example") + KafkaTransactions txProducer; + + @Incoming("in-channel") + @Blocking(ordered = false) + public void process(ConsumerRecord record, IncomingKafkaRecordMetadata metadata) { + txProducer.withTransactionAndAwait(metadata, emitter -> { + emitter.send(KafkaRecord.of(record.key(), record.value() + 1)); + return Uni.createFrom().voidItem(); + }); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/outbound/KafkaPriceMessageProducer.java b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/outbound/KafkaPriceMessageProducer.java new file mode 100644 index 000000000..52e86b093 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/outbound/KafkaPriceMessageProducer.java @@ -0,0 +1,26 @@ +package kafka.outbound; + +import java.time.Duration; +import java.util.Random; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +@ApplicationScoped +public class KafkaPriceMessageProducer { + + private final Random random = new Random(); + + @Outgoing("prices-out") + public Multi> generate() { + // Build an infinite stream of random prices + // It emits a price every second + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .map(x -> Message.of(random.nextDouble())); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/outbound/KafkaPriceProcessor.java b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/outbound/KafkaPriceProcessor.java new file mode 100644 index 000000000..7d423ae89 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/outbound/KafkaPriceProcessor.java @@ -0,0 +1,19 @@ +package kafka.outbound; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +@ApplicationScoped +public class KafkaPriceProcessor { + + // tag::code[] + @Incoming("in") + @Outgoing("out") + public double process(int in) { + return in * 0.88; + } + // end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/outbound/KafkaPriceProducer.java b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/outbound/KafkaPriceProducer.java new file mode 100644 index 000000000..220a0214f --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/outbound/KafkaPriceProducer.java @@ -0,0 +1,25 @@ +package kafka.outbound; + +import java.time.Duration; +import java.util.Random; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +@ApplicationScoped +public class KafkaPriceProducer { + + private final Random random = new Random(); + + @Outgoing("prices-out") + public Multi generate() { + // Build an infinite stream of random prices + // It emits a price every second + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .map(x -> random.nextDouble()); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/outbound/KafkaProducerRecordExample.java b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/outbound/KafkaProducerRecordExample.java new file mode 100644 index 000000000..0fb3f3dc4 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/outbound/KafkaProducerRecordExample.java @@ -0,0 +1,18 @@ +package kafka.outbound; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.apache.kafka.clients.producer.ProducerRecord; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +@ApplicationScoped +public class KafkaProducerRecordExample { + + // tag::code[] + @Outgoing("out") + public ProducerRecord generate() { + return new ProducerRecord<>("my-topic", "key", "value"); + } + // end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/outbound/KafkaRecordExample.java b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/outbound/KafkaRecordExample.java new file mode 100644 index 000000000..560850605 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/outbound/KafkaRecordExample.java @@ -0,0 +1,21 @@ +package kafka.outbound; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.reactive.messaging.kafka.Record; + +@ApplicationScoped +public class KafkaRecordExample { + + // tag::code[] + @Incoming("in") + @Outgoing("out") + public Record process(String in) { + return Record.of("my-key", in); + } + // end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/outbound/KafkaReplier.java b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/outbound/KafkaReplier.java new file mode 100644 index 000000000..8bad449e8 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/outbound/KafkaReplier.java @@ -0,0 +1,20 @@ +package kafka.outbound; + +import java.util.Random; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +@ApplicationScoped +public class KafkaReplier { + + Random rand = new Random(); + + @Incoming("request") + @Outgoing("reply") + int handleRequest(String request) { + return rand.nextInt(100); + } +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/outbound/KafkaRequestReplyEmitter.java b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/outbound/KafkaRequestReplyEmitter.java new file mode 100644 index 000000000..c1994d090 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/outbound/KafkaRequestReplyEmitter.java @@ -0,0 +1,21 @@ +package kafka.outbound; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; + +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.kafka.reply.KafkaRequestReply; + +@ApplicationScoped +public class KafkaRequestReplyEmitter { + + @Inject + @Channel("my-request") + KafkaRequestReply quoteRequest; + + public Uni requestQuote(String request) { + return quoteRequest.request(request); + } +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/outbound/KafkaRequestReplyMessageEmitter.java b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/outbound/KafkaRequestReplyMessageEmitter.java new file mode 100644 index 000000000..e70379b62 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/outbound/KafkaRequestReplyMessageEmitter.java @@ -0,0 +1,26 @@ +package kafka.outbound; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.kafka.api.OutgoingKafkaRecordMetadata; +import io.smallrye.reactive.messaging.kafka.reply.KafkaRequestReply; + +@ApplicationScoped +public class KafkaRequestReplyMessageEmitter { + + @Inject + @Channel("my-request") + KafkaRequestReply quoteRequest; + + public Uni> requestMessage(String request) { + return quoteRequest.request(Message.of(request) + .addMetadata(OutgoingKafkaRecordMetadata.builder() + .withKey(request) + .build())); + } +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/outbound/KafkaRequestReplyMultiEmitter.java b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/outbound/KafkaRequestReplyMultiEmitter.java new file mode 100644 index 000000000..8d8897ba1 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/outbound/KafkaRequestReplyMultiEmitter.java @@ -0,0 +1,21 @@ +package org.acme; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; + +import io.smallrye.mutiny.Multi; +import io.smallrye.reactive.messaging.kafka.reply.KafkaRequestReply; + +@ApplicationScoped +public class KafkaRequestReplyMultiEmitter { + + @Inject + @Channel("my-request") + KafkaRequestReply quoteRequest; + + public Multi requestQuote(String request) { + return quoteRequest.requestMulti(request).select().first(5); + } +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/outbound/KafkaTransactionalProducer.java b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/outbound/KafkaTransactionalProducer.java new file mode 100644 index 000000000..597c33cc2 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/outbound/KafkaTransactionalProducer.java @@ -0,0 +1,28 @@ +package kafka.outbound; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; + +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.kafka.KafkaRecord; +import io.smallrye.reactive.messaging.kafka.transactions.KafkaTransactions; + +@ApplicationScoped +public class KafkaTransactionalProducer { + + @Inject + @Channel("tx-out-example") + KafkaTransactions txProducer; + + public Uni emitInTransaction() { + return txProducer.withTransaction(emitter -> { + emitter.send(KafkaRecord.of(1, "a")); + emitter.send(KafkaRecord.of(2, "b")); + emitter.send(KafkaRecord.of(3, "c")); + return Uni.createFrom().voidItem(); + }); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/outbound/MyReplyFailureHandler.java b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/outbound/MyReplyFailureHandler.java new file mode 100644 index 000000000..fcd7f8f9c --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/outbound/MyReplyFailureHandler.java @@ -0,0 +1,23 @@ +package kafka.outbound; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.apache.kafka.common.header.Header; + +import io.smallrye.common.annotation.Identifier; +import io.smallrye.reactive.messaging.kafka.KafkaRecord; +import io.smallrye.reactive.messaging.kafka.reply.ReplyFailureHandler; + +@ApplicationScoped +@Identifier("my-reply-error") +public class MyReplyFailureHandler implements ReplyFailureHandler { + + @Override + public Throwable handleReply(KafkaRecord replyRecord) { + Header header = replyRecord.getHeaders().lastHeader("REPLY_ERROR"); + if (header != null) { + return new IllegalArgumentException(new String(header.value())); + } + return null; + } +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/outbound/ProducerInterceptorBeanExample.java b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/outbound/ProducerInterceptorBeanExample.java new file mode 100644 index 000000000..a01e555cd --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/kafka/outbound/ProducerInterceptorBeanExample.java @@ -0,0 +1,37 @@ +package kafka.outbound; + +import java.util.Map; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.apache.kafka.clients.producer.ProducerInterceptor; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.apache.kafka.clients.producer.RecordMetadata; + +import io.smallrye.common.annotation.Identifier; + +@ApplicationScoped +@Identifier("my-producer-interceptor") +public class ProducerInterceptorBeanExample implements ProducerInterceptor { + + @Override + public ProducerRecord onSend(ProducerRecord producerRecord) { + // called before send + return producerRecord; + } + + @Override + public void onAcknowledgement(RecordMetadata recordMetadata, Exception e) { + // called on send acknowledgement callback + } + + @Override + public void close() { + // called on client close + } + + @Override + public void configure(Map map) { + // called on client configuration + } +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/keyed/KeyValueExtractorFromPayload.java b/content/docs/smallrye-reactive-messaging/latest/_examples/keyed/KeyValueExtractorFromPayload.java new file mode 100644 index 000000000..6191d081b --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/keyed/KeyValueExtractorFromPayload.java @@ -0,0 +1,35 @@ +package keyed; + +import java.lang.reflect.Type; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; + +// tag::code[] +import io.smallrye.reactive.messaging.keyed.KeyValueExtractor; + +@ApplicationScoped +public class KeyValueExtractorFromPayload implements KeyValueExtractor { + + @Override + public boolean canExtract(Message msg, Type keyType, Type valueType) { + // Called for the first message of the stream to select the extractor. + // Here we only check for the type, but the logic can be more complex + return keyType.equals(String.class) && valueType.equals(String.class); + } + + @Override + public String extractKey(Message message, Type keyType) { + String string = message.getPayload().toString(); + return string.substring(0, string.indexOf(":")); + } + + @Override + public String extractValue(Message message, Type valueType) { + String string = message.getPayload().toString(); + return string.substring(string.indexOf(":") + 1); + } + +} +// end::code[] diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/keyed/KeyedExample.java b/content/docs/smallrye-reactive-messaging/latest/_examples/keyed/KeyedExample.java new file mode 100644 index 000000000..438a0185e --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/keyed/KeyedExample.java @@ -0,0 +1,25 @@ +package keyed; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; +import io.smallrye.reactive.messaging.keyed.Keyed; +import io.smallrye.reactive.messaging.keyed.KeyedMulti; + +public class KeyedExample { + + // tag::code[] + @Incoming("in") + @Outgoing("out") + public Multi reshape( + @Keyed(KeyValueExtractorFromPayload.class) KeyedMulti multi) { + return multi.onItem().scan(AtomicInteger::new, (i, s) -> { + i.incrementAndGet(); + return i; + }).map(i -> Integer.toString(i.get())); + } + // end::code[] +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/keyed/KeyedMultiExample.java b/content/docs/smallrye-reactive-messaging/latest/_examples/keyed/KeyedMultiExample.java new file mode 100644 index 000000000..c917cc8d1 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/keyed/KeyedMultiExample.java @@ -0,0 +1,27 @@ +package keyed; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; +import io.smallrye.reactive.messaging.keyed.KeyedMulti; + +public class KeyedMultiExample { + + // tag::code[] + @Incoming("in") + @Outgoing("out") + public Multi reshape(KeyedMulti multi) { + // Called once per key and receive the stream of value for that specific key + String key = multi.key(); + return multi.onItem().scan(AtomicInteger::new, (i, s) -> { + i.incrementAndGet(); + return i; + }) + .map(i -> Integer.toString(i.get())); + } + // end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/merge/MergeExamples.java b/content/docs/smallrye-reactive-messaging/latest/_examples/merge/MergeExamples.java new file mode 100644 index 000000000..fbf4d4ac8 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/merge/MergeExamples.java @@ -0,0 +1,31 @@ +package merge; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.reactive.messaging.annotations.Merge; + +public class MergeExamples { + + // tag::chain[] + @Incoming("in1") + @Outgoing("out") + public int increment(int i) { + return i + 1; + } + + @Incoming("in2") + @Outgoing("out") + public int multiply(int i) { + return i * 2; + } + + @Incoming("out") + @Merge + public void getAll(int i) { + //... + } + + // end::chain[] + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/messages/MessageExamples.java b/content/docs/smallrye-reactive-messaging/latest/_examples/messages/MessageExamples.java new file mode 100644 index 000000000..15dbf7e0f --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/messages/MessageExamples.java @@ -0,0 +1,96 @@ +package messages; + +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Metadata; + +@SuppressWarnings({ "unused" }) +public class MessageExamples { + + public void example1() { + Message message = Message.of("hello", Metadata.of(new MyMetadata())); + // tag::message[] + String payload = message.getPayload(); + Optional metadata = message.getMetadata(MyMetadata.class); + // end::message[] + } + + public void creation() { + Price price = new Price(20.5); + // tag::creation[] + // Create a simple message wrapping a payload + Message m1 = Message.of(price); + + // Create a message with metadata + Message m2 = Message.of(price, Metadata.of(new PriceMetadata())); + + // Create a message with several metadata + Message m3 = Message.of(price, + Metadata.of(new PriceMetadata(), new MyMetadata())); + + // Create a message with an acknowledgement callback + Message m4 = Message.of(price, () -> { + // Called when the message is acknowledged by the next consumer. + return CompletableFuture.completedFuture(null); + }); + + // Create a message with both metadata and acknowledgement callback + Message m5 = Message.of(price, + Metadata.of(new PriceMetadata()), + () -> { + // Called when the message is acknowledged by the next consumer. + return CompletableFuture.completedFuture(null); + }); + // end::creation[] + } + + public void copy() { + Price price = new Price(20.5); + Message message = Message.of(price); + // tag::copy[] + + // Create a new message with a new payload but with the same metadata + Message m1 = message.withPayload(new Price(12.4)); + + // Create a new message with a new payload and add another metadata + Message m2 = message + .withPayload(new Price(15.0)) + .withMetadata(Metadata.of(new PriceMetadata())); + + // Create a new message with a new payload and a custom acknowledgement + Message m3 = message + .withPayload(new Price(15.0)) + .withAck(() -> + // acknowledge the incoming message + message.ack() + .thenAccept(x -> { + // do something + })); + // end::copy[] + } + + public void nullPayload() { + // tag::null-payload[] + // Create a message with null payload + Message m6 = Message.of(null, Metadata.of(new PriceMetadata())); + // end::null-payload[] + } + + public static class Price { + final double price; + + public Price(double p) { + this.price = p; + } + } + + static class PriceMetadata { + final long timestamp; + + public PriceMetadata() { + this.timestamp = System.currentTimeMillis(); + } + } +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/mqtt/customization/ClientProducers.java b/content/docs/smallrye-reactive-messaging/latest/_examples/mqtt/customization/ClientProducers.java new file mode 100644 index 000000000..fbcaaea08 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/mqtt/customization/ClientProducers.java @@ -0,0 +1,32 @@ +package mqtt.customization; + +import jakarta.enterprise.inject.Produces; + +import io.smallrye.common.annotation.Identifier; +import io.smallrye.reactive.messaging.mqtt.session.MqttClientSessionOptions; +import io.vertx.core.net.PemKeyCertOptions; +import io.vertx.core.net.PemTrustOptions; + +public class ClientProducers { + + // tag::named[] + @Produces + @Identifier("my-options") + public MqttClientSessionOptions getOptions() { + // You can use the produced options to configure the TLS connection + PemKeyCertOptions keycert = new PemKeyCertOptions() + .addCertPath("./tls/tls.crt") + .addKeyPath("./tls/tls.key"); + PemTrustOptions trust = new PemTrustOptions().addCertPath("./tlc/ca.crt"); + + return new MqttClientSessionOptions() + .setSsl(true) + .setKeyCertOptions(keycert) + .setTrustOptions(trust) + .setHostnameVerificationAlgorithm("HTTPS") + .setConnectTimeout(30000) + .setReconnectInterval(5000); + } + // end::named[] + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/mqtt/inbound/MqttPriceConsumer.java b/content/docs/smallrye-reactive-messaging/latest/_examples/mqtt/inbound/MqttPriceConsumer.java new file mode 100644 index 000000000..9115d809a --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/mqtt/inbound/MqttPriceConsumer.java @@ -0,0 +1,17 @@ +package mqtt.inbound; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; + +@ApplicationScoped +public class MqttPriceConsumer { + + @Incoming("prices") + public void consume(byte[] raw) { + double price = Double.parseDouble(new String(raw)); + + // process your price. + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/mqtt/inbound/MqttPriceMessageConsumer.java b/content/docs/smallrye-reactive-messaging/latest/_examples/mqtt/inbound/MqttPriceMessageConsumer.java new file mode 100644 index 000000000..ed40c96db --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/mqtt/inbound/MqttPriceMessageConsumer.java @@ -0,0 +1,21 @@ +package mqtt.inbound; + +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; + +@ApplicationScoped +public class MqttPriceMessageConsumer { + + @Incoming("prices") + public CompletionStage consume(Message price) { + // process your price. + + // Acknowledge the incoming message + return price.ack(); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/mqtt/outbound/MqttPriceMessageProducer.java b/content/docs/smallrye-reactive-messaging/latest/_examples/mqtt/outbound/MqttPriceMessageProducer.java new file mode 100644 index 000000000..744ce54b5 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/mqtt/outbound/MqttPriceMessageProducer.java @@ -0,0 +1,26 @@ +package mqtt.outbound; + +import java.time.Duration; +import java.util.Random; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +@ApplicationScoped +public class MqttPriceMessageProducer { + + private Random random = new Random(); + + @Outgoing("prices") + public Multi> generate() { + // Build an infinite stream of random prices + // It emits a price every second + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .map(x -> Message.of(random.nextDouble())); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/mqtt/outbound/MqttPriceProducer.java b/content/docs/smallrye-reactive-messaging/latest/_examples/mqtt/outbound/MqttPriceProducer.java new file mode 100644 index 000000000..22c0998e9 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/mqtt/outbound/MqttPriceProducer.java @@ -0,0 +1,25 @@ +package mqtt.outbound; + +import java.time.Duration; +import java.util.Random; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +@ApplicationScoped +public class MqttPriceProducer { + + private Random random = new Random(); + + @Outgoing("prices") + public Multi generate() { + // Build an infinite stream of random prices + // It emits a price every second + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .map(x -> random.nextDouble()); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/observability/ContextMessageObservationCollector.java b/content/docs/smallrye-reactive-messaging/latest/_examples/observability/ContextMessageObservationCollector.java new file mode 100644 index 000000000..608e368dc --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/observability/ContextMessageObservationCollector.java @@ -0,0 +1,57 @@ +package observability; + +import java.time.Duration; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.observation.DefaultMessageObservation; +import io.smallrye.reactive.messaging.observation.MessageObservation; +import io.smallrye.reactive.messaging.observation.MessageObservationCollector; +import io.smallrye.reactive.messaging.observation.ObservationContext; + +@ApplicationScoped +public class ContextMessageObservationCollector + implements MessageObservationCollector { + + @Override + public MyContext initObservation(String channel, boolean incoming, boolean emitter) { + // Called on observation setup, per channel + // if returned null the observation for that channel is disabled + return new MyContext(channel, incoming, emitter); + } + + @Override + public MessageObservation onNewMessage(String channel, Message message, MyContext ctx) { + // Called after message has been created + return new DefaultMessageObservation(channel); + } + + public static class MyContext implements ObservationContext { + + private final String channel; + private final boolean incoming; + private final boolean emitter; + + public MyContext(String channel, boolean incoming, boolean emitter) { + this.channel = channel; + this.incoming = incoming; + this.emitter = emitter; + } + + @Override + public void complete(MessageObservation observation) { + // called after message processing has completed and observation is done + // register duration + Duration duration = observation.getCompletionDuration(); + Throwable reason = observation.getReason(); + if (reason != null) { + // message was nacked + } else { + // message was acked successfully + } + } + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/observability/SimpleMessageObservationCollector.java b/content/docs/smallrye-reactive-messaging/latest/_examples/observability/SimpleMessageObservationCollector.java new file mode 100644 index 000000000..838bceb5f --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/observability/SimpleMessageObservationCollector.java @@ -0,0 +1,21 @@ +package observability; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.observation.DefaultMessageObservation; +import io.smallrye.reactive.messaging.observation.MessageObservation; +import io.smallrye.reactive.messaging.observation.MessageObservationCollector; +import io.smallrye.reactive.messaging.observation.ObservationContext; + +@ApplicationScoped +public class SimpleMessageObservationCollector implements MessageObservationCollector { + + @Override + public MessageObservation onNewMessage(String channel, Message message, ObservationContext ctx) { + // Called after message has been created + return new DefaultMessageObservation(channel); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/outgoings/OutgoingsExample.java b/content/docs/smallrye-reactive-messaging/latest/_examples/outgoings/OutgoingsExample.java new file mode 100644 index 000000000..4e5bf44db --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/outgoings/OutgoingsExample.java @@ -0,0 +1,18 @@ +package outgoings; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +public class OutgoingsExample { + + //tag::code[] + @Incoming("in") + @Outgoing("out1") + @Outgoing("out2") + public String process(String s) { + // send messages from channel-in to both channel-out1 and channel-out2 + return s.toUpperCase(); + } + //end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/outgoings/SplitterMultiExample.java b/content/docs/smallrye-reactive-messaging/latest/_examples/outgoings/SplitterMultiExample.java new file mode 100644 index 000000000..550443c61 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/outgoings/SplitterMultiExample.java @@ -0,0 +1,37 @@ +package outgoings; + +import java.util.Objects; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.operators.multi.split.MultiSplitter; + +public class SplitterMultiExample { + + // tag::code[] + enum Caps { + ALL_CAPS, + ALL_LOW, + MIXED + } + + @Incoming("in") + @Outgoing("sink1") + @Outgoing("sink2") + @Outgoing("sink3") + public MultiSplitter reshape(Multi in) { + return in.split(Caps.class, s -> { + if (Objects.equals(s, s.toLowerCase())) { + return Caps.ALL_LOW; + } else if (Objects.equals(s, s.toUpperCase())) { + return Caps.ALL_CAPS; + } else { + return Caps.MIXED; + } + }); + } + // end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/outgoings/TargetedExample.java b/content/docs/smallrye-reactive-messaging/latest/_examples/outgoings/TargetedExample.java new file mode 100644 index 000000000..7835ef9d8 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/outgoings/TargetedExample.java @@ -0,0 +1,44 @@ +package outgoings; + +import java.util.Map; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.reactive.messaging.Messages; +import io.smallrye.reactive.messaging.Targeted; +import io.smallrye.reactive.messaging.TargetedMessages; + +public class TargetedExample { + + //tag::code[] + @Incoming("in") + @Outgoing("out1") + @Outgoing("out2") + @Outgoing("out3") + public Targeted process(double price) { + // send messages from channel-in to both channel-out1 and channel-out2 + Targeted targeted = Targeted.of("out1", "Price: " + price, + "out2", "Quote: " + price); + if (price > 90.0) { + return targeted.with("out3", price); + } + return targeted; + } + //end::code[] + + //tag::targeted-messages[] + @Incoming("channel-in") + @Outgoing("channel-out1") + @Outgoing("channel-out2") + @Outgoing("channel-out3") + public TargetedMessages processMessage(Message msg) { + // send messages from channel-in to both channel-out1 and channel-out2 + return Messages.chain(msg) + .with(Map.of("channel-out1", msg.withPayload(msg.getPayload().toUpperCase()), + "channel-out2", msg.withPayload(msg.getPayload().toLowerCase()))); + } + //end::targeted-messages[] + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/pausable/PausableController.java b/content/docs/smallrye-reactive-messaging/latest/_examples/pausable/PausableController.java new file mode 100644 index 000000000..f95612833 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/pausable/PausableController.java @@ -0,0 +1,46 @@ +package pausable; + +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Incoming; + +import io.smallrye.reactive.messaging.ChannelRegistry; +import io.smallrye.reactive.messaging.PausableChannel; + +@ApplicationScoped +public class PausableController { + + // Option 1: Inject ChannelRegistry and retrieve the pausable channel + @Inject + ChannelRegistry registry; + + // Option 2: Directly inject the pausable channel + @Inject + @Channel("my-channel") + PausableChannel myChannel; + + @PostConstruct + public void resume() { + // Wait for the application to be ready + // Option 1: Retrieve the pausable channel from the registry + PausableChannel pausable = registry.getPausable("my-channel"); + // Resume the processing of the messages + pausable.resume(); + } + + public void pause() { + // Option 2: Use the injected pausable channel + + // Pause the processing of the messages + myChannel.pause(); + } + + @Incoming("my-channel") + void process(String message) { + // Process the message + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/processing/ProcessingExamples.java b/content/docs/smallrye-reactive-messaging/latest/_examples/processing/ProcessingExamples.java new file mode 100644 index 000000000..548722816 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/processing/ProcessingExamples.java @@ -0,0 +1,72 @@ +package processing; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Uni; + +public class ProcessingExamples { + + // tag::process-message[] + @Incoming("in") + @Outgoing("out") + public Message processMessage(Message in) { + return in.withPayload(Integer.toString(in.getPayload())); + } + // end::process-message[] + + // tag::process-message-cs[] + @Incoming("in") + @Outgoing("out") + public CompletionStage> processMessageCS(Message in) { + CompletionStage cs = invokeService(in.getPayload()); + return cs.thenApply(in::withPayload); + } + // end::process-message-cs[] + + // tag::process-message-uni[] + @Incoming("in") + @Outgoing("out") + public Uni> processMessageUni(Message in) { + return invokeService(in.getPayload()) + .map(in::withPayload); + } + // end::process-message-uni[] + + // tag::process-payload[] + @Incoming("in") + @Outgoing("out") + public String processPayload(int in) { + return Integer.toString(in); + } + // end::process-payload[] + + // tag::process-payload-cs[] + @Incoming("in") + @Outgoing("out") + public CompletionStage processPayloadCS(int in) { + return invokeService(in); + } + // end::process-payload-cs[] + + // tag::process-payload-uni[] + @Incoming("in") + @Outgoing("out") + public Uni processPayload(String in) { + return invokeService(in); + } + // end::process-payload-uni[] + + private CompletionStage invokeService(int param) { + return CompletableFuture.completedFuture("ok"); + } + + private Uni invokeService(String param) { + return Uni.createFrom().item("ok"); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/processing/StreamExamples.java b/content/docs/smallrye-reactive-messaging/latest/_examples/processing/StreamExamples.java new file mode 100644 index 000000000..c33bfecd8 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/processing/StreamExamples.java @@ -0,0 +1,41 @@ +package processing; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; + +public class StreamExamples { + + // tag::processing-stream-message[] + @Incoming("in") + @Outgoing("out") + public Multi> processMessageStream(Multi> stream) { + return stream + .onItem().transformToUni(message -> invokeService(message.getPayload()) + .onFailure().recoverWithItem("fallback") + .onItem().transform(message::withPayload)) + .concatenate(); + + } + // end::processing-stream-message[] + + // tag::processing-stream-payload[] + @Incoming("in") + @Outgoing("out") + public Multi processPayloadStream(Multi stream) { + return stream + .onItem().transformToUni(payload -> invokeService(payload) + .onFailure().recoverWithItem("fallback")) + .concatenate(); + + } + // end::processing-stream-payload[] + + private Uni invokeService(int i) { + return Uni.createFrom().item("ok " + i); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/pulsar/configuration/PulsarClientConfigProducers.java b/content/docs/smallrye-reactive-messaging/latest/_examples/pulsar/configuration/PulsarClientConfigProducers.java new file mode 100644 index 000000000..89f1d31c5 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/pulsar/configuration/PulsarClientConfigProducers.java @@ -0,0 +1,66 @@ +package pulsar.configuration; + +import java.util.HashMap; +import java.util.Map; + +import jakarta.enterprise.inject.Produces; + +import org.apache.pulsar.client.api.BatcherBuilder; +import org.apache.pulsar.client.api.PulsarClientSharedResources; +import org.apache.pulsar.client.impl.AutoClusterFailover; +import org.apache.pulsar.client.impl.DefaultCryptoKeyReader; +import org.apache.pulsar.client.impl.conf.ClientConfigurationData; +import org.apache.pulsar.client.impl.conf.ConsumerConfigurationData; +import org.apache.pulsar.client.impl.customroute.PartialRoundRobinMessageRouterImpl; + +import io.smallrye.common.annotation.Identifier; + +public class PulsarClientConfigProducers { + + // tag::consumer[] + @Produces + @Identifier("my-consumer-options") + public ConsumerConfigurationData getConsumerConfig() { + ConsumerConfigurationData data = new ConsumerConfigurationData<>(); + data.setAckReceiptEnabled(true); + data.setCryptoKeyReader(DefaultCryptoKeyReader.builder() + //... + .build()); + return data; + } + // end::consumer[] + + // tag::client[] + @Produces + @Identifier("prices") + public ClientConfigurationData getClientConfig() { + ClientConfigurationData data = new ClientConfigurationData(); + data.setEnableTransaction(true); + data.setServiceUrlProvider(AutoClusterFailover.builder() + // ... + .build()); + return data; + } + // end::client[] + + // tag::shared-resources[] + @Produces + public PulsarClientSharedResources getSharedResources() { + return PulsarClientSharedResources.builder() + .configureEventLoop(config -> config.numberOfThreads(4)) + .build(); + } + // end::shared-resources[] + + // tag::producer[] + @Produces + @Identifier("prices") + public Map getProducerconfig() { + Map map = new HashMap<>(); + map.put("batcherBuilder", BatcherBuilder.KEY_BASED); + map.put("sendTimeoutMs", 3000); + map.put("customMessageRouter", new PartialRoundRobinMessageRouterImpl(4)); + return map; + } + // end::producer[] +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/pulsar/configuration/PulsarSchemaProvider.java b/content/docs/smallrye-reactive-messaging/latest/_examples/pulsar/configuration/PulsarSchemaProvider.java new file mode 100644 index 000000000..3fe6141d0 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/pulsar/configuration/PulsarSchemaProvider.java @@ -0,0 +1,30 @@ +package pulsar.configuration; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; + +import org.apache.pulsar.client.api.Schema; +import org.apache.pulsar.common.schema.KeyValue; +import org.apache.pulsar.common.schema.KeyValueEncodingType; + +import io.smallrye.common.annotation.Identifier; + +@ApplicationScoped +public class PulsarSchemaProvider { + + @Produces + @Identifier("user-schema") + Schema userSchema = Schema.AVRO(User.class); + + @Produces + @Identifier("a-channel") + Schema> keyValueSchema() { + return Schema.KeyValue(Schema.INT32, Schema.JSON(User.class), KeyValueEncodingType.SEPARATED); + } + + public static class User { + String name; + int age; + + } +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/pulsar/inbound/PulsarMessageBatchExample.java b/content/docs/smallrye-reactive-messaging/latest/_examples/pulsar/inbound/PulsarMessageBatchExample.java new file mode 100644 index 000000000..9ce9eead1 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/pulsar/inbound/PulsarMessageBatchExample.java @@ -0,0 +1,54 @@ +package pulsar.inbound; + +import java.util.List; +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.TracingMetadata; +import io.smallrye.reactive.messaging.pulsar.PulsarIncomingBatchMessageMetadata; + +@ApplicationScoped +public class PulsarMessageBatchExample { + + // tag::code[] + @Incoming("prices") + public CompletionStage consumeMessage(Message> messages) { + messages.getMetadata(PulsarIncomingBatchMessageMetadata.class).ifPresent(metadata -> { + for (org.apache.pulsar.client.api.Message message : metadata.getMessages()) { + String key = message.getKey(); + String topic = message.getTopicName(); + long timestamp = message.getEventTime(); + //... process messages + } + }); + // ack will commit the latest offsets (per partition) of the batch. + return messages.ack(); + } + + @Incoming("prices") + public void consumeMessages(org.apache.pulsar.client.api.Messages messages) { + for (org.apache.pulsar.client.api.Message msg : messages) { + //... process messages + } + } + // end::code[] + + // tag::batch[] + @Incoming("prices") + public void consumeMessages(org.apache.pulsar.client.api.Messages messages, + PulsarIncomingBatchMessageMetadata metadata) { + for (org.apache.pulsar.client.api.Message message : messages) { + TracingMetadata tracing = metadata.getMetadataForMessage(message, TracingMetadata.class); + if (tracing != null) { + tracing.getCurrentContext().makeCurrent(); + } + //... process messages + } + } + // end::batch[] + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/pulsar/inbound/PulsarMessageBatchPayloadExample.java b/content/docs/smallrye-reactive-messaging/latest/_examples/pulsar/inbound/PulsarMessageBatchPayloadExample.java new file mode 100644 index 000000000..649b7d1ae --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/pulsar/inbound/PulsarMessageBatchPayloadExample.java @@ -0,0 +1,21 @@ +package pulsar.inbound; + +import java.util.List; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; + +@ApplicationScoped +public class PulsarMessageBatchPayloadExample { + + // tag::code[] + @Incoming("prices") + public void consume(List prices) { + for (double price : prices) { + // process price + } + } + // end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/pulsar/inbound/PulsarMetadataExample.java b/content/docs/smallrye-reactive-messaging/latest/_examples/pulsar/inbound/PulsarMetadataExample.java new file mode 100644 index 000000000..a4add7ea1 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/pulsar/inbound/PulsarMetadataExample.java @@ -0,0 +1,35 @@ +package pulsar.inbound; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.pulsar.PulsarIncomingMessageMetadata; + +public class PulsarMetadataExample { + + @SuppressWarnings("unchecked") + public void metadata() { + Message incoming = Message.of(12.0); + // tag::code[] + PulsarIncomingMessageMetadata metadata = incoming.getMetadata(PulsarIncomingMessageMetadata.class).orElse(null); + if (metadata != null) { + // The topic + String topic = metadata.getTopicName(); + + // The key + String key = metadata.getKey(); + + // The event time + long timestamp = metadata.getEventTime(); + + // The raw data + byte[] rawData = metadata.getData(); + + // The underlying message + org.apache.pulsar.client.api.Message message = metadata.getMessage(); + + // ... + } + // end::code[] + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/pulsar/inbound/PulsarPriceConsumer.java b/content/docs/smallrye-reactive-messaging/latest/_examples/pulsar/inbound/PulsarPriceConsumer.java new file mode 100644 index 000000000..9973a6158 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/pulsar/inbound/PulsarPriceConsumer.java @@ -0,0 +1,15 @@ +package pulsar.inbound; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; + +@ApplicationScoped +public class PulsarPriceConsumer { + + @Incoming("prices") + public void consume(double price) { + // process your price. + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/pulsar/inbound/PulsarPriceMessageConsumer.java b/content/docs/smallrye-reactive-messaging/latest/_examples/pulsar/inbound/PulsarPriceMessageConsumer.java new file mode 100644 index 000000000..4cdc3f12a --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/pulsar/inbound/PulsarPriceMessageConsumer.java @@ -0,0 +1,21 @@ +package pulsar.inbound; + +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; + +@ApplicationScoped +public class PulsarPriceMessageConsumer { + + @Incoming("prices") + public CompletionStage consume(Message price) { + // process your price. + + // Acknowledge the incoming message + return price.ack(); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/pulsar/outbound/PulsarExactlyOnceProcessor.java b/content/docs/smallrye-reactive-messaging/latest/_examples/pulsar/outbound/PulsarExactlyOnceProcessor.java new file mode 100644 index 000000000..c207cba70 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/pulsar/outbound/PulsarExactlyOnceProcessor.java @@ -0,0 +1,35 @@ +package pulsar.outbound; + +import java.util.List; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.pulsar.PulsarIncomingBatchMessageMetadata; +import io.smallrye.reactive.messaging.pulsar.PulsarMessage; +import io.smallrye.reactive.messaging.pulsar.transactions.PulsarTransactions; + +@ApplicationScoped +public class PulsarExactlyOnceProcessor { + + @Inject + @Channel("tx-out-example") + PulsarTransactions txProducer; + + @Incoming("in-channel") + public Uni emitInTransaction(Message> batch) { + return txProducer.withTransactionAndAck(batch, emitter -> { + PulsarIncomingBatchMessageMetadata metadata = batch.getMetadata(PulsarIncomingBatchMessageMetadata.class).get(); + for (org.apache.pulsar.client.api.Message message : metadata. getMessages()) { + emitter.send(PulsarMessage.of(message.getValue() + 1, message.getKey())); + } + return Uni.createFrom().voidItem(); + }); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/pulsar/outbound/PulsarKeyValueExample.java b/content/docs/smallrye-reactive-messaging/latest/_examples/pulsar/outbound/PulsarKeyValueExample.java new file mode 100644 index 000000000..ab0b43a6c --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/pulsar/outbound/PulsarKeyValueExample.java @@ -0,0 +1,28 @@ +package pulsar.outbound; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; + +import org.apache.pulsar.client.api.Schema; +import org.apache.pulsar.common.schema.KeyValue; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.common.annotation.Identifier; + +@ApplicationScoped +public class PulsarKeyValueExample { + + // tag::code[] + @Identifier("out") + @Produces + Schema> schema = Schema.KeyValue(Schema.STRING, Schema.INT64); + + @Incoming("in") + @Outgoing("out") + public KeyValue process(long in) { + return new KeyValue<>("my-key", in); + } + // end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/pulsar/outbound/PulsarOutboundMetadataExample.java b/content/docs/smallrye-reactive-messaging/latest/_examples/pulsar/outbound/PulsarOutboundMetadataExample.java new file mode 100644 index 000000000..152974fe2 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/pulsar/outbound/PulsarOutboundMetadataExample.java @@ -0,0 +1,26 @@ +package pulsar.outbound; + +import java.util.Map; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.pulsar.PulsarOutgoingMessageMetadata; + +public class PulsarOutboundMetadataExample { + + public Message metadata(Message incoming) { + // tag::code[] + // Creates an PulsarOutgoingMessageMetadata + // The type parameter is the type of the record's key + PulsarOutgoingMessageMetadata metadata = PulsarOutgoingMessageMetadata.builder() + .withKey("my-key") + .withProperties(Map.of("property-key", "value")) + .build(); + + // Create a new message from the `incoming` message + // Add `metadata` to the metadata from the `incoming` message. + return incoming.addMetadata(metadata); + // end::code[] + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/pulsar/outbound/PulsarOutgoingMessageExample.java b/content/docs/smallrye-reactive-messaging/latest/_examples/pulsar/outbound/PulsarOutgoingMessageExample.java new file mode 100644 index 000000000..f452f437e --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/pulsar/outbound/PulsarOutgoingMessageExample.java @@ -0,0 +1,18 @@ +package pulsar.outbound; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.reactive.messaging.pulsar.OutgoingMessage; + +public class PulsarOutgoingMessageExample { + + // tag::code[] + @Incoming("in") + @Outgoing("out") + OutgoingMessage process(org.apache.pulsar.client.api.Message in) { + return OutgoingMessage.from(in) + .withValue(Long.valueOf(in.getValue())); + } + // end::code[] +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/pulsar/outbound/PulsarPriceMessageProducer.java b/content/docs/smallrye-reactive-messaging/latest/_examples/pulsar/outbound/PulsarPriceMessageProducer.java new file mode 100644 index 000000000..1f53f7c68 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/pulsar/outbound/PulsarPriceMessageProducer.java @@ -0,0 +1,26 @@ +package pulsar.outbound; + +import java.time.Duration; +import java.util.Random; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +@ApplicationScoped +public class PulsarPriceMessageProducer { + + private Random random = new Random(); + + @Outgoing("prices") + public Multi> generate() { + // Build an infinite stream of random prices + // It emits a price every second + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .map(x -> Message.of(random.nextDouble())); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/pulsar/outbound/PulsarPriceProducer.java b/content/docs/smallrye-reactive-messaging/latest/_examples/pulsar/outbound/PulsarPriceProducer.java new file mode 100644 index 000000000..2073b7763 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/pulsar/outbound/PulsarPriceProducer.java @@ -0,0 +1,25 @@ +package pulsar.outbound; + +import java.time.Duration; +import java.util.Random; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +@ApplicationScoped +public class PulsarPriceProducer { + + private Random random = new Random(); + + @Outgoing("prices") + public Multi generate() { + // Build an infinite stream of random prices + // It emits a price every second + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .map(x -> random.nextDouble()); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/pulsar/outbound/PulsarTransactionalProducer.java b/content/docs/smallrye-reactive-messaging/latest/_examples/pulsar/outbound/PulsarTransactionalProducer.java new file mode 100644 index 000000000..8bee09bb3 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/pulsar/outbound/PulsarTransactionalProducer.java @@ -0,0 +1,38 @@ +package pulsar.outbound; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.pulsar.OutgoingMessage; +import io.smallrye.reactive.messaging.pulsar.transactions.PulsarTransactions; + +@ApplicationScoped +public class PulsarTransactionalProducer { + + @Inject + @Channel("tx-out-example") + PulsarTransactions> txProducer; + + @Inject + @Channel("other-producer") + PulsarTransactions producer; + + @Incoming("in") + public Uni emitInTransaction(Message in) { + return txProducer.withTransaction(emitter -> { + emitter.send(OutgoingMessage.of("a", 1)); + emitter.send(OutgoingMessage.of("b", 2)); + emitter.send(OutgoingMessage.of("c", 3)); + producer.send(emitter, "4"); + producer.send(emitter, "5"); + producer.send(emitter, "6"); + return Uni.createFrom().completionStage(in::ack); + }); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/quickstart/ReactiveMessagingExample.java b/content/docs/smallrye-reactive-messaging/latest/_examples/quickstart/ReactiveMessagingExample.java new file mode 100644 index 000000000..318b39739 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/quickstart/ReactiveMessagingExample.java @@ -0,0 +1,35 @@ +package quickstart; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +@ApplicationScoped +public class ReactiveMessagingExample { + + @Outgoing("source") + public Multi source() { + return Multi.createFrom().items("hello", "from", "SmallRye", "reactive", "messaging"); + } + + @Incoming("source") + @Outgoing("processed-a") + public String toUpperCase(String payload) { + return payload.toUpperCase(); + } + + @Incoming("processed-a") + @Outgoing("processed-b") + public Multi filter(Multi input) { + return input.select().where(item -> item.length() > 4); + } + + @Incoming("processed-b") + public void sink(String word) { + System.out.println(">> " + word); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/rabbitmq/outbound/MyRabbitMQReplyFailureHandler.java b/content/docs/smallrye-reactive-messaging/latest/_examples/rabbitmq/outbound/MyRabbitMQReplyFailureHandler.java new file mode 100644 index 000000000..637edb2bc --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/rabbitmq/outbound/MyRabbitMQReplyFailureHandler.java @@ -0,0 +1,21 @@ +package rabbitmq.outbound; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.smallrye.common.annotation.Identifier; +import io.smallrye.reactive.messaging.rabbitmq.IncomingRabbitMQMessage; +import io.smallrye.reactive.messaging.rabbitmq.reply.ReplyFailureHandler; + +@ApplicationScoped +@Identifier("my-reply-error") +public class MyRabbitMQReplyFailureHandler implements ReplyFailureHandler { + + @Override + public Throwable handleReply(IncomingRabbitMQMessage replyMessage) { + var error = (String) replyMessage.getHeaders().get("REPLY_ERROR"); + if (error != null) { + return (Throwable) new IllegalArgumentException(error); + } + return null; + } +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/rabbitmq/outbound/RabbitMQReplier.java b/content/docs/smallrye-reactive-messaging/latest/_examples/rabbitmq/outbound/RabbitMQReplier.java new file mode 100644 index 000000000..aec8bfa73 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/rabbitmq/outbound/RabbitMQReplier.java @@ -0,0 +1,20 @@ +package rabbitmq.outbound; + +import java.util.Random; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +@ApplicationScoped +public class RabbitMQReplier { + + Random rand = new Random(); + + @Incoming("request") + @Outgoing("reply") + String handleRequest(String request) { + return request + "-" + rand.nextInt(100); + } +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/rabbitmq/outbound/RabbitMQRequestReplyEmitter.java b/content/docs/smallrye-reactive-messaging/latest/_examples/rabbitmq/outbound/RabbitMQRequestReplyEmitter.java new file mode 100644 index 000000000..019255318 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/rabbitmq/outbound/RabbitMQRequestReplyEmitter.java @@ -0,0 +1,21 @@ +package rabbitmq.outbound; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; + +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.rabbitmq.reply.RabbitMQRequestReply; + +@ApplicationScoped +public class RabbitMQRequestReplyEmitter { + + @Inject + @Channel("my-request") + RabbitMQRequestReply quoteRequest; + + public Uni requestQuote(String request) { + return quoteRequest.request(request); + } +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/rabbitmq/outbound/RabbitMQRequestReplyMessageEmitter.java b/content/docs/smallrye-reactive-messaging/latest/_examples/rabbitmq/outbound/RabbitMQRequestReplyMessageEmitter.java new file mode 100644 index 000000000..96b9b0f81 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/rabbitmq/outbound/RabbitMQRequestReplyMessageEmitter.java @@ -0,0 +1,22 @@ +package rabbitmq.outbound; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.rabbitmq.reply.RabbitMQRequestReply; + +@ApplicationScoped +public class RabbitMQRequestReplyMessageEmitter { + + @Inject + @Channel("my-request") + RabbitMQRequestReply quoteRequest; + + public Uni> requestMessage(String request) { + return quoteRequest.request(Message.of(request)); + } +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/rabbitmq/outbound/RabbitMQRequestReplyMultiEmitter.java b/content/docs/smallrye-reactive-messaging/latest/_examples/rabbitmq/outbound/RabbitMQRequestReplyMultiEmitter.java new file mode 100644 index 000000000..2069164d0 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/rabbitmq/outbound/RabbitMQRequestReplyMultiEmitter.java @@ -0,0 +1,21 @@ +package rabbitmq.outbound; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; + +import io.smallrye.mutiny.Multi; +import io.smallrye.reactive.messaging.rabbitmq.reply.RabbitMQRequestReply; + +@ApplicationScoped +public class RabbitMQRequestReplyMultiEmitter { + + @Inject + @Channel("my-request") + RabbitMQRequestReply quoteRequest; + + public Multi requestQuote(String request) { + return quoteRequest.requestMulti(request).select().first(5); + } +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/skip/SingleSkip.java b/content/docs/smallrye-reactive-messaging/latest/_examples/skip/SingleSkip.java new file mode 100644 index 000000000..f7209220b --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/skip/SingleSkip.java @@ -0,0 +1,59 @@ +package skip; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Uni; + +public class SingleSkip { + + // tag::skip[] + // Skip when processing payload synchronously - returning `null` + @Incoming("in") + @Outgoing("out") + public String processPayload(String s) { + if (s.equalsIgnoreCase("skip")) { + return null; + } + return s.toUpperCase(); + } + + // Skip when processing message synchronously - returning `null` + @Incoming("in") + @Outgoing("out") + public Message processMessage(Message m) { + String s = m.getPayload(); + if (s.equalsIgnoreCase("skip")) { + m.ack(); + return null; + } + return m.withPayload(s.toUpperCase()); + } + + // Skip when processing payload asynchronously - returning a `Uni` with a `null` value + @Incoming("in") + @Outgoing("out") + public Uni processPayloadAsync(String s) { + if (s.equalsIgnoreCase("skip")) { + // Important, you must not return `null`, but a `null` content + return Uni.createFrom().nullItem(); + } + return Uni.createFrom().item(s.toUpperCase()); + } + + // Skip when processing message asynchronously - returning a `Uni` with a `null` value + @Incoming("in") + @Outgoing("out") + public Uni> processMessageAsync(Message m) { + String s = m.getPayload(); + if (s.equalsIgnoreCase("skip")) { + m.ack(); + return Uni.createFrom().nullItem(); + } + return Uni.createFrom().item(m.withPayload(s.toUpperCase())); + } + + // end::skip[] + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/skip/StreamSkip.java b/content/docs/smallrye-reactive-messaging/latest/_examples/skip/StreamSkip.java new file mode 100644 index 000000000..2108a1077 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/skip/StreamSkip.java @@ -0,0 +1,48 @@ +package skip; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +public class StreamSkip { + + // tag::skip[] + @Incoming("in") + @Outgoing("out-1") + public Multi processPayload(String s) { + if (s.equalsIgnoreCase("skip")) { + return Multi.createFrom().empty(); + } + return Multi.createFrom().item(s.toUpperCase()); + } + + @Incoming("in") + @Outgoing("out-2") + public Multi> processMessage(Message m) { + String s = m.getPayload(); + if (s.equalsIgnoreCase("skip")) { + return Multi.createFrom().empty(); + } + return Multi.createFrom().item(m.withPayload(s.toUpperCase())); + } + + @Incoming("in") + @Outgoing("out-3") + public Multi processPayloadStream(Multi stream) { + return stream + .select().where(s -> !s.equalsIgnoreCase("skip")) + .onItem().transform(String::toUpperCase); + } + + @Incoming("in") + @Outgoing("out-4") + public Multi> processMessageStream(Multi> stream) { + return stream + .select().where(m -> !m.getPayload().equalsIgnoreCase("skip")) + .onItem().transform(m -> m.withPayload(m.getPayload().toUpperCase())); + } + // end::skip[] + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/sns/outbound/SnsMessageProducer.java b/content/docs/smallrye-reactive-messaging/latest/_examples/sns/outbound/SnsMessageProducer.java new file mode 100644 index 000000000..cc2e815d6 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/sns/outbound/SnsMessageProducer.java @@ -0,0 +1,27 @@ +package sns.outbound; + +import java.time.Duration; +import java.util.UUID; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Metadata; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; +import io.smallrye.reactive.messaging.aws.sns.SnsOutboundMetadata; + +@ApplicationScoped +public class SnsMessageProducer { + + @Outgoing("prices") + public Multi> generate() { + // It emits a UUID every second + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .map(x -> Message.of(UUID.randomUUID().toString(), + Metadata.of(SnsOutboundMetadata.builder() + .groupId("group-1") + .build()))); + } +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/sns/outbound/SnsOutboundMetadataExample.java b/content/docs/smallrye-reactive-messaging/latest/_examples/sns/outbound/SnsOutboundMetadataExample.java new file mode 100644 index 000000000..9d44dacb0 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/sns/outbound/SnsOutboundMetadataExample.java @@ -0,0 +1,29 @@ +package sns.outbound; + +import java.util.Map; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Metadata; + +import io.smallrye.reactive.messaging.aws.sns.SnsOutboundMetadata; +import software.amazon.awssdk.services.sns.model.MessageAttributeValue; + +@ApplicationScoped +public class SnsOutboundMetadataExample { + + public Message metadata(Message incoming) { + // tag::code[] + final SnsOutboundMetadata metadata = SnsOutboundMetadata.builder() + .messageAttributes(Map.of("my-attribute", MessageAttributeValue.builder() + .dataType("String").stringValue("my-value").build())) + .groupId("group-1") + .smsPhoneNumber("+1234567890") + .build(); + + // Add `metadata` to the metadata of the outgoing message. + return Message.of("Hello", Metadata.of(metadata)); + // end::code[] + } +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/sns/outbound/SnsProducer.java b/content/docs/smallrye-reactive-messaging/latest/_examples/sns/outbound/SnsProducer.java new file mode 100644 index 000000000..cde773fa6 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/sns/outbound/SnsProducer.java @@ -0,0 +1,22 @@ +package sns.outbound; + +import java.time.Duration; +import java.util.UUID; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +@ApplicationScoped +public class SnsProducer { + + @Outgoing("data") + public Multi generate() { + // It emits a UUID every second + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .map(x -> UUID.randomUUID().toString()); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/sqs/inbound/SqsCustomAckStrategy.java b/content/docs/smallrye-reactive-messaging/latest/_examples/sqs/inbound/SqsCustomAckStrategy.java new file mode 100644 index 000000000..ad8b679ba --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/sqs/inbound/SqsCustomAckStrategy.java @@ -0,0 +1,36 @@ +package sqs.inbound; + +import java.util.function.BiConsumer; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.smallrye.common.annotation.Identifier; +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.aws.sqs.SqsAckHandler; +import io.smallrye.reactive.messaging.aws.sqs.SqsConnectorIncomingConfiguration; +import io.smallrye.reactive.messaging.aws.sqs.SqsMessage; +import io.vertx.mutiny.core.Vertx; +import software.amazon.awssdk.services.sqs.SqsAsyncClient; + +public class SqsCustomAckStrategy implements SqsAckHandler { + + @ApplicationScoped + @Identifier("custom") + public static class Factory implements SqsAckHandler.Factory { + + @Override + public SqsAckHandler create(SqsConnectorIncomingConfiguration conf, + Vertx vertx, + SqsAsyncClient client, + Uni queueUrlUni, + BiConsumer reportFailure) { + return new SqsCustomAckStrategy(); + } + } + + @Override + public Uni handle(SqsMessage message) { + return Uni.createFrom().voidItem() + .emitOn(message::runOnMessageContext); + } +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/sqs/inbound/SqsCustomNackStrategy.java b/content/docs/smallrye-reactive-messaging/latest/_examples/sqs/inbound/SqsCustomNackStrategy.java new file mode 100644 index 000000000..6daeec399 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/sqs/inbound/SqsCustomNackStrategy.java @@ -0,0 +1,33 @@ +package sqs.inbound; + +import java.util.function.BiConsumer; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Metadata; + +import io.smallrye.common.annotation.Identifier; +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.aws.sqs.SqsFailureHandler; +import io.smallrye.reactive.messaging.aws.sqs.SqsMessage; +import software.amazon.awssdk.services.sqs.SqsAsyncClient; + +public class SqsCustomNackStrategy implements SqsFailureHandler { + + @ApplicationScoped + @Identifier("custom") + public static class Factory implements SqsFailureHandler.Factory { + + @Override + public SqsFailureHandler create(String channel, SqsAsyncClient client, Uni queueUrlUni, + BiConsumer reportFailure) { + return new SqsCustomNackStrategy(); + } + } + + @Override + public Uni handle(SqsMessage message, Metadata metadata, Throwable throwable) { + return Uni.createFrom().voidItem() + .emitOn(message::runOnMessageContext); + } +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/sqs/inbound/SqsMessageStringConsumer.java b/content/docs/smallrye-reactive-messaging/latest/_examples/sqs/inbound/SqsMessageStringConsumer.java new file mode 100644 index 000000000..218e26f0c --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/sqs/inbound/SqsMessageStringConsumer.java @@ -0,0 +1,17 @@ +package sqs.inbound; + +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; + +@ApplicationScoped +public class SqsMessageStringConsumer { + @Incoming("data") + CompletionStage consume(Message msg) { + System.out.println("Received: " + msg.getPayload()); + return msg.ack(); + } +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/sqs/inbound/SqsMetadataExample.java b/content/docs/smallrye-reactive-messaging/latest/_examples/sqs/inbound/SqsMetadataExample.java new file mode 100644 index 000000000..cd1b4a5aa --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/sqs/inbound/SqsMetadataExample.java @@ -0,0 +1,21 @@ +package sqs.inbound; + +import java.util.Map; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; + +import io.smallrye.reactive.messaging.aws.sqs.SqsIncomingMetadata; +import software.amazon.awssdk.services.sqs.model.MessageAttributeValue; + +@ApplicationScoped +public class SqsMetadataExample { + + @Incoming("queue") + public void metadata(String body, SqsIncomingMetadata metadata) { + Map attributes = metadata.getMessage().messageAttributes(); + attributes.forEach((k, v) -> System.out.println(k + " -> " + v.stringValue())); + System.out.println("Message body: " + body); + } +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/sqs/inbound/SqsReceiveMessageRequestCustomizerExample.java b/content/docs/smallrye-reactive-messaging/latest/_examples/sqs/inbound/SqsReceiveMessageRequestCustomizerExample.java new file mode 100644 index 000000000..1316a6c0a --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/sqs/inbound/SqsReceiveMessageRequestCustomizerExample.java @@ -0,0 +1,17 @@ +package sqs.inbound; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.smallrye.common.annotation.Identifier; +import io.smallrye.reactive.messaging.aws.sqs.SqsReceiveMessageRequestCustomizer; +import software.amazon.awssdk.services.sqs.model.ReceiveMessageRequest; + +@Identifier("my-customizer") // or with channel name @Identifier("data") +@ApplicationScoped +public class SqsReceiveMessageRequestCustomizerExample implements SqsReceiveMessageRequestCustomizer { + @Override + public void customize(ReceiveMessageRequest.Builder builder) { + builder.visibilityTimeout(10) + .messageAttributeNames("my-attribute"); + } +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/sqs/inbound/SqsSdkMessageConsumer.java b/content/docs/smallrye-reactive-messaging/latest/_examples/sqs/inbound/SqsSdkMessageConsumer.java new file mode 100644 index 000000000..bdaf6fd90 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/sqs/inbound/SqsSdkMessageConsumer.java @@ -0,0 +1,21 @@ +package sqs.inbound; + +import java.util.Map; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; + +import software.amazon.awssdk.services.sqs.model.Message; +import software.amazon.awssdk.services.sqs.model.MessageAttributeValue; + +@ApplicationScoped +public class SqsSdkMessageConsumer { + + @Incoming("data") + void consume(Message msg) { + System.out.println("Received: " + msg.body()); + Map attributes = msg.messageAttributes(); + // ... + } +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/sqs/inbound/SqsStringConsumer.java b/content/docs/smallrye-reactive-messaging/latest/_examples/sqs/inbound/SqsStringConsumer.java new file mode 100644 index 000000000..ffdfcf6d9 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/sqs/inbound/SqsStringConsumer.java @@ -0,0 +1,13 @@ +package sqs.inbound; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; + +@ApplicationScoped +public class SqsStringConsumer { + @Incoming("data") + void consume(String messageBody) { + System.out.println("Received: " + messageBody); + } +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/sqs/json/SqsJsonMapping.java b/content/docs/smallrye-reactive-messaging/latest/_examples/sqs/json/SqsJsonMapping.java new file mode 100644 index 000000000..4980edbb8 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/sqs/json/SqsJsonMapping.java @@ -0,0 +1,56 @@ +package sqs.json; + +import java.time.Duration; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +public class SqsJsonMapping { + + // tag::code[] + @ApplicationScoped + public static class Generator { + + @Outgoing("to-rabbitmq") + public Multi prices() { + AtomicInteger count = new AtomicInteger(); + return Multi.createFrom().ticks().every(Duration.ofMillis(1000)) + .map(l -> new Price().setPrice(count.incrementAndGet())) + .onOverflow().drop(); + } + + } + + @ApplicationScoped + public static class Consumer { + + List prices = new CopyOnWriteArrayList<>(); + + @Incoming("from-rabbitmq") + public void consume(Price price) { + prices.add(price); + } + + public List list() { + return prices; + } + } + + public static class Price { + public int price; + + public Price setPrice(int price) { + this.price = price; + return this; + } + } + // end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/sqs/outbound/SqsMessageStringProducer.java b/content/docs/smallrye-reactive-messaging/latest/_examples/sqs/outbound/SqsMessageStringProducer.java new file mode 100644 index 000000000..eac978671 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/sqs/outbound/SqsMessageStringProducer.java @@ -0,0 +1,27 @@ +package sqs.outbound; + +import java.time.Duration; +import java.util.UUID; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Metadata; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; +import io.smallrye.reactive.messaging.aws.sqs.SqsOutboundMetadata; + +@ApplicationScoped +public class SqsMessageStringProducer { + + @Outgoing("prices") + public Multi> generate() { + // It emits a UUID every second + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .map(x -> Message.of(UUID.randomUUID().toString(), + Metadata.of(SqsOutboundMetadata.builder() + .groupId("group-1") + .build()))); + } +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/sqs/outbound/SqsOutboundMetadataExample.java b/content/docs/smallrye-reactive-messaging/latest/_examples/sqs/outbound/SqsOutboundMetadataExample.java new file mode 100644 index 000000000..e512f4743 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/sqs/outbound/SqsOutboundMetadataExample.java @@ -0,0 +1,27 @@ +package sqs.outbound; + +import java.util.Map; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Metadata; + +import io.smallrye.reactive.messaging.aws.sqs.SqsOutboundMetadata; +import software.amazon.awssdk.services.sqs.model.MessageAttributeValue; + +public class SqsOutboundMetadataExample { + + public Message metadata(Message incoming) { + + // tag::code[] + final SqsOutboundMetadata metadata = SqsOutboundMetadata.builder() + .messageAttributes(Map.of("my-attribute", MessageAttributeValue.builder() + .dataType("String").stringValue("my-value").build())) + .groupId("group-1") + .build(); + + // Add `metadata` to the metadata of the outgoing message. + return Message.of("Hello", Metadata.of(metadata)); + // end::code[] + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/sqs/outbound/SqsStringProducer.java b/content/docs/smallrye-reactive-messaging/latest/_examples/sqs/outbound/SqsStringProducer.java new file mode 100644 index 000000000..4a39f5b4e --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/sqs/outbound/SqsStringProducer.java @@ -0,0 +1,22 @@ +package sqs.outbound; + +import java.time.Duration; +import java.util.UUID; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +@ApplicationScoped +public class SqsStringProducer { + + @Outgoing("data") + public Multi generate() { + // It emits a UUID every second + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .map(x -> UUID.randomUUID().toString()); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/testing/MyTest.java b/content/docs/smallrye-reactive-messaging/latest/_examples/testing/MyTest.java new file mode 100644 index 000000000..1e1451f67 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/testing/MyTest.java @@ -0,0 +1,52 @@ +package testing; + +import jakarta.enterprise.inject.Any; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import io.smallrye.reactive.messaging.memory.TestIncoming; +import io.smallrye.reactive.messaging.memory.TestOutgoing; +import io.smallrye.reactive.messaging.memory.TestingConnector; + +// @io.quarkus.test.junit.QuarkusTest or the Junit 5 extension that allows injection in tests +public class MyTest { + + // 1. Switch the channels to the test connector: + @BeforeAll + public static void switchMyChannels() { + TestingConnector.switchIncomingChannelsToTesting("prices"); + TestingConnector.switchOutgoingChannelsToTesting("processed-prices"); + } + + // 2. Don't forget to reset the channel after the tests: + @AfterAll + public static void revertMyChannels() { + TestingConnector.clear(); + } + + // 3. Inject the test connector in your test, + // or use the bean manager to retrieve the instance + @Inject + @Any + TestingConnector connector; + + @Test + void test() { + // 4. Retrieves the incoming channel to deliver messages + TestIncoming prices = connector.incoming("prices"); + // 5. Retrieves the outgoing channel to verify what the app sent + TestOutgoing results = connector.outgoing("processed-prices"); + + // 6. Deliver fake messages to the app: + prices.deliver(1); + prices.deliver(2); + prices.deliver(3); + + // 7. Verify what the app sent + Assertions.assertEquals(3, results.sent().size()); + } +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_examples/testing/MyTestSetup.java b/content/docs/smallrye-reactive-messaging/latest/_examples/testing/MyTestSetup.java new file mode 100644 index 000000000..ae5b9a3f0 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_examples/testing/MyTestSetup.java @@ -0,0 +1,23 @@ +package testing; + +import java.util.HashMap; +import java.util.Map; + +import io.smallrye.reactive.messaging.memory.TestingConnector; + +public class MyTestSetup { + + // tag::code[] + public Map start() { + Map env = new HashMap<>(); + env.putAll(TestingConnector.switchIncomingChannelsToTesting("prices")); + env.putAll(TestingConnector.switchOutgoingChannelsToTesting("my-data-stream")); + return env; + } + + public void stop() { + TestingConnector.clear(); + } + // end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/latest/_generated/smallrye-amqp-incoming.adoc b/content/docs/smallrye-reactive-messaging/latest/_generated/smallrye-amqp-incoming.adoc new file mode 100644 index 000000000..9804e5883 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_generated/smallrye-amqp-incoming.adoc @@ -0,0 +1,37 @@ +[cols="<,<,^,^,<",options="header",] +|=== +|Attribute (_alias_) |Description |Type |Mandatory |Default +|_address_ |The AMQP address. If not set, the channel name is used |string |false | +|_auto-acknowledgement_ |Whether the received AMQP messages must be acknowledged when received |boolean |false |`false` +|_broadcast_ |Whether the received AMQP messages must be dispatched to multiple _subscribers_ |boolean |false |`false` +|_capabilities_ |A comma-separated list of capabilities proposed by the sender or receiver client. |string |false | +|_cbs.audience_ |The audience used to authenticate with the broker using the CBS exchange. If not set, the audience is derived from the address. |string |false |++``++ +|_cbs.enabled_ |Whether the CBS authorization is enabled. If enabled, the connector will use the claims-based security to authenticate with the broker. If disabled, the connector will not use the CBS exchange and will not authenticate with the broker. |boolean |false |`false` +|_cbs.exchange_ |The name of the CBS exchange used to authenticate with the broker. |string |false |`set-token` +|_cbs.scopes_ |The scopes used to authenticate with the broker using the CBS exchange. If not set, the scopes are derived from the address. |string |false |++``++ +|_cbs.token-manager_ |The name of the CbsTokenManager bean used to manage the CBS tokens. |string |false |`default-cbs-token-manager` +|_client-options-name_ _(amqp-client-options-name)_ |The name of the AMQP Client Option bean used to customize the AMQP client configuration |string |false | +|_cloud-events_ |Enables (default) or disables the Cloud Event support. If enabled on an _incoming_ channel, the connector analyzes the incoming records and try to create Cloud Event metadata. If enabled on an _outgoing_, the connector sends the outgoing messages as Cloud Event if the message includes Cloud Event Metadata. |boolean |false |`true` +|_connect-timeout_ _(amqp-connect-timeout)_ |The connection timeout in milliseconds |int |false |`1000` +|_container-id_ |The AMQP container id. Channels with the same container id share the same AMQP connection. |string |false | +|_durable_ |Whether AMQP subscription is durable |boolean |false |`false` +|_failure-strategy_ |Specify the failure strategy to apply when a message produced from an AMQP message is nacked. Accepted values are `fail` (default), `accept`, `release`, `reject`, `modified-failed`, `modified-failed-undeliverable-here` |string |false |`fail` +|_health-enabled_ |Whether health reporting is enabled (default) or disabled |boolean |false |`true` +|_health-timeout_ |The max number of seconds to wait to determine if the connection with the broker is still established for the readiness check. After that threshold, the check is considered as failed. |int |false |`3` +|_host_ _(amqp-host)_ |The broker hostname |string |false |`localhost` +|_initial-credits_ |The number of credits initially requested for incoming messages. This controls how many messages the receiver can buffer. |int |false |`1000` +|_link-name_ |The name of the link. If not set, the channel name is used. |string |false | +|_password_ _(amqp-password)_ |The password used to authenticate to the broker |string |false | +|_port_ _(amqp-port)_ |The broker port |int |false |`5672` +|_qos_ |Sets the local QOS config, values can be ++{++@code null}, ++{++@code AT++_++MOST++_++ONCE} or ++{++@code AT++_++LEAST++_++ONCE}. |string |false | +|_reconnect-attempts_ _(amqp-reconnect-attempts)_ |The number of reconnection attempts |int |false |`100` +|_reconnect-interval_ _(amqp-reconnect-interval)_ |The interval in second between two reconnection attempts |int |false |`10` +|_retry-on-fail-attempts_ |The number of tentative to retry on failure |int |false |`6` +|_retry-on-fail-interval_ |The interval (in seconds) between two sending attempts |int |false |`5` +|_selector_ |Sets a message selector. This attribute is used to define an `apache.org:selector-filter:string` filter on the source terminus, using SQL-based syntax to request the server filters which messages are delivered to the receiver (if supported by the server in question). Precise functionality supported and syntax needed can vary depending on the server. |string |false | +|_sni-server-name_ _(amqp-sni-server-name)_ |If set, explicitly override the hostname to use for the TLS SNI server name |string |false | +|_tracing-enabled_ |Whether tracing is enabled (default) or disabled |boolean |false |`true` +|_use-ssl_ _(amqp-use-ssl)_ |Whether the AMQP connection uses SSL/TLS |boolean |false |`false` +|_username_ _(amqp-username)_ |The username used to authenticate to the broker |string |false | +|_virtual-host_ _(amqp-virtual-host)_ |If set, configure the hostname value used for the connection AMQP Open frame and TLS SNI server name (if TLS is in use) |string |false | +|=== diff --git a/content/docs/smallrye-reactive-messaging/latest/_generated/smallrye-amqp-outgoing.adoc b/content/docs/smallrye-reactive-messaging/latest/_generated/smallrye-amqp-outgoing.adoc new file mode 100644 index 000000000..e38833d3d --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_generated/smallrye-amqp-outgoing.adoc @@ -0,0 +1,44 @@ +[cols="<,<,^,^,<",options="header",] +|=== +|Attribute (_alias_) |Description |Type |Mandatory |Default +|_address_ |The AMQP address. If not set, the channel name is used |string |false | +|_capabilities_ |A comma-separated list of capabilities proposed by the sender or receiver client. |string |false | +|_cbs.audience_ |The audience used to authenticate with the broker using the CBS exchange. If not set, the audience is derived from the address. |string |false |++``++ +|_cbs.enabled_ |Whether the CBS authorization is enabled. If enabled, the connector will use the claims-based security to authenticate with the broker. If disabled, the connector will not use the CBS exchange and will not authenticate with the broker. |boolean |false |`false` +|_cbs.exchange_ |The name of the CBS exchange used to authenticate with the broker. |string |false |`set-token` +|_cbs.scopes_ |The scopes used to authenticate with the broker using the CBS exchange. If not set, the scopes are derived from the address. |string |false |++``++ +|_cbs.token-manager_ |The name of the CbsTokenManager bean used to manage the CBS tokens. |string |false |`default-cbs-token-manager` +|_client-options-name_ _(amqp-client-options-name)_ |The name of the AMQP Client Option bean used to customize the AMQP client configuration |string |false | +|_cloud-events_ |Enables (default) or disables the Cloud Event support. If enabled on an _incoming_ channel, the connector analyzes the incoming records and try to create Cloud Event metadata. If enabled on an _outgoing_, the connector sends the outgoing messages as Cloud Event if the message includes Cloud Event Metadata. |boolean |false |`true` +|_cloud-events-data-content-type_ _(cloud-events-default-data-content-type)_ |Configure the default `datacontenttype` attribute of the outgoing Cloud Event. Requires `cloud-events` to be set to `true`. This value is used if the message does not configure the `datacontenttype` attribute itself |string |false | +|_cloud-events-data-schema_ _(cloud-events-default-data-schema)_ |Configure the default `dataschema` attribute of the outgoing Cloud Event. Requires `cloud-events` to be set to `true`. This value is used if the message does not configure the `dataschema` attribute itself |string |false | +|_cloud-events-insert-timestamp_ _(cloud-events-default-timestamp)_ |Whether or not the connector should insert automatically the `time` attribute into the outgoing Cloud Event. Requires `cloud-events` to be set to `true`. This value is used if the message does not configure the `time` attribute itself |boolean |false |`true` +|_cloud-events-mode_ |The Cloud Event mode (`structured` or `binary` (default)). Indicates how are written the cloud events in the outgoing record |string |false |`binary` +|_cloud-events-source_ _(cloud-events-default-source)_ |Configure the default `source` attribute of the outgoing Cloud Event. Requires `cloud-events` to be set to `true`. This value is used if the message does not configure the `source` attribute itself |string |false | +|_cloud-events-subject_ _(cloud-events-default-subject)_ |Configure the default `subject` attribute of the outgoing Cloud Event. Requires `cloud-events` to be set to `true`. This value is used if the message does not configure the `subject` attribute itself |string |false | +|_cloud-events-type_ _(cloud-events-default-type)_ |Configure the default `type` attribute of the outgoing Cloud Event. Requires `cloud-events` to be set to `true`. This value is used if the message does not configure the `type` attribute itself |string |false | +|_connect-timeout_ _(amqp-connect-timeout)_ |The connection timeout in milliseconds |int |false |`1000` +|_container-id_ |The AMQP container id. Channels with the same container id share the same AMQP connection. |string |false | +|_credit-retrieval-period_ |The period (in milliseconds) between two attempts to retrieve the credits granted by the broker. This time is used when the sender run out of credits. |int |false |`2000` +|_durable_ |Whether sent AMQP messages are marked durable |boolean |false |`false` +|_health-enabled_ |Whether health reporting is enabled (default) or disabled |boolean |false |`true` +|_health-timeout_ |The max number of seconds to wait to determine if the connection with the broker is still established for the readiness check. After that threshold, the check is considered as failed. |int |false |`3` +|_host_ _(amqp-host)_ |The broker hostname |string |false |`localhost` +|_lazy-client_ |Whether to create the connection and sender at startup or at first send request |boolean |false |`true` +|_link-name_ |The name of the link. If not set, the channel name is used. |string |false | +|_max-inflight-messages_ |The maximum number of messages to be written to the broker concurrently. The number of sent messages waiting to be acknowledged by the broker are limited by this value and credits granted by the broker. The default value `0` means only credits apply. |long |false |`0` +|_merge_ |Whether the connector should allow multiple upstreams |boolean |false |`false` +|_password_ _(amqp-password)_ |The password used to authenticate to the broker |string |false | +|_port_ _(amqp-port)_ |The broker port |int |false |`5672` +|_reconnect-attempts_ _(amqp-reconnect-attempts)_ |The number of reconnection attempts |int |false |`100` +|_reconnect-interval_ _(amqp-reconnect-interval)_ |The interval in second between two reconnection attempts |int |false |`10` +|_retry-on-fail-attempts_ |The number of tentative to retry on failure |int |false |`6` +|_retry-on-fail-interval_ |The interval (in seconds) between two sending attempts |int |false |`5` +|_sni-server-name_ _(amqp-sni-server-name)_ |If set, explicitly override the hostname to use for the TLS SNI server name |string |false | +|_tracing-enabled_ |Whether tracing is enabled (default) or disabled |boolean |false |`true` +|_ttl_ |The time-to-live of the send AMQP messages. 0 to disable the TTL |long |false |`0` +|_use-anonymous-sender_ |Whether or not the connector should use an anonymous sender. Default value is `true` if the broker supports it, `false` otherwise. If not supported, it is not possible to dynamically change the destination address. |boolean |false | +|_use-ssl_ _(amqp-use-ssl)_ |Whether the AMQP connection uses SSL/TLS |boolean |false |`false` +|_username_ _(amqp-username)_ |The username used to authenticate to the broker |string |false | +|_virtual-host_ _(amqp-virtual-host)_ |If set, configure the hostname value used for the connection AMQP Open frame and TLS SNI server name (if TLS is in use) |string |false | +|=== diff --git a/content/docs/smallrye-reactive-messaging/latest/_generated/smallrye-camel-incoming.adoc b/content/docs/smallrye-reactive-messaging/latest/_generated/smallrye-camel-incoming.adoc new file mode 100644 index 000000000..e14c417cf --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_generated/smallrye-camel-incoming.adoc @@ -0,0 +1,6 @@ +[cols="<,<,^,^,<",options="header",] +|=== +|Attribute (_alias_) |Description |Type |Mandatory |Default +|_endpoint-uri_ |The URI of the Camel endpoint (read from or written to) |string |true | +|_failure-strategy_ |Specify the failure strategy to apply when a message produced from a Camel exchange is nacked. Values can be `fail` (default) or `ignore` |string |false |`fail` +|=== diff --git a/content/docs/smallrye-reactive-messaging/latest/_generated/smallrye-camel-outgoing.adoc b/content/docs/smallrye-reactive-messaging/latest/_generated/smallrye-camel-outgoing.adoc new file mode 100644 index 000000000..ae1860580 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_generated/smallrye-camel-outgoing.adoc @@ -0,0 +1,6 @@ +[cols="<,<,^,^,<",options="header",] +|=== +|Attribute (_alias_) |Description |Type |Mandatory |Default +|_endpoint-uri_ |The URI of the Camel endpoint (read from or written to) |string |true | +|_merge_ |Whether the connector should allow multiple upstreams |boolean |false |`false` +|=== diff --git a/content/docs/smallrye-reactive-messaging/latest/_generated/smallrye-jms-incoming.adoc b/content/docs/smallrye-reactive-messaging/latest/_generated/smallrye-jms-incoming.adoc new file mode 100644 index 000000000..d93940a5d --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_generated/smallrye-jms-incoming.adoc @@ -0,0 +1,25 @@ +[cols="<,<,^,^,<",options="header",] +|=== +|Attribute (_alias_) |Description |Type |Mandatory |Default +|_broadcast_ |Whether or not the JMS message should be dispatched to multiple consumers |boolean |false |`false` +|_client-id_ |The client id |String |false | +|_connection-factory-name_ |The name of the JMS connection factory (`jakarta.jms.ConnectionFactory`) to be used. If not set, it uses any exposed JMS connection factory |String |false | +|_dead-letter-queue.destination_ |When the `failure-strategy` is set to `dead-letter-queue` indicates on which queue the message is sent. Defaults is `dead-letter-topic-$channel` |string |false | +|_dead-letter-queue.producer-client-id_ |When the `failure-strategy` is set to `dead-letter-queue` indicates what client id the generated producer should use. Defaults is `jms-dead-letter-topic-producer-$client-id` |string |false | +|_destination_ |The name of the JMS destination. If not set the name of the channel is used |String |false | +|_destination-type_ |The type of destination. It can be either `queue` or `topic` |string |false |`queue` +|_durable_ |Set to `true` to use a durable subscription |boolean |false |`false` +|_failure-strategy_ |Specify the failure strategy to apply when a message produced from a record is acknowledged negatively (nack). Values can be `fail` (default), `ignore`, or `dead-letter-queue` |string |false |`fail` +|_no-local_ |Enable or disable local delivery |boolean |false |`false` +|_password_ |The password to connect to to the JMS server |String |false | +|_retry_ |Whether to retry on terminal stream errors. |boolean |false |`true` +|_retry.initial-delay_ |The initial delay for the retry. |string |false |`PT1S` +|_retry.jitter_ |How much the delay jitters as a multiplier between 0 and 1. The formula is current delay ++*++ jitter. For example, with a current delay of 2H, a jitter of 0.5 will result in an actual delay somewhere between 1H and 3H. |double |false |`0.5` +|_retry.max-delay_ |The maximum delay |string |false |`PT10S` +|_retry.max-retries_ |Maximum number of retries for terminal stream errors. |int |false |`3` +|_reuse-jms-context_ |Whether to reuse JMS contexts by creating child contexts from a parent context. When enabled, child contexts will be created using JMSContext.createContext() instead of ConnectionFactory.createContext() |boolean |false |`false` +|_selector_ |The JMS selector |String |false | +|_session-mode_ |The session mode. Accepted values are AUTO++_++ACKNOWLEDGE, SESSION++_++TRANSACTED, CLIENT++_++ACKNOWLEDGE, DUPS++_++OK++_++ACKNOWLEDGE |String |false |`AUTO++_++ACKNOWLEDGE` +|_tracing-enabled_ |Whether tracing is enabled (default) or disabled |boolean |false |`true` +|_username_ |The username to connect to to the JMS server |String |false | +|=== diff --git a/content/docs/smallrye-reactive-messaging/latest/_generated/smallrye-jms-outgoing.adoc b/content/docs/smallrye-reactive-messaging/latest/_generated/smallrye-jms-outgoing.adoc new file mode 100644 index 000000000..335b694e3 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_generated/smallrye-jms-outgoing.adoc @@ -0,0 +1,28 @@ +[cols="<,<,^,^,<",options="header",] +|=== +|Attribute (_alias_) |Description |Type |Mandatory |Default +|_client-id_ |The client id |String |false | +|_connection-factory-name_ |The name of the JMS connection factory (`jakarta.jms.ConnectionFactory`) to be used. If not set, it uses any exposed JMS connection factory |String |false | +|_correlation-id_ |The JMS Message correlation id |string |false | +|_delivery-delay_ |The delivery delay |long |false | +|_delivery-mode_ |The delivery mode. Either `persistent` or `non++_++persistent` |string |false | +|_destination_ |The name of the JMS destination. If not set the name of the channel is used |String |false | +|_destination-type_ |The type of destination. It can be either `queue` or `topic` |string |false |`queue` +|_disable-message-id_ |Omit the message id in the outbound JMS message |boolean |false | +|_disable-message-timestamp_ |Omit the message timestamp in the outbound JMS message |boolean |false | +|_merge_ |Whether the connector should allow multiple upstreams |boolean |false |`false` +|_password_ |The password to connect to to the JMS server |String |false | +|_priority_ |The JMS Message priority |int |false | +|_reply-to_ |The reply to destination if any |string |false | +|_reply-to-destination-type_ |The type of destination for the response. It can be either `queue` or `topic` |string |false |`queue` +|_retry_ |Whether to retry on terminal stream errors. |boolean |false |`true` +|_retry.initial-delay_ |The initial delay for the retry. |string |false |`PT1S` +|_retry.jitter_ |How much the delay jitters as a multiplier between 0 and 1. The formula is current delay ++*++ jitter. For example, with a current delay of 2H, a jitter of 0.5 will result in an actual delay somewhere between 1H and 3H. |double |false |`0.5` +|_retry.max-delay_ |The maximum delay |string |false |`PT10S` +|_retry.max-retries_ |Maximum number of retries for terminal stream errors. |int |false |`3` +|_reuse-jms-context_ |Whether to reuse JMS contexts by creating child contexts from a parent context. When enabled, child contexts will be created using JMSContext.createContext() instead of ConnectionFactory.createContext() |boolean |false |`false` +|_session-mode_ |The session mode. Accepted values are AUTO++_++ACKNOWLEDGE, SESSION++_++TRANSACTED, CLIENT++_++ACKNOWLEDGE, DUPS++_++OK++_++ACKNOWLEDGE |String |false |`AUTO++_++ACKNOWLEDGE` +|_tracing-enabled_ |Whether tracing is enabled (default) or disabled |boolean |false |`true` +|_ttl_ |The JMS Message time-to-live |long |false | +|_username_ |The username to connect to to the JMS server |String |false | +|=== diff --git a/content/docs/smallrye-reactive-messaging/latest/_generated/smallrye-kafka-incoming.adoc b/content/docs/smallrye-reactive-messaging/latest/_generated/smallrye-kafka-incoming.adoc new file mode 100644 index 000000000..54dfa4117 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_generated/smallrye-kafka-incoming.adoc @@ -0,0 +1,65 @@ +[cols="<,<,^,^,<",options="header",] +|=== +|Attribute (_alias_) |Description |Type |Mandatory |Default +|_assign-seek_ |Assign partitions and optionally seek to offsets, instead of subscribing to topics. A comma-separating list of triplets in form of ++`++: | |:`to assign statically to the consumer and seek to the given offsets. Offset`0`seeks to beginning and offset`-1++`++ seeks to the end of the topic-partition. If the topic is omitted the configured topic will be used. If the offset is omitted partitions are assigned to the consumer but won't be seeked to offset. |string +|_auto.offset.reset_ |What to do when there is no initial offset in Kafka.Accepted values are earliest, latest and none |string |false |`latest` +|_batch_ |Whether the Kafka records are consumed in batch. The channel injection point must consume a compatible type, such as `List++<++Payload++>++` or `KafkaRecordBatch++<++Payload++>++`. |boolean |false |`false` +|_bootstrap.servers_ _(kafka.bootstrap.servers)_ |A comma-separated list of host:port to use for establishing the initial connection to the Kafka cluster. |string |false |`localhost:9092` +|_broadcast_ |Whether the Kafka records should be dispatched to multiple consumer |boolean |false |`false` +|_checkpoint.state-store_ |While using the `checkpoint` commit-strategy, the name set in `@Identifier` of a bean that implements `io.smallrye.reactive.messaging.kafka.StateStore.Factory` to specify the state store implementation. |string |false | +|_checkpoint.state-type_ |While using the `checkpoint` commit-strategy, the fully qualified type name of the state object to persist in the state store. When provided, it can be used by the state store implementation to help persisting the processing state object. |string |false | +|_checkpoint.unsynced-state-max-age.ms_ |While using the `checkpoint` commit-strategy, specify the max age in milliseconds that the processing state must be persisted before the connector is marked as unhealthy. Setting this attribute to 0 disables this monitoring. |int |false |`10000` +|_client-id-prefix_ |Prefix for Kafka client `client.id` attribute. If defined configured or generated `client.id` will be prefixed with the given value. |string |false | +|_cloud-events_ |Enables (default) or disables the Cloud Event support. If enabled on an _incoming_ channel, the connector analyzes the incoming records and try to create Cloud Event metadata. If enabled on an _outgoing_, the connector sends the outgoing messages as Cloud Event if the message includes Cloud Event Metadata. |boolean |false |`true` +|_commit-strategy_ |Specify the commit strategy to apply when a message produced from a record is acknowledged. Values can be `latest`, `ignore` or `throttled`. If `enable.auto.commit` is true then the default is `ignore` otherwise it is `throttled` |string |false | +|_consumer-rebalance-listener.name_ |The name set in `@Identifier` of a bean that implements `io.smallrye.reactive.messaging.kafka.KafkaConsumerRebalanceListener`. If set, this rebalance listener is applied to the consumer. |string |false | +|_dead-letter-queue.key.serializer_ |When the `failure-strategy` is set to `dead-letter-queue` indicates the key serializer to use. If not set the serializer associated to the key deserializer is used |string |false | +|_dead-letter-queue.producer-client-id_ |When the `failure-strategy` is set to `dead-letter-queue` indicates what client id the generated producer should use. Defaults is `kafka-dead-letter-topic-producer-$client-id` |string |false | +|_dead-letter-queue.topic_ |When the `failure-strategy` is set to `dead-letter-queue` indicates on which topic the record is sent. Defaults is `dead-letter-topic-$channel` |string |false | +|_dead-letter-queue.value.serializer_ |When the `failure-strategy` is set to `dead-letter-queue` indicates the value serializer to use. If not set the serializer associated to the value deserializer is used |string |false | +|_delayed-retry-topic.max-retries_ |When the `failure-strategy` is set to `delayed-retry-topic` indicates the maximum number of retries. If higher than the number of delayed retry topics, last topic is used. |int |false | +|_delayed-retry-topic.timeout_ |When the `failure-strategy` is set to `delayed-retry-topic` indicates the global timeout per record. |int |false |`120000` +|_delayed-retry-topic.topics_ |When the `failure-strategy` is set to `delayed-retry-topic` indicates topics to use. If not set the source channel name is used, with 10, 20 and 50 seconds delayed topics. |string |false | +|_enable.auto.commit_ |If enabled, consumer's offset will be periodically committed in the background by the underlying Kafka client, ignoring the actual processing outcome of the records. It is recommended to NOT enable this setting and let Reactive Messaging handles the commit. |boolean |false |`false` +|_fail-on-deserialization-failure_ |When no deserialization failure handler is set and a deserialization failure happens, report the failure and mark the application as unhealthy. If set to `false` and a deserialization failure happens, a `null` value is forwarded. |boolean |false |`true` +|_failure-strategy_ |Specify the failure strategy to apply when a message produced from a record is acknowledged negatively (nack). Values can be `fail` (default), `ignore`, or `dead-letter-queue` |string |false |`fail` +|_fetch.min.bytes_ |The minimum amount of data the server should return for a fetch request. The default setting of 1 byte means that fetch requests are answered as soon as a single byte of data is available or the fetch request times out waiting for data to arrive. |int |false |`1` +|_graceful-shutdown_ |Whether or not a graceful shutdown should be attempted when the application terminates. |boolean |false |`true` +|_group.id_ |A unique string that identifies the consumer group the application belongs to. If not set, a unique, generated id is used |string |false | +|_health-enabled_ |Whether health reporting is enabled (default) or disabled |boolean |false |`true` +|_health-readiness-enabled_ |Whether readiness health reporting is enabled (default) or disabled |boolean |false |`true` +|_health-readiness-timeout_ |_deprecated_ - During the readiness health check, the connector connects to the broker and retrieves the list of topics. This attribute specifies the maximum duration (in ms) for the retrieval. If exceeded, the channel is considered not-ready. Deprecated: Use 'health-topic-verification-timeout' instead. |long |false | +|_health-readiness-topic-verification_ |_deprecated_ - Whether the readiness check should verify that topics exist on the broker. Default to false. Enabling it requires an admin connection. Deprecated: Use 'health-topic-verification-enabled' instead. |boolean |false | +|_health-topic-verification-enabled_ |Whether the startup and readiness check should verify that topics exist on the broker. Default to false. Enabling it requires an admin client connection. |boolean |false |`false` +|_health-topic-verification-readiness-disabled_ |Whether the topic verification is disabled for the readiness health check. |boolean |false |`false` +|_health-topic-verification-startup-disabled_ |Whether the topic verification is disabled for the startup health check. |boolean |false |`false` +|_health-topic-verification-timeout_ |During the startup and readiness health check, the connector connects to the broker and retrieves the list of topics. This attribute specifies the maximum duration (in ms) for the retrieval. If exceeded, the channel is considered not-ready. |long |false |`2000` +|_kafka-configuration_ |Identifier of a CDI bean that provides the default Kafka consumer/producer configuration for this channel. The channel configuration can still override any attribute. The bean must have a type of Map++<++String, Object++>++ and must use the @io.smallrye.common.annotation.Identifier qualifier to set the identifier. |string |false | +|_key-deserialization-failure-handler_ |The name set in `@Identifier` of a bean that implements `io.smallrye.reactive.messaging.kafka.DeserializationFailureHandler`. If set, deserialization failure happening when deserializing keys are delegated to this handler which may retry or provide a fallback value. |string |false | +|_key.deserializer_ |The deserializer classname used to deserialize the record's key |string |false |`org.apache.kafka.common.serialization.StringDeserializer` +|_lazy-client_ |Whether Kafka client is created lazily or eagerly. |boolean |false |`false` +|_max-queue-size-factor_ |Multiplier factor to determine maximum number of records queued for processing, using `max.poll.records` ++*++ `max-queue-size-factor`. Defaults to 2. In `batch` mode `max.poll.records` is considered `1`. |int |false |`2` +|_ordered_ |Configures ordering guarantees for concurrent processing. Possible values: `key` for per-key sequential processing, or `partition` for per-partition sequential processing. Requires `@Blocking(ordered = false)` for concurrency. Works with any commit strategy. |string |false | +|_ordered.max-concurrency_ |Maximum number of groups (keys or partitions) that can be processed concurrently. Defaults to `max.poll.records`. |int |false | +|_partitions_ |_deprecated_ - The number of partitions to be consumed concurrently. The connector creates the specified amount of Kafka consumers. It should match the number of partition of the targeted topic. Deprecated: Use `concurrency` channel attribute instead. |int |false |`1` +|_pattern_ |Indicate that the `topic` property is a regular expression. Must be used with the `topic` property. Cannot be used with the `topics` property |boolean |false |`false` +|_pause-if-no-requests_ |Whether the polling must be paused when the application does not request items and resume when it does. This allows implementing back-pressure based on the application capacity. Note that polling is not stopped, but will not retrieve any records when paused. |boolean |false |`true` +|_poll-timeout_ |The polling timeout in milliseconds. When polling records, the poll will wait at most that duration before returning records. Default is 1000ms |int |false |`1000` +|_requests_ |_deprecated_ - When `partitions` is greater than 1, this attribute allows configuring how many records are requested by each consumers every time. Deprecated: Use `concurrency` channel attribute instead. |int |false |`128` +|_retry_ |Whether or not the connection to the broker is re-attempted in case of failure |boolean |false |`true` +|_retry-attempts_ |The maximum number of reconnection before failing. -1 means infinite retry |int |false |`-1` +|_retry-max-wait_ |The max delay (in seconds) between 2 reconnects |int |false |`30` +|_share-group_ |_deprecated_ - Deprecated, use 'share-group.enabled' instead |boolean |false |`false` +|_share-group.enabled_ |Whether to use Kafka Share Groups for consumption. When enabled, the consumer will use a ShareConsumer which provides cooperative record processing across multiple consumers without explicit partition assignment. |boolean |false |`false` +|_share-group.failure-acknowledgement-type_ |Default acknowledgement type to apply to the record, when the message is nacked. |string |false |`release` +|_share-group.failure-deserialization-acknowledgement-type_ |Default acknowledgement type to apply to the record, when the message is nacked because of failure on deserialization. |string |false |`reject` +|_share-group.unprocessed-record-max-age.ms_ |While using share groups, specify the max age in milliseconds that an unprocessed record can be before the connector reports a failure. Setting this attribute to 0 disables this monitoring. |int |false |`60000` +|_throttled.ordered_ |_deprecated_ - While using the `throttled` commit-strategy, configures ordering guarantees for concurrent processing. Possible values: `key` for per-key sequential processing within partitions, or `partition` for per-partition sequential processing. Requires `@Blocking(ordered = false)` for concurrency. Deprecated: Use `ordered` instead. |string |false | +|_throttled.ordered.max-concurrency_ |_deprecated_ - While using the `throttled` commit-strategy with ordered processing, specifies the maximum number of groups (keys or partitions) that can be processed concurrently. This should typically match your worker pool's `max-concurrency` setting. Defaults to `max.poll.records`. Deprecated: Use `ordered.max-concurrency` instead. |int |false | +|_throttled.unprocessed-record-max-age.ms_ |While using the `throttled` commit-strategy, specify the max age in milliseconds that an unprocessed message can be before the connector is marked as unhealthy. Setting this attribute to 0 disables this monitoring. |int |false |`60000` +|_topic_ |The consumed / populated Kafka topic. If neither this property nor the `topics` properties are set, the channel name is used |string |false | +|_topics_ |A comma-separating list of topics to be consumed. Cannot be used with the `topic` or `pattern` properties |string |false | +|_tracing-enabled_ |Whether tracing is enabled (default) or disabled |boolean |false |`true` +|_value-deserialization-failure-handler_ |The name set in `@Identifier` of a bean that implements `io.smallrye.reactive.messaging.kafka.DeserializationFailureHandler`. If set, deserialization failure happening when deserializing values are delegated to this handler which may retry or provide a fallback value. |string |false | +|_value.deserializer_ |The deserializer classname used to deserialize the record's value |string |true | +|=== diff --git a/content/docs/smallrye-reactive-messaging/latest/_generated/smallrye-kafka-outgoing.adoc b/content/docs/smallrye-reactive-messaging/latest/_generated/smallrye-kafka-outgoing.adoc new file mode 100644 index 000000000..85f3102a9 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_generated/smallrye-kafka-outgoing.adoc @@ -0,0 +1,46 @@ +[cols="<,<,^,^,<",options="header",] +|=== +|Attribute (_alias_) |Description |Type |Mandatory |Default +|_acks_ |The number of acknowledgments the producer requires the leader to have received before considering a request complete. This controls the durability of records that are sent. Accepted values are: 0, 1, all |string |false |`1` +|_bootstrap.servers_ _(kafka.bootstrap.servers)_ |A comma-separated list of host:port to use for establishing the initial connection to the Kafka cluster. |string |false |`localhost:9092` +|_buffer.memory_ |The total bytes of memory the producer can use to buffer records waiting to be sent to the server. |long |false |`33554432` +|_client-id-prefix_ |Prefix for Kafka client `client.id` attribute. If defined configured or generated `client.id` will be prefixed with the given value. |string |false | +|_close-timeout_ |The amount of milliseconds waiting for a graceful shutdown of the Kafka producer |int |false |`10000` +|_cloud-events_ |Enables (default) or disables the Cloud Event support. If enabled on an _incoming_ channel, the connector analyzes the incoming records and try to create Cloud Event metadata. If enabled on an _outgoing_, the connector sends the outgoing messages as Cloud Event if the message includes Cloud Event Metadata. |boolean |false |`true` +|_cloud-events-data-content-type_ _(cloud-events-default-data-content-type)_ |Configure the default `datacontenttype` attribute of the outgoing Cloud Event. Requires `cloud-events` to be set to `true`. This value is used if the message does not configure the `datacontenttype` attribute itself |string |false | +|_cloud-events-data-schema_ _(cloud-events-default-data-schema)_ |Configure the default `dataschema` attribute of the outgoing Cloud Event. Requires `cloud-events` to be set to `true`. This value is used if the message does not configure the `dataschema` attribute itself |string |false | +|_cloud-events-insert-timestamp_ _(cloud-events-default-timestamp)_ |Whether or not the connector should insert automatically the `time` attribute into the outgoing Cloud Event. Requires `cloud-events` to be set to `true`. This value is used if the message does not configure the `time` attribute itself |boolean |false |`true` +|_cloud-events-mode_ |The Cloud Event mode (`structured` or `binary` (default)). Indicates how are written the cloud events in the outgoing record |string |false |`binary` +|_cloud-events-source_ _(cloud-events-default-source)_ |Configure the default `source` attribute of the outgoing Cloud Event. Requires `cloud-events` to be set to `true`. This value is used if the message does not configure the `source` attribute itself |string |false | +|_cloud-events-subject_ _(cloud-events-default-subject)_ |Configure the default `subject` attribute of the outgoing Cloud Event. Requires `cloud-events` to be set to `true`. This value is used if the message does not configure the `subject` attribute itself |string |false | +|_cloud-events-type_ _(cloud-events-default-type)_ |Configure the default `type` attribute of the outgoing Cloud Event. Requires `cloud-events` to be set to `true`. This value is used if the message does not configure the `type` attribute itself |string |false | +|_health-enabled_ |Whether health reporting is enabled (default) or disabled |boolean |false |`true` +|_health-readiness-enabled_ |Whether readiness health reporting is enabled (default) or disabled |boolean |false |`true` +|_health-readiness-timeout_ |_deprecated_ - During the readiness health check, the connector connects to the broker and retrieves the list of topics. This attribute specifies the maximum duration (in ms) for the retrieval. If exceeded, the channel is considered not-ready. Deprecated: Use 'health-topic-verification-timeout' instead. |long |false | +|_health-readiness-topic-verification_ |_deprecated_ - Whether the readiness check should verify that topics exist on the broker. Default to false. Enabling it requires an admin connection. Deprecated: Use 'health-topic-verification-enabled' instead. |boolean |false | +|_health-topic-verification-enabled_ |Whether the startup and readiness check should verify that topics exist on the broker. Default to false. Enabling it requires an admin client connection. |boolean |false |`false` +|_health-topic-verification-readiness-disabled_ |Whether the topic verification is disabled for the readiness health check. |boolean |false |`false` +|_health-topic-verification-startup-disabled_ |Whether the topic verification is disabled for the startup health check. |boolean |false |`false` +|_health-topic-verification-timeout_ |During the startup and readiness health check, the connector connects to the broker and retrieves the list of topics. This attribute specifies the maximum duration (in ms) for the retrieval. If exceeded, the channel is considered not-ready. |long |false |`2000` +|_interceptor-bean_ |The name set in `@Identifier` of a bean that implements `org.apache.kafka.clients.producer.ProducerInterceptor`. If set, the identified bean will be used as the producer interceptor. |string |false | +|_kafka-configuration_ |Identifier of a CDI bean that provides the default Kafka consumer/producer configuration for this channel. The channel configuration can still override any attribute. The bean must have a type of Map++<++String, Object++>++ and must use the @io.smallrye.common.annotation.Identifier qualifier to set the identifier. |string |false | +|_key_ |A key to used when writing the record |string |false | +|_key-serialization-failure-handler_ |The name set in `@Identifier` of a bean that implements `io.smallrye.reactive.messaging.kafka.SerializationFailureHandler`. If set, serialization failure happening when serializing keys are delegated to this handler which may provide a fallback value. |string |false | +|_key.serializer_ |The serializer classname used to serialize the record's key |string |false |`org.apache.kafka.common.serialization.StringSerializer` +|_lazy-client_ |Whether Kafka client is created lazily or eagerly. |boolean |false |`false` +|_max-inflight-messages_ |The maximum number of messages to be written to Kafka concurrently. It limits the number of messages waiting to be written and acknowledged by the broker. You can set this attribute to `0` remove the limit |long |false |`1024` +|_merge_ |Whether the connector should allow multiple upstreams |boolean |false |`false` +|_partition_ |The target partition id. -1 to let the client determine the partition |int |false |`-1` +|_pooled-producer_ |_deprecated_ - Deprecated, use 'pooled-producer.enabled' instead |boolean |false |`false` +|_pooled-producer.enabled_ |Whether to use a pool of Kafka producers for concurrent transactions. Each transaction scope acquires a producer from the pool, enabling concurrent exactly-once processing. Requires transactional.id to be set. |boolean |false |`false` +|_pooled-producer.initial-pool-size_ |Number of Kafka producers to pre-create in the pool at startup. Respects the lazy-client setting. Defaults to 0 (lazy creation). |int |false |`0` +|_pooled-producer.max-pool-size_ |Maximum number of Kafka producers in the pool. When all producers are in use and the pool is exhausted, an exception is thrown. Defaults to 10. |int |false |`10` +|_propagate-headers_ |A comma-separating list of incoming record headers to be propagated to the outgoing record |string |false |++``++ +|_propagate-record-key_ |Propagate incoming record key to the outgoing record |boolean |false |`false` +|_retries_ |If set to a positive number, the connector will try to resend any record that was not delivered successfully (with a potentially transient error) until the number of retries is reached. If set to 0, retries are disabled. If not set, the connector tries to resend any record that failed to be delivered (because of a potentially transient error) during an amount of time configured by `delivery.timeout.ms`. |long |false |`2147483647` +|_topic_ |The consumed / populated Kafka topic. If neither this property nor the `topics` properties are set, the channel name is used |string |false | +|_tracing-enabled_ |Whether tracing is enabled (default) or disabled |boolean |false |`true` +|_value-serialization-failure-handler_ |The name set in `@Identifier` of a bean that implements `io.smallrye.reactive.messaging.kafka.SerializationFailureHandler`. If set, serialization failure happening when serializing values are delegated to this handler which may provide a fallback value. |string |false | +|_value.serializer_ |The serializer classname used to serialize the payload |string |true | +|_waitForWriteCompletion_ |Whether the client waits for Kafka to acknowledge the written record before acknowledging the message |boolean |false |`true` +|=== diff --git a/content/docs/smallrye-reactive-messaging/latest/_generated/smallrye-mqtt-incoming.adoc b/content/docs/smallrye-reactive-messaging/latest/_generated/smallrye-mqtt-incoming.adoc new file mode 100644 index 000000000..69d568103 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_generated/smallrye-mqtt-incoming.adoc @@ -0,0 +1,45 @@ +[cols="<,<,^,^,<",options="header",] +|=== +|Attribute (_alias_) |Description |Type |Mandatory |Default +|_authentication-method_ |MQTT 5.0: the authentication method for enhanced authentication |string |false | +|_auto-clean-session_ |Set to start with a clean session (`true` by default) |boolean |false |`true` +|_auto-generated-client-id_ |Set if the MQTT client must generate clientId automatically |boolean |false |`true` +|_auto-keep-alive_ |Set if the MQTT client must handle `PINGREQ` automatically |boolean |false |`true` +|_broadcast_ |Whether or not the messages should be dispatched to multiple consumers |boolean |false |`false` +|_buffer-size_ |The size buffer of incoming messages waiting to be processed |int |false |`128` +|_client-id_ |Set the client identifier |string |false | +|_client-options-name_ _(mqtt-client-options-name)_ |The name of the MQTT Client Option bean (`io.smallrye.reactive.messaging.mqtt.session.MqttClientSessionOptions`) used to customize the MQTT client configuration |string |false | +|_connect-timeout-seconds_ |Set the connect timeout (in seconds) |int |false |`60` +|_failure-strategy_ |Specify the failure strategy to apply when a message produced from a MQTT message is nacked. Values can be `fail` (default), or `ignore` |string |false |`fail` +|_health-enabled_ |Whether health reporting is enabled (default) or disabled |boolean |false |`true` +|_host_ |Set the MQTT server host name/IP |string |true | +|_keep-alive-seconds_ |Set the keep alive timeout in seconds |int |false |`30` +|_max-inflight-queue_ |Set max count of unacknowledged messages |int |false |`10` +|_max-message-size_ |Set max MQTT message size in bytes |int |false |`8092` +|_mqtt-version_ |Set the MQTT protocol version (4 for 3.1.1 or 5 for 5.0) |int |false |`4` +|_no-local_ |MQTT 5.0: if true, the server will not forward messages published by this client back to this client |boolean |false |`false` +|_password_ |Set the password to connect to the server |string |false | +|_port_ |Set the MQTT server port. Default to 8883 if ssl is enabled, or 1883 without ssl |int |false | +|_qos_ |Set the QoS level when subscribing to the topic or when sending a message (0, 1, or 2) |int |false |`0` +|_reconnect-interval-seconds_ |Set the reconnect interval in seconds |int |false |`1` +|_retain-as-published_ |MQTT 5.0: if true, the server keeps the retain flag as set by the publishing client |boolean |false |`false` +|_retain-handling_ |MQTT 5.0: retain handling option (0=send on subscribe, 1=send if new subscription, 2=do not send) |int |false |`0` +|_server-name_ |Set the SNI server name |string |false | +|_session-expiry-interval_ |Set the session expiry interval in seconds (MQTT 5.0 only) |int |false | +|_ssl_ |Set whether SSL/TLS is enabled |boolean |false |`false` +|_ssl.hostname-verification-algorithm_ |Set the hostname verifier algorithm for the TLS connection.Accepted values are `HTTPS`, `LDAPS`, and `NONE` (defaults). `NONE` disables the hostname verification. |string |false |`NONE` +|_ssl.keystore.location_ |Set the keystore location. In case of `pem` type this is the server ca cert path |string |false | +|_ssl.keystore.password_ |Set the keystore password. In case of `pem` type this is the key path |string |false | +|_ssl.keystore.type_ |Set the keystore type ++[++`pkcs12`, `jks`, `pem`++]++ |string |false |`pkcs12` +|_ssl.truststore.location_ |Set the truststore location. In case of `pem` type this is the client cert path |string |false | +|_ssl.truststore.password_ |Set the truststore password. In case of `pem` type this is not necessary |string |false | +|_ssl.truststore.type_ |Set the truststore type ++[++`pkcs12`, `jks`, `pem`++]++ |string |false |`pkcs12` +|_subscription-identifier_ |MQTT 5.0: subscription identifier to include in the SUBSCRIBE packet |int |false | +|_topic_ |Set the MQTT topic. If not set, the channel name is used |string |false | +|_trust-all_ |Set whether all server certificates should be trusted |boolean |false |`false` +|_unsubscribe-on-disconnection_ |This flag restore the old behavior to unsubscribe from the broken on disconnection |boolean |false |`false` +|_username_ |Set the username to connect to the server |string |false | +|_will-flag_ |Set if will information are provided on connection |boolean |false |`false` +|_will-qos_ |Set the QoS level for the will message |int |false |`0` +|_will-retain_ |Set if the will message must be retained |boolean |false |`false` +|=== diff --git a/content/docs/smallrye-reactive-messaging/latest/_generated/smallrye-mqtt-outgoing.adoc b/content/docs/smallrye-reactive-messaging/latest/_generated/smallrye-mqtt-outgoing.adoc new file mode 100644 index 000000000..7f079e6ca --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_generated/smallrye-mqtt-outgoing.adoc @@ -0,0 +1,40 @@ +[cols="<,<,^,^,<",options="header",] +|=== +|Attribute (_alias_) |Description |Type |Mandatory |Default +|_authentication-method_ |MQTT 5.0: the authentication method for enhanced authentication |string |false | +|_auto-clean-session_ |Set to start with a clean session (`true` by default) |boolean |false |`true` +|_auto-generated-client-id_ |Set if the MQTT client must generate clientId automatically |boolean |false |`true` +|_auto-keep-alive_ |Set if the MQTT client must handle `PINGREQ` automatically |boolean |false |`true` +|_client-id_ |Set the client identifier |string |false | +|_client-options-name_ _(mqtt-client-options-name)_ |The name of the MQTT Client Option bean (`io.smallrye.reactive.messaging.mqtt.session.MqttClientSessionOptions`) used to customize the MQTT client configuration |string |false | +|_connect-timeout-seconds_ |Set the connect timeout (in seconds) |int |false |`60` +|_health-enabled_ |Whether health reporting is enabled (default) or disabled |boolean |false |`true` +|_host_ |Set the MQTT server host name/IP |string |true | +|_keep-alive-seconds_ |Set the keep alive timeout in seconds |int |false |`30` +|_max-inflight-queue_ |Set max count of unacknowledged messages |int |false |`10` +|_max-message-size_ |Set max MQTT message size in bytes |int |false |`8092` +|_merge_ |Whether the connector should allow multiple upstreams |boolean |false |`false` +|_mqtt-version_ |Set the MQTT protocol version (4 for 3.1.1 or 5 for 5.0) |int |false |`4` +|_password_ |Set the password to connect to the server |string |false | +|_port_ |Set the MQTT server port. Default to 8883 if ssl is enabled, or 1883 without ssl |int |false | +|_qos_ |Set the QoS level when subscribing to the topic or when sending a message (0, 1, or 2) |int |false |`0` +|_reconnect-interval-seconds_ |Set the reconnect interval in seconds |int |false |`1` +|_retain_ |Whether the published message should be retained |boolean |false |`false` +|_server-name_ |Set the SNI server name |string |false | +|_session-expiry-interval_ |Set the session expiry interval in seconds (MQTT 5.0 only) |int |false | +|_ssl_ |Set whether SSL/TLS is enabled |boolean |false |`false` +|_ssl.hostname-verification-algorithm_ |Set the hostname verifier algorithm for the TLS connection.Accepted values are `HTTPS`, `LDAPS`, and `NONE` (defaults). `NONE` disables the hostname verification. |string |false |`NONE` +|_ssl.keystore.location_ |Set the keystore location. In case of `pem` type this is the server ca cert path |string |false | +|_ssl.keystore.password_ |Set the keystore password. In case of `pem` type this is the key path |string |false | +|_ssl.keystore.type_ |Set the keystore type ++[++`pkcs12`, `jks`, `pem`++]++ |string |false |`pkcs12` +|_ssl.truststore.location_ |Set the truststore location. In case of `pem` type this is the client cert path |string |false | +|_ssl.truststore.password_ |Set the truststore password. In case of `pem` type this is not necessary |string |false | +|_ssl.truststore.type_ |Set the truststore type ++[++`pkcs12`, `jks`, `pem`++]++ |string |false |`pkcs12` +|_topic_ |Set the MQTT topic. If not set, the channel name is used |string |false | +|_trust-all_ |Set whether all server certificates should be trusted |boolean |false |`false` +|_unsubscribe-on-disconnection_ |This flag restore the old behavior to unsubscribe from the broken on disconnection |boolean |false |`false` +|_username_ |Set the username to connect to the server |string |false | +|_will-flag_ |Set if will information are provided on connection |boolean |false |`false` +|_will-qos_ |Set the QoS level for the will message |int |false |`0` +|_will-retain_ |Set if the will message must be retained |boolean |false |`false` +|=== diff --git a/content/docs/smallrye-reactive-messaging/latest/_generated/smallrye-pulsar-incoming.adoc b/content/docs/smallrye-reactive-messaging/latest/_generated/smallrye-pulsar-incoming.adoc new file mode 100644 index 000000000..6d8f6f7e1 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_generated/smallrye-pulsar-incoming.adoc @@ -0,0 +1,21 @@ +[cols="<,<,^,^,<",options="header",] +|=== +|Attribute (_alias_) |Description |Type |Mandatory |Default +|_ack-strategy_ |Specify the commit strategy to apply when a message produced from a record is acknowledged. Values can be `ack`, `cumulative`. |string |false |`ack` +|_ackTimeout.redeliveryBackoff_ |Comma separated values for configuring ack timeout MultiplierRedeliveryBackoff, min delay, max delay, multiplier. |string |false | +|_batchReceive_ |Whether batch receive is used to consume messages |boolean |false |`false` +|_client-configuration_ |Identifier of a CDI bean that provides the default Pulsar client configuration for this channel. The channel configuration can still override any attribute. The bean must have a type of Map++<++String, Object++>++ and must use the @io.smallrye.common.annotation.Identifier qualifier to set the identifier. |string |false | +|_consumer-configuration_ |Identifier of a CDI bean that provides the default Pulsar consumer configuration for this channel. The channel configuration can still override any attribute. The bean must have a type of Map++<++String, Object++>++ and must use the @io.smallrye.common.annotation.Identifier qualifier to set the identifier. |string |false | +|_deadLetterPolicy.deadLetterTopic_ |Name of the dead letter topic where the failing messages will be sent |string |false | +|_deadLetterPolicy.initialSubscriptionName_ |Name of the initial subscription name of the dead letter topic |string |false | +|_deadLetterPolicy.maxRedeliverCount_ |Maximum number of times that a message will be redelivered before being sent to the dead letter topic |int |false | +|_deadLetterPolicy.retryLetterTopic_ |Name of the retry topic where the failing messages will be sent |string |false | +|_failure-strategy_ |Specify the failure strategy to apply when a message produced from a record is acknowledged negatively (nack). Values can be `nack` (default), `fail`, `ignore` or ++`++reconsume-later |string |false |`nack` +|_health-enabled_ |Whether health reporting is enabled (default) or disabled |boolean |false |`true` +|_negativeAck.redeliveryBackoff_ |Comma separated values for configuring negative ack MultiplierRedeliveryBackoff, min delay, max delay, multiplier. |string |false | +|_reconsumeLater.delay_ |Default delay for reconsume failure-strategy, in seconds |long |false |`3` +|_schema_ |The Pulsar schema type of this channel. When configured a schema is built with the given SchemaType and used for the channel. When absent, the schema is resolved searching for a CDI bean typed `Schema` qualified with `@Identifier` and the channel name. As a fallback AUTO++_++CONSUME or AUTO++_++PRODUCE are used. |string |false | +|_serviceUrl_ |The service URL for the Pulsar service |string |false |`pulsar://localhost:6650` +|_topic_ |The consumed / populated Pulsar topic. If not set, the channel name is used |string |false | +|_tracing-enabled_ |Whether tracing is enabled (default) or disabled |boolean |false |`true` +|=== diff --git a/content/docs/smallrye-reactive-messaging/latest/_generated/smallrye-pulsar-outgoing.adoc b/content/docs/smallrye-reactive-messaging/latest/_generated/smallrye-pulsar-outgoing.adoc new file mode 100644 index 000000000..9d44f7b1d --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_generated/smallrye-pulsar-outgoing.adoc @@ -0,0 +1,13 @@ +[cols="<,<,^,^,<",options="header",] +|=== +|Attribute (_alias_) |Description |Type |Mandatory |Default +|_client-configuration_ |Identifier of a CDI bean that provides the default Pulsar client configuration for this channel. The channel configuration can still override any attribute. The bean must have a type of Map++<++String, Object++>++ and must use the @io.smallrye.common.annotation.Identifier qualifier to set the identifier. |string |false | +|_health-enabled_ |Whether health reporting is enabled (default) or disabled |boolean |false |`true` +|_max-inflight-messages_ |The maximum size of a queue holding pending messages, i.e messages waiting to receive an acknowledgment from a broker. Defaults to 1000 messages |int |false | +|_producer-configuration_ |Identifier of a CDI bean that provides the default Pulsar producer configuration for this channel. The channel configuration can still override any attribute. The bean must have a type of Map++<++String, Object++>++ and must use the @io.smallrye.common.annotation.Identifier qualifier to set the identifier. |string |false | +|_schema_ |The Pulsar schema type of this channel. When configured a schema is built with the given SchemaType and used for the channel. When absent, the schema is resolved searching for a CDI bean typed `Schema` qualified with `@Identifier` and the channel name. As a fallback AUTO++_++CONSUME or AUTO++_++PRODUCE are used. |string |false | +|_serviceUrl_ |The service URL for the Pulsar service |string |false |`pulsar://localhost:6650` +|_topic_ |The consumed / populated Pulsar topic. If not set, the channel name is used |string |false | +|_tracing-enabled_ |Whether tracing is enabled (default) or disabled |boolean |false |`true` +|_waitForWriteCompletion_ |Whether the client waits for the broker to acknowledge the written record before acknowledging the message |boolean |false |`true` +|=== diff --git a/content/docs/smallrye-reactive-messaging/latest/_generated/smallrye-sns-outgoing.adoc b/content/docs/smallrye-reactive-messaging/latest/_generated/smallrye-sns-outgoing.adoc new file mode 100644 index 000000000..c25ed7a4f --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_generated/smallrye-sns-outgoing.adoc @@ -0,0 +1,17 @@ +[cols="<,<,^,^,<",options="header",] +|=== +|Attribute (_alias_) |Description |Type |Mandatory |Default +|_batch_ |When set, sends messages in batches of maximum 10 messages |boolean |false |`false` +|_batch-delay_ |In batch send mode, the maximum delay in milliseconds to wait for messages to be included in the batch. Defaults to 3000 ms |int |false |`3000` +|_batch-size_ |In batch send mode, the maximum number of messages to include in batch, currently SNS maximum is 10 messages |int |false |`10` +|_credentials-provider_ |The credential provider to be used in the client, defaults to AWS default provider chain |string |false | +|_email.subject_ |When set, sends messages with the specified subject |string |false | +|_endpoint-override_ |The endpoint override |string |false | +|_group.id_ |When set, sends messages with the specified group id |string |false | +|_health-enabled_ |Whether health reporting is enabled (default) or disabled |boolean |false |`true` +|_message-structure_ |Set to json if you want to send a different message for each protocol. |string |false | +|_region_ |The name of the SNS region, defaults to AWS region resolver |string |false | +|_sms.phoneNumber_ |When set, sends messages to the specified phone number. Not supported in combination with batching. |string |false | +|_topic_ |The name of the SNS topic, defaults to channel name if not provided |string |false | +|_topic.arn_ |The arn of the SNS topic |string |false | +|=== diff --git a/content/docs/smallrye-reactive-messaging/latest/_generated/smallrye-sqs-incoming.adoc b/content/docs/smallrye-reactive-messaging/latest/_generated/smallrye-sqs-incoming.adoc new file mode 100644 index 000000000..b461eea07 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_generated/smallrye-sqs-incoming.adoc @@ -0,0 +1,21 @@ +[cols="<,<,^,^,<",options="header",] +|=== +|Attribute (_alias_) |Description |Type |Mandatory |Default +|_ack-strategy_ |The identifier for the bean implementing ack strategy factory. Strategies: 'delete', 'ignore' |string |false |`delete` +|_ack.delete_ |_deprecated_ - Whether the acknowledgement deletes the message from the queue. Deprecated, use ack-strategy instead |boolean |false | +|_credentials-provider_ |The credential provider to be used in the client |string |false | +|_endpoint-override_ |The endpoint override |string |false | +|_failure-strategy_ |The identifier for the bean implementing failure strategy factory. Strategies: 'ignore', 'fail', 'visibility', 'delete' |string |false |`ignore` +|_health-enabled_ |Whether health reporting is enabled (default) or disabled |boolean |false |`true` +|_max-number-of-messages_ |The maximum number of messages to receive |int |false |`10` +|_queue_ |The name of the SQS queue, defaults to channel name if not provided |string |false | +|_queue.url_ |The url of the SQS queue |string |false | +|_receive.request.customizer_ |The identifier for the bean implementing a customizer to receive requests, defaults to channel name if not provided |string |false | +|_receive.request.message-attribute-names_ |The message attribute names to retrieve when receiving messages. |string |false | +|_receive.request.pause.resume_ |Whether the polling must be paused when the application does not request items and resume when it does. This allows implementing back-pressure based on the application capacity. Note that polling is not stopped, but will not retrieve any records when paused. |boolean |false |`true` +|_receive.request.retries_ |If set to a positive number, the connector will try to retry the request that was not delivered successfully (with a potentially transient error) until the number of retries is reached. If set to 0, retries are disabled. |long |false |`2147483647` +|_region_ |The name of the SQS region |string |false | +|_tracing-enabled_ |Whether tracing is enabled (default) or disabled |boolean |false |`true` +|_visibility-timeout_ |The duration in seconds that the received messages are hidden from subsequent retrieve requests after being retrieved by a receive request |int |false | +|_wait-time-seconds_ |The maximum amount of time in seconds to wait for messages to be received |int |false |`1` +|=== diff --git a/content/docs/smallrye-reactive-messaging/latest/_generated/smallrye-sqs-outgoing.adoc b/content/docs/smallrye-reactive-messaging/latest/_generated/smallrye-sqs-outgoing.adoc new file mode 100644 index 000000000..28e23dc2c --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_generated/smallrye-sqs-outgoing.adoc @@ -0,0 +1,15 @@ +[cols="<,<,^,^,<",options="header",] +|=== +|Attribute (_alias_) |Description |Type |Mandatory |Default +|_batch_ |When set, sends messages in batches of maximum 10 messages |boolean |false |`false` +|_batch-delay_ |In batch send mode, the maximum delay in milliseconds to wait for messages to be included in the batch |int |false |`3000` +|_batch-size_ |In batch send mode, the maximum number of messages to include in batch, currently SQS maximum is 10 messages |int |false |`10` +|_credentials-provider_ |The credential provider to be used in the client |string |false | +|_endpoint-override_ |The endpoint override |string |false | +|_group.id_ |When set, sends messages with the specified group id |string |false | +|_health-enabled_ |Whether health reporting is enabled (default) or disabled |boolean |false |`true` +|_queue_ |The name of the SQS queue, defaults to channel name if not provided |string |false | +|_queue.url_ |The url of the SQS queue |string |false | +|_region_ |The name of the SQS region |string |false | +|_tracing-enabled_ |Whether tracing is enabled (default) or disabled |boolean |false |`true` +|=== diff --git a/content/docs/smallrye-reactive-messaging/latest/_nav.yml b/content/docs/smallrye-reactive-messaging/latest/_nav.yml new file mode 100644 index 000000000..6c52e73e5 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_nav.yml @@ -0,0 +1,192 @@ +title: SmallRye Reactive Messaging +nav: +- title: Home + page: index +- title: Core + items: + - title: Getting Started + page: getting-started + - title: Concepts + items: + - title: Introduction + page: concepts/concepts + - title: Development Model + page: concepts/model + - title: Emitters and Channel + page: concepts/emitter + - title: Connectors + page: concepts/connectors + - title: Contributing Connectors + page: concepts/contributing-connectors + - title: Acknowledgement + page: concepts/acknowledgement + - title: Blocking Processing + page: concepts/blocking + - title: Method Signatures + page: concepts/signatures + - title: Skipping Messages + page: concepts/skipping + - title: Message Converters + page: concepts/converters + - title: Keyed Streams + page: concepts/keyed-multi + - title: Channel Decorators and Interceptors + page: concepts/decorators + - title: Broadcast + page: concepts/broadcast + - title: Merge channels + page: concepts/merge + - title: Incoming Channel Concurrency + page: concepts/incoming-concurrency + - title: '@Incomings' + page: concepts/incomings + - title: '@Outgoings' + page: concepts/outgoings + - title: Testing + page: concepts/testing + - title: Logging + page: concepts/logging + - title: Observability API + page: concepts/observability + - title: Advanced Configuration + page: concepts/advanced-config + - title: Message Context + page: concepts/message-context + - title: Metadata Injection + page: concepts/incoming-metadata-injection + - title: Generic Payloads + page: concepts/generic-payloads + - title: Pausable Channels + page: concepts/pausable-channels +- title: Kafka + items: + - page: kafka/kafka + - title: Receiving records + page: kafka/receiving-kafka-records + - title: Writing records + page: kafka/writing-kafka-records + - title: Health Checks + page: kafka/health + - title: Using Avro + page: kafka/avro-configuration + - title: Using Protobuf + page: kafka/protobuf-configuration + - title: Rebalance Listeners + page: kafka/consumer-rebalance-listener + - title: Kerberos authentication + page: kafka/kerberos + - title: Accessing the client + page: kafka/client-service + - title: Customizing Default Kafka Configuration + page: kafka/default-configuration + - title: Test Companion for Kafka + page: kafka/test-companion + - title: Kafka Transactions and Exactly-Once Processing + page: kafka/transactions + - title: Kafka Request/Reply + page: kafka/request-reply +- title: AMQP 1.0 + items: + - page: amqp/amqp + - title: Receiving messages + page: amqp/receiving-amqp-messages + - title: Sending messages + page: amqp/sending-amqp-messages + - title: AMQP Request/Reply + page: amqp/request-reply + - title: CBS Authentication + page: amqp/cbs-authentication + - title: Health Checks + page: amqp/health + - title: Client Customization + page: amqp/client-customization + - title: Using RabbitMQ + page: amqp/rabbitmq +- title: RabbitMQ + items: + - page: rabbitmq/rabbitmq + - title: Receiving messages + page: rabbitmq/receiving-messages-from-rabbitmq + - title: Sending messages + page: rabbitmq/sending-messages-to-rabbitmq + - title: Health Checks + page: rabbitmq/rabbitmq-health + - title: Client Customization + page: rabbitmq/rabbitmq-client-customization + - title: Connecting to managed instances + page: rabbitmq/rabbitmq-cloud + - title: RabbitMQ Request/Reply + page: rabbitmq/request-reply +- title: RabbitMQ OG + items: + - page: rabbitmq-og/rabbitmq-og + - title: Receiving messages + page: rabbitmq-og/receiving-messages-from-rabbitmq + - title: Sending messages + page: rabbitmq-og/sending-messages-to-rabbitmq + - title: Health Checks + page: rabbitmq-og/rabbitmq-og-health + - title: Client Customization + page: rabbitmq-og/rabbitmq-og-client-customization + - title: OpenTelemetry Tracing + page: rabbitmq-og/rabbitmq-og-tracing + - title: Connecting to managed instances + page: rabbitmq-og/rabbitmq-og-cloud +- title: Pulsar + items: + - page: pulsar/pulsar + - title: Receiving messages + page: pulsar/receiving-pulsar-messages + - title: Sending messages + page: pulsar/sending-messages-to-pulsar + - title: Configuring the Schema + page: pulsar/schema-configuration + - title: Configuring the Pulsar client + page: pulsar/client-configuration + - title: Health Checks + page: pulsar/health + - title: Accessing the client + page: pulsar/client-service + - title: Pulsar Transactions and Exactly-Once Processing + page: pulsar/transactions +- title: Apache Camel + items: + - page: camel/camel + - title: Receiving messages from Camel + page: camel/receiving-messages-from-camel + - title: Sending messages to Camel + page: camel/sending-messages-to-camel + - title: Implementing Camel processor + page: camel/camel-processor + - title: Using existing Camel routes + page: camel/using-existing-routes +- title: JMS + items: + - page: jms/jms + - title: Receiving JMS messages + page: jms/receiving-jms-messages + - title: Sending JMS messages + page: jms/sending-jms-messages + - title: Advanced configuration + page: jms/advanced-jms +- title: MQTT + items: + - page: mqtt/mqtt + - title: Receiving MQTT messages + page: mqtt/receiving-mqtt-messages + - title: Sending MQTT messages + page: mqtt/sending-messages-to-mqtt + - title: Customizing the MQTT client + page: mqtt/client-customization +- title: AWS SQS + items: + - page: sqs/sqs + - title: Receiving AWS SQS messages + page: sqs/receiving-aws-sqs-messages + - title: Sending AWS SQS messages + page: sqs/sending-aws-sqs-messages +- title: AWS SNS + items: + - page: sns/sns + - title: Sending AWS SNS messages + page: sns/sending-aws-sns-messages diff --git a/content/docs/smallrye-reactive-messaging/latest/_partials/pulsar/config/smallrye-pulsar-client.adoc b/content/docs/smallrye-reactive-messaging/latest/_partials/pulsar/config/smallrye-pulsar-client.adoc new file mode 100644 index 000000000..513f4c895 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_partials/pulsar/config/smallrye-pulsar-client.adoc @@ -0,0 +1,58 @@ +[width="100%",cols="<13%,<42%,^17%,^17%,<11%",options="header",] +|=== +|Attribute |Description |Type |Config file |Default +|_serviceUrl_ |Pulsar cluster HTTP URL to connect to a broker. |String |true | +|_serviceUrlProvider_ |The implementation class of ServiceUrlProvider used to generate ServiceUrl. |ServiceUrlProvider |false | +|_authentication_ |Authentication settings of the client. |Authentication |false | +|_authPluginClassName_ |Class name of authentication plugin of the client. |String |true | +|_authParams_ |Authentication parameter of the client. |String |true | +|_authParamMap_ |Authentication map of the client. |Map |true | +|_operationTimeoutMs_ |Client operation timeout (in milliseconds). |long |true |30000 +|_lookupTimeoutMs_ |Client lookup timeout (in milliseconds). |long |true |-1 +|_statsIntervalSeconds_ |Interval to print client stats (in seconds). |long |true |60 +|_numIoThreads_ |Number of IO threads. |int |true |10 +|_numListenerThreads_ |Number of consumer listener threads. |int |true |10 +|_connectionsPerBroker_ |Number of connections established between the client and each Broker. A value of 0 means to disable connection pooling. |int |true |1 +|_connectionMaxIdleSeconds_ |Release the connection if it is not used for more than ++[++connectionMaxIdleSeconds++]++ seconds. If ++[++connectionMaxIdleSeconds++]++ ++<++ 0, disabled the feature that auto release the idle connections |int |true |180 +|_useTcpNoDelay_ |Whether to use TCP NoDelay option. |boolean |true |true +|_useTls_ |Whether to use TLS. |boolean |true |false +|_tlsKeyFilePath_ |Path to the TLS key file. |String |true | +|_tlsCertificateFilePath_ |Path to the TLS certificate file. |String |true | +|_tlsTrustCertsFilePath_ |Path to the trusted TLS certificate file. |String |true | +|_tlsAllowInsecureConnection_ |Whether the client accepts untrusted TLS certificates from the broker. |boolean |true |false +|_tlsHostnameVerificationEnable_ |Whether the hostname is validated when the client creates a TLS connection with brokers. |boolean |true |false +|_concurrentLookupRequest_ |The number of concurrent lookup requests that can be sent on each broker connection. Setting a maximum prevents overloading a broker. |int |true |5000 +|_maxLookupRequest_ |Maximum number of lookup requests allowed on each broker connection to prevent overloading a broker. |int |true |50000 +|_maxLookupRedirects_ |Maximum times of redirected lookup requests. |int |true |20 +|_maxNumberOfRejectedRequestPerConnection_ |Maximum number of rejected requests of a broker in a certain time frame (60 seconds) after the current connection is closed and the client creating a new connection to connect to a different broker. |int |true |50 +|_keepAliveIntervalSeconds_ |Seconds of keeping alive interval for each client broker connection. |int |true |30 +|_connectionTimeoutMs_ |Duration of waiting for a connection to a broker to be established.If the duration passes without a response from a broker, the connection attempt is dropped. |int |true |10000 +|_requestTimeoutMs_ |Maximum duration for completing a request. |int |true |60000 +|_readTimeoutMs_ |Maximum read time of a request. |int |true |60000 +|_autoCertRefreshSeconds_ |Seconds of auto refreshing certificate. |int |true |300 +|_initialBackoffIntervalNanos_ |Initial backoff interval (in nanosecond). |long |true |100000000 +|_maxBackoffIntervalNanos_ |Max backoff interval (in nanosecond). |long |true |60000000000 +|_enableBusyWait_ |Whether to enable BusyWait for EpollEventLoopGroup. |boolean |true |false +|_listenerName_ |Listener name for lookup. Clients can use listenerName to choose one of the listeners as the service URL to create a connection to the broker as long as the network is accessible."advertisedListeners" must enabled in broker side. |String |true | +|_useKeyStoreTls_ |Set TLS using KeyStore way. |boolean |true |false +|_sslProvider_ |The TLS provider used by an internal client to authenticate with other Pulsar brokers. |String |true | +|_tlsKeyStoreType_ |TLS KeyStore type configuration. |String |true |JKS +|_tlsKeyStorePath_ |Path of TLS KeyStore. |String |true | +|_tlsKeyStorePassword_ |Password of TLS KeyStore. |String |true | +|_tlsTrustStoreType_ |TLS TrustStore type configuration. You need to set this configuration when client authentication is required. |String |true |JKS +|_tlsTrustStorePath_ |Path of TLS TrustStore. |String |true | +|_tlsTrustStorePassword_ |Password of TLS TrustStore. |String |true | +|_tlsCiphers_ |Set of TLS Ciphers. |Set |true |++[]++ +|_tlsProtocols_ |Protocols of TLS. |Set |true |++[]++ +|_memoryLimitBytes_ |Limit of client memory usage (in byte). The 64M default can guarantee a high producer throughput. |long |true |67108864 +|_proxyServiceUrl_ |URL of proxy service. proxyServiceUrl and proxyProtocol must be mutually inclusive. |String |true | +|_proxyProtocol_ |Protocol of proxy service. proxyServiceUrl and proxyProtocol must be mutually inclusive. |ProxyProtocol |true | +|_enableTransaction_ |Whether to enable transaction. |boolean |true |false +|_clock_ | |Clock |false | +|_dnsLookupBindAddress_ |The Pulsar client dns lookup bind address, default behavior is bind on 0.0.0.0 |String |true | +|_dnsLookupBindPort_ |The Pulsar client dns lookup bind port, takes effect when dnsLookupBindAddress is configured, default value is 0. |int |true |0 +|_socks5ProxyAddress_ |Address of SOCKS5 proxy. |InetSocketAddress |true | +|_socks5ProxyUsername_ |User name of SOCKS5 proxy. |String |true | +|_socks5ProxyPassword_ |Password of SOCKS5 proxy. |String |true | +|_description_ |The extra description of the client version. The length cannot exceed 64. |String |true | +|=== diff --git a/content/docs/smallrye-reactive-messaging/latest/_partials/pulsar/config/smallrye-pulsar-consumer.adoc b/content/docs/smallrye-reactive-messaging/latest/_partials/pulsar/config/smallrye-pulsar-consumer.adoc new file mode 100644 index 000000000..1f4b499aa --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_partials/pulsar/config/smallrye-pulsar-consumer.adoc @@ -0,0 +1,137 @@ +[width="100%",cols="<13%,<42%,^17%,^17%,<11%",options="header",] +|=== +|Attribute |Description |Type |Config file |Default +|_topicNames_ |Topic name |Set |true |++[]++ +|_topicsPattern_ |Topic pattern |Pattern |true | +|_subscriptionName_ |Subscription name |String |true | +|_subscriptionType_ |Subscription type. + +Four subscription types are available: + +++*++ Exclusive + +++*++ Failover + +++*++ Shared + +++*++ Key++_++Shared |SubscriptionType |true |Exclusive +|_subscriptionProperties_ | |Map |true | +|_subscriptionMode_ | |SubscriptionMode |true |Durable +|_messageListener_ | |MessageListener |false | +|_consumerEventListener_ | |ConsumerEventListener |false | +|_negativeAckRedeliveryBackoff_ |Interface for custom message is negativeAcked policy. You can specify `RedeliveryBackoff` for a consumer. |RedeliveryBackoff |false | +|_ackTimeoutRedeliveryBackoff_ |Interface for custom message is ackTimeout policy. You can specify `RedeliveryBackoff` for a consumer. |RedeliveryBackoff |false | +|_receiverQueueSize_ |Size of a consumer's receiver queue. + + + +For example, the number of messages accumulated by a consumer before an application calls `Receive`. + + + +A value higher than the default value increases consumer throughput, though at the expense of more memory utilization. |int |true |1000 +|_acknowledgementsGroupTimeMicros_ |Group a consumer acknowledgment for a specified time. + + + +By default, a consumer uses 100ms grouping time to send out acknowledgments to a broker. + + + +Setting a group time of 0 sends out acknowledgments immediately. + + + +A longer ack group time is more efficient at the expense of a slight increase in message re-deliveries after a failure. |long |true |100000 +|_maxAcknowledgmentGroupSize_ |Group a consumer acknowledgment for the number of messages. |int |true |1000 +|_negativeAckRedeliveryDelayMicros_ |Delay to wait before redelivering messages that failed to be processed. + + + +When an application uses `Consumer#negativeAcknowledge(Message)`, failed messages are redelivered after a fixed timeout. |long |true |60000000 +|_maxTotalReceiverQueueSizeAcrossPartitions_ |The max total receiver queue size across partitions. + + + +This setting reduces the receiver queue size for individual partitions if the total receiver queue size exceeds this value. |int |true |50000 +|_consumerName_ |Consumer name |String |true | +|_ackTimeoutMillis_ |Timeout of unacked messages |long |true |0 +|_tickDurationMillis_ |Granularity of the ack-timeout redelivery. + + + +Using an higher `tickDurationMillis` reduces the memory overhead to track messages when setting ack-timeout to a bigger value (for example, 1 hour). |long |true |1000 +|_priorityLevel_ |Priority level for a consumer to which a broker gives more priority while dispatching messages in Shared subscription type. + + + +The broker follows descending priorities. For example, 0=max-priority, 1, 2,... + + + +In Shared subscription type, the broker *first dispatches messages to the max priority level consumers if they have permits*. Otherwise, the broker considers next priority level consumers. + + + +**Example 1** + +If a subscription has consumerA with `priorityLevel` 0 and consumerB with `priorityLevel` 1, then the broker *only dispatches messages to consumerA until it runs out permits* and then starts dispatching messages to consumerB. + + + +**Example 2** + +Consumer Priority, Level, Permits + +C1, 0, 2 + +C2, 0, 1 + +C3, 0, 1 + +C4, 1, 2 + +C5, 1, 1 + + + +Order in which a broker dispatches messages to consumers is: C1, C2, C3, C1, C4, C5, C4. |int |true |0 +|_maxPendingChunkedMessage_ |The maximum size of a queue holding pending chunked messages. When the threshold is reached, the consumer drops pending messages to optimize memory utilization. |int |true |10 +|_autoAckOldestChunkedMessageOnQueueFull_ |Whether to automatically acknowledge pending chunked messages when the threshold of `maxPendingChunkedMessage` is reached. If set to `false`, these messages will be redelivered by their broker. |boolean |true |false +|_expireTimeOfIncompleteChunkedMessageMillis_ |The time interval to expire incomplete chunks if a consumer fails to receive all the chunks in the specified time period. The default value is 1 minute. |long |true |60000 +|_cryptoKeyReader_ | |CryptoKeyReader |false | +|_messageCrypto_ | |MessageCrypto |false | +|_cryptoFailureAction_ |Consumer should take action when it receives a message that can not be decrypted. + +++*++ *FAIL*: this is the default option to fail messages until crypto succeeds. + +++*++ *DISCARD*:silently acknowledge and not deliver message to an application. + +++*++ *CONSUME*: deliver encrypted messages to applications. It is the application's responsibility to decrypt the message. + + + +The decompression of message fails. + + + +If messages contain batch messages, a client is not be able to retrieve individual messages in batch. + + + +Delivered encrypted message contains `EncryptionContext` which contains encryption and compression information in it using which application can decrypt consumed message payload. |ConsumerCryptoFailureAction |true |FAIL +|_properties_ |A name or value property of this consumer. + + + +`properties` is application defined metadata attached to a consumer. + + + +When getting a topic stats, associate this metadata with the consumer stats for easier identification. |SortedMap |true |++{++} +|_readCompacted_ |If enabling `readCompacted`, a consumer reads messages from a compacted topic rather than reading a full message backlog of a topic. + + + +A consumer only sees the latest value for each key in the compacted topic, up until reaching the point in the topic message when compacting backlog. Beyond that point, send messages as normal. + + + +Only enabling `readCompacted` on subscriptions to persistent topics, which have a single active consumer (like failure or exclusive subscriptions). + + + +Attempting to enable it on subscriptions to non-persistent topics or on shared subscriptions leads to a subscription call throwing a `PulsarClientException`. |boolean |true |false +|_subscriptionInitialPosition_ |Initial position at which to set cursor when subscribing to a topic at first time. |SubscriptionInitialPosition |true |Latest +|_patternAutoDiscoveryPeriod_ |Topic auto discovery period when using a pattern for topic's consumer. + + + +The default and minimum value is 1 minute. |int |true |60 +|_regexSubscriptionMode_ |When subscribing to a topic using a regular expression, you can pick a certain type of topics. + + + +++*++ *PersistentOnly*: only subscribe to persistent topics. + +++*++ *NonPersistentOnly*: only subscribe to non-persistent topics. + +++*++ *AllTopics*: subscribe to both persistent and non-persistent topics. |RegexSubscriptionMode |true |PersistentOnly +|_deadLetterPolicy_ |Dead letter policy for consumers. + + + +By default, some messages are probably redelivered many times, even to the extent that it never stops. + + + +By using the dead letter mechanism, messages have the max redelivery count. *When exceeding the maximum number of redeliveries, messages are sent to the Dead Letter Topic and acknowledged automatically*. + + + +You can enable the dead letter mechanism by setting `deadLetterPolicy`. + + + +**Example** + + + +client.newConsumer() + +.deadLetterPolicy(DeadLetterPolicy.builder().maxRedeliverCount(10).build()) + +.subscribe(); + + + +Default dead letter topic name is `++{++TopicName}-++{++Subscription}-DLQ`. + + + +To set a custom dead letter topic name: + + + +client.newConsumer() + +.deadLetterPolicy(DeadLetterPolicy.builder().maxRedeliverCount(10) + +.deadLetterTopic("your-topic-name").build()) + +.subscribe(); + + + +When specifying the dead letter policy while not specifying `ackTimeoutMillis`, you can set the ack timeout to 30000 millisecond. |DeadLetterPolicy |true | +|_retryEnable_ | |boolean |true |false +|_batchReceivePolicy_ | |BatchReceivePolicy |false | +|_autoUpdatePartitions_ |If `autoUpdatePartitions` is enabled, a consumer subscribes to partition increasement automatically. + + + +**Note**: this is only for partitioned consumers. |boolean |true |true +|_autoUpdatePartitionsIntervalSeconds_ | |long |true |60 +|_replicateSubscriptionState_ |If `replicateSubscriptionState` is enabled, a subscription state is replicated to geo-replicated clusters. |boolean |true |false +|_resetIncludeHead_ | |boolean |true |false +|_keySharedPolicy_ | |KeySharedPolicy |false | +|_batchIndexAckEnabled_ | |boolean |true |false +|_ackReceiptEnabled_ | |boolean |true |false +|_poolMessages_ | |boolean |true |false +|_payloadProcessor_ | |MessagePayloadProcessor |false | +|_startPaused_ | |boolean |true |false +|_autoScaledReceiverQueueSizeEnabled_ | |boolean |true |false +|_topicConfigurations_ | |List |true |++[]++ +|=== diff --git a/content/docs/smallrye-reactive-messaging/latest/_partials/pulsar/config/smallrye-pulsar-producer.adoc b/content/docs/smallrye-reactive-messaging/latest/_partials/pulsar/config/smallrye-pulsar-producer.adoc new file mode 100644 index 000000000..e8755e86b --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/_partials/pulsar/config/smallrye-pulsar-producer.adoc @@ -0,0 +1,60 @@ +[width="100%",cols="<13%,<42%,^17%,^17%,<11%",options="header",] +|=== +|Attribute |Description |Type |Config file |Default +|_topicName_ |Topic name |String |true | +|_producerName_ |Producer name |String |true | +|_sendTimeoutMs_ |Message send timeout in ms. + +If a message is not acknowledged by a server before the `sendTimeout` expires, an error occurs. |long |true |30000 +|_blockIfQueueFull_ |If it is set to `true`, when the outgoing message queue is full, the `Send` and `SendAsync` methods of producer block, rather than failing and throwing errors. + +If it is set to `false`, when the outgoing message queue is full, the `Send` and `SendAsync` methods of producer fail and `ProducerQueueIsFullError` exceptions occur. + + + +The `MaxPendingMessages` parameter determines the size of the outgoing message queue. |boolean |true |false +|_maxPendingMessages_ |The maximum size of a queue holding pending messages. + + + +For example, a message waiting to receive an acknowledgment from a https://pulsar.apache.org/docs/reference-terminology#broker[broker]. + + + +By default, when the queue is full, all calls to the `Send` and `SendAsync` methods fail *unless* you set `BlockIfQueueFull` to `true`. |int |true |0 +|_maxPendingMessagesAcrossPartitions_ |The maximum number of pending messages across partitions. + + + +Use the setting to lower the max pending messages for each partition (`#setMaxPendingMessages(int)`) if the total number exceeds the configured value. |int |true |0 +|_messageRoutingMode_ |Message routing logic for producers on https://pulsar.apache.org/docs/concepts-architecture-overview#partitioned-topics[partitioned topics]. + +Apply the logic only when setting no key on messages. + +Available options are as follows: + +++*++ `pulsar.RoundRobinDistribution`: round robin + +++*++ `pulsar.UseSinglePartition`: publish all messages to a single partition + +++*++ `pulsar.CustomPartition`: a custom partitioning scheme |MessageRoutingMode |true | +|_hashingScheme_ |Hashing function determining the partition where you publish a particular message (partitioned topics only). + +Available options are as follows: + +++*++ `pulsar.JavastringHash`: the equivalent of `string.hashCode()` in Java + +++*++ `pulsar.Murmur3++_++32Hash`: applies the https://en.wikipedia.org/wiki/MurmurHash[Murmur3] hashing function + +++*++ `pulsar.BoostHash`: applies the hashing function from C{plus}{plus}'shttps://www.boost.org/doc/libs/1_62_0/doc/html/hash.html[Boost] library |HashingScheme |true |JavaStringHash +|_cryptoFailureAction_ |Producer should take action when encryption fails. + +++*++ *FAIL*: if encryption fails, unencrypted messages fail to send. + +++*++ *SEND*: if encryption fails, unencrypted messages are sent. |ProducerCryptoFailureAction |true |FAIL +|_customMessageRouter_ | |MessageRouter |false | +|_batchingMaxPublishDelayMicros_ |Batching time period of sending messages. |long |true |1000 +|_batchingPartitionSwitchFrequencyByPublishDelay_ | |int |true |10 +|_batchingMaxMessages_ |The maximum number of messages permitted in a batch. |int |true |1000 +|_batchingMaxBytes_ | |int |true |131072 +|_batchingEnabled_ |Enable batching of messages. |boolean |true |true +|_batcherBuilder_ | |BatcherBuilder |false | +|_chunkingEnabled_ |Enable chunking of messages. |boolean |true |false +|_chunkMaxMessageSize_ | |int |true |-1 +|_cryptoKeyReader_ | |CryptoKeyReader |false | +|_messageCrypto_ | |MessageCrypto |false | +|_encryptionKeys_ | |Set |true |++[]++ +|_compressionType_ |Message data compression type used by a producer. + +Available options: + +++*++ https://github.com/lz4/lz4[LZ4] + +++*++ https://zlib.net/[ZLIB] + +++*++ https://facebook.github.io/zstd/[ZSTD] + +++*++ https://google.github.io/snappy/[SNAPPY] |CompressionType |true |NONE +|_initialSequenceId_ | |Long |true | +|_autoUpdatePartitions_ | |boolean |true |true +|_autoUpdatePartitionsIntervalSeconds_ | |long |true |60 +|_multiSchema_ | |boolean |true |true +|_accessMode_ | |ProducerAccessMode |true |Shared +|_lazyStartPartitionedProducers_ | |boolean |true |false +|_properties_ | |SortedMap |true |++{++} +|_initialSubscriptionName_ |Use this configuration to automatically create an initial subscription when creating a topic. If this field is not set, the initial subscription is not created. |String |true | +|=== diff --git a/content/docs/smallrye-reactive-messaging/latest/amqp/amqp.adoc b/content/docs/smallrye-reactive-messaging/latest/amqp/amqp.adoc new file mode 100644 index 000000000..d4326ffeb --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/amqp/amqp.adoc @@ -0,0 +1,51 @@ += AMQP 1.0 Connector +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The AMQP Connector adds support for AMQP 1.0 to Reactive Messaging. + +Advanced Message Queuing Protocol 1.0 (AMQP 1.0) is an open standard for +passing business messages between applications or organizations. + +With this connector, your application can: + +* receive messages from an AMQP Broker or Router. +* send `Message` to an AMQP _address_ +* implement xref:amqp/request-reply.adoc[request-reply] messaging patterns +* authenticate using xref:amqp/cbs-authentication.adoc[Claims-Based Security (CBS)] tokens + +The AMQP connector is based on the https://vertx.io/docs/vertx-amqp-client/java/[Vert.x AMQP Client]. + +Using the AMQP connector + +To use the AMQP Connector, add the following dependency to your project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.reactive + smallrye-reactive-messaging-amqp + {project-version} + +---- + +The connector name is: `smallrye-amqp`. + +So, to indicate that a channel is managed by this connector you need: + +[source,properties] +---- +# Inbound +mp.messaging.incoming.[channel-name].connector=smallrye-amqp + +# Outbound +mp.messaging.outgoing.[channel-name].connector=smallrye-amqp +---- + +[IMPORTANT] +.RabbitMQ +==== + +To use RabbitMQ, refer to xref:#amqp-rabbitmq[Using RabbitMQ]. + +==== diff --git a/content/docs/smallrye-reactive-messaging/latest/amqp/cbs-authentication.adoc b/content/docs/smallrye-reactive-messaging/latest/amqp/cbs-authentication.adoc new file mode 100644 index 000000000..5b5fd6132 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/amqp/cbs-authentication.adoc @@ -0,0 +1,186 @@ += Claims-Based Security (CBS) Authentication +:version-root: ../ +include::{version-root}_attributes.adoc[] + +[WARNING] +.Experimental +==== + +CBS authentication support is an experimental feature. + +==== + +The AMQP connector supports https://docs.oasis-open.org/amqp/amqp-cbs/v1.0/amqp-cbs-v1.0.html[Claims-Based Security (CBS)] for token-based authentication with AMQP 1.0 brokers. +CBS allows the client to authenticate by sending a security token (such as a JWT or SAS token) to the broker over the AMQP connection, rather than relying solely on SASL mechanisms. +This is commonly used with brokers like *Azure Service Bus*. + +== Enabling CBS + +To enable CBS authentication on a channel, set the `cbs.enabled` attribute: + +[source,properties] +---- +mp.messaging.incoming.my-channel.connector=smallrye-amqp +mp.messaging.incoming.my-channel.host=my-broker.servicebus.windows.net +mp.messaging.incoming.my-channel.port=5671 +mp.messaging.incoming.my-channel.use-ssl=true +mp.messaging.incoming.my-channel.cbs.enabled=true +---- + +CBS can be enabled on both incoming and outgoing channels independently. + +== Providing Tokens + +When CBS is enabled, the connector requires a CDI bean implementing +link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-amqp/{javadoc-version}/io/smallrye/reactive/messaging/amqp/cbs/CbsTokenProvider.html[`CbsTokenProvider`]. +This bean is called during initial connection and on each token refresh cycle. + +[source,java] +---- +include::{examples-dir}/amqp/customization/MyCbsTokenProvider.java[tags=provider,indent=0] +---- + +The `getToken` method receives the channel configuration, allowing you to adapt token generation per channel (e.g., using different audiences or scopes). +It returns a `Uni++<++CbsToken++>++` to support non-blocking token retrieval from external identity providers. + +=== The CbsToken Interface + +A `CbsToken` provides: + +[width="100%",cols="16%,84%",options="header",] +|=== +|Method |Description +|`token()` |The token string value (JWT, SAS token, etc.) +|`type()` |The token type (e.g., `"jwt"` or `"servicebus.windows.net:sastoken"`) +|`expiresAt()` |When the token expires +|`refreshAt()` |When the token should be refreshed (defaults to `expiresAt()` if not set) +|=== + +The `DefaultCbsToken` record provides a convenient implementation: + +[source,java] +---- +// With explicit refresh time (refresh before expiration) +new DefaultCbsToken(token, "jwt", expiresAt, refreshAt); + +// Refresh at expiration +new DefaultCbsToken(token, "jwt", expiresAt); + +// Using validity duration in seconds +new DefaultCbsToken(token, "jwt", 3600); +---- + +== Token Refresh + +The connector automatically refreshes tokens before they expire. +After a successful authorization, a timer is scheduled based on the `refreshAt()` value of the token. + +* If `refreshAt()` is set earlier than `expiresAt()`, the token is refreshed proactively. +* If the refresh fails after retries, the connection is closed and re-established. + +Setting `refreshAt()` earlier than `expiresAt()` is recommended to avoid authentication gaps during refresh: + +[source,java] +---- +new DefaultCbsToken( + token, "jwt", + OffsetDateTime.now().plusHours(1), // expires in 1 hour + OffsetDateTime.now().plusMinutes(45)); // refresh after 45 minutes +---- + +== CBS Exchange Protocols + +The connector supports two CBS exchange protocols, configured with `cbs.exchange`: + +=== `set-token` (default) + +The `set-token` protocol sends the token as an AMQP message to the CBS node with: + +* Subject: `set-token` +* Application property `token-type`: the token type +* Body: the token string + +The broker acknowledges the message to confirm the token was accepted. + +[source,properties] +---- +mp.messaging.incoming.my-channel.cbs.exchange=set-token +---- + +=== `put-token` + +The `put-token` protocol uses a request-reply exchange, compatible with *Azure Service Bus*. +It sends the token as a request and validates the response status code. + +[source,properties] +---- +mp.messaging.incoming.my-channel.cbs.exchange=put-token +mp.messaging.incoming.my-channel.cbs.audience=my-namespace.servicebus.windows.net/my-queue +---- + +When using `put-token`, the `cbs.audience` attribute sets the token audience. +If not configured, the audience is derived from the broker host and target address in the format `amqp://HOST/ADDRESS`. + +== Connection Sharing with CBS + +When multiple channels share the same connection using `container-id`, CBS authentication is performed once for the shared connection: + +[source,properties] +---- +mp.messaging.incoming.my-channel-in.connector=smallrye-amqp +mp.messaging.incoming.my-channel-in.container-id=shared +mp.messaging.incoming.my-channel-in.cbs.enabled=true + +mp.messaging.outgoing.my-channel-out.connector=smallrye-amqp +mp.messaging.outgoing.my-channel-out.container-id=shared +mp.messaging.outgoing.my-channel-out.cbs.enabled=true +---- + +Both channels share a single connection and a single CBS token authorization. + +== Azure Service Bus Example + +To connect to Azure Service Bus using CBS with SAS tokens: + +[source,java] +---- +include::{examples-dir}/amqp/customization/MySasTokenProvider.java[tags=sas-provider,indent=0] +---- + +[source,properties] +---- +mp.messaging.incoming.my-channel.connector=smallrye-amqp +mp.messaging.incoming.my-channel.host=my-namespace.servicebus.windows.net +mp.messaging.incoming.my-channel.port=5671 +mp.messaging.incoming.my-channel.use-ssl=true +mp.messaging.incoming.my-channel.cbs.enabled=true +mp.messaging.incoming.my-channel.cbs.exchange=put-token +mp.messaging.incoming.my-channel.cbs.audience=my-namespace.servicebus.windows.net/my-queue +---- + +== Retry Configuration + +If token authorization fails, the connector retries with exponential backoff. +The retry behavior is controlled by the standard connector retry attributes: + +[source,properties] +---- +mp.messaging.incoming.my-channel.retry-on-fail-attempts=6 # max retries (default: 6) +mp.messaging.incoming.my-channel.retry-on-fail-interval=5 # retry delay in seconds (default: 5) +---- + +If all retries are exhausted, the connection is closed and re-established using the connector's reconnection mechanism. + +== Configuration Reference + +[width="100%",cols="17%,7%,24%,52%",options="header",] +|=== +|Attribute |Type |Default |Description +|`cbs.enabled` |boolean |`false` |Enable CBS authorization +|`cbs.exchange` |string |`set-token` |CBS exchange protocol: `set-token` or `put-token` +|`cbs.audience` |string | |Token audience (auto-derived from address if not set) +|`cbs.scopes` |string | |Token scopes for authorization +|`cbs.token-manager` |string |`default-cbs-token-manager` |CDI bean identifier of a custom `CbsTokenManager.Factory` +|=== + +All attributes are available for both incoming and outgoing channels. diff --git a/content/docs/smallrye-reactive-messaging/latest/amqp/client-customization.adoc b/content/docs/smallrye-reactive-messaging/latest/amqp/client-customization.adoc new file mode 100644 index 000000000..bc214acb8 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/amqp/client-customization.adoc @@ -0,0 +1,50 @@ += Customizing the underlying AMQP client +:version-root: ../ +include::{version-root}_attributes.adoc[] + +You can customize the underlying AMQP Client configuration by +_producing_ an instance of +https://vertx.io/docs/apidocs/io/vertx/amqp/AmqpClientOptions.html[`AmqpClientOptions`]: + +[source,java] +---- +include::{examples-dir}/amqp/customization/ClientProducers.java[tags=named,indent=0] +---- + +This instance is retrieved and used to configure the client used by the +connector. You need to indicate the name of the client using the +`client-options-name` attribute: + +[source,properties] +---- +mp.messaging.incoming.prices.client-options-name=my-named-options +---- + +If you experience frequent disconnections from the broker, the AmqpClientOptions can also be used to set a heartbeat if you need to keep the AMQP connection permanently. +Some brokers might terminate the AMQP connection after a certain idle timeout. +You can provide a heartbeat value which will be used by the Vert.x proton client to advertise the idle timeout when opening transport to a remote peer. + +[source,java] +---- +@Produces +@Identifier("my-named-options") +public AmqpClientOptions getNamedOptions() { + // set a heartbeat of 30s (in milliseconds) + return new AmqpClientOptions() + .setHeartbeat(30000); +} +---- + +== Client capabilities + +Both incoming and outgoing AMQP channels can be configured with a list +of capabilities to declare during sender and receiver connections with +the AMQP broker. Note that supported capability names are broker +specific. + +[source,properties] +---- +mp.messaging.incoming.sink.capabilities=temporary-topic +... +mp.messaging.outgoing.source.capabilities=shared +---- diff --git a/content/docs/smallrye-reactive-messaging/latest/amqp/health.adoc b/content/docs/smallrye-reactive-messaging/latest/amqp/health.adoc new file mode 100644 index 000000000..79ec6eb9f --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/amqp/health.adoc @@ -0,0 +1,34 @@ += Health reporting +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The AMQP connector reports the startup, liveness, and readiness of each +inbound (Receiving messages) and outbound (sending messages) channel +managed by the connector: + +* Startup :: For both inbound and outbound, the startup probe reports _OK_ when the +connection with the broker is established, and the AMQP senders and +receivers are opened (the links are attached to the broker). +* Liveness :: For both inbound and outbound, the liveness check verifies that the +connection is established. The check still returns _OK_ if the +connection got cut, but we are attempting a reconnection. +* Readiness :: For the inbound, it checks that the connection is established and the +receiver is opened. Unlike the liveness check, this probe reports _KO_ +until the connection is re-established. For the outbound, it checks that +the connection is established and the sender is opened. Unlike the +liveness check, this probe reports _KO_ until the connection is +re-established. + +[NOTE] +==== + +To disable health reporting, set the `health-enabled` attribute for the +channel to `false`. + +==== + +Note that a message processing failures _nacks_ the message, which is +then handled by the failure-strategy. It is the responsibility of the +`failure-strategy` to report the failure and influence the outcome of +the checks. The `fail` failure strategy reports the failure, and so the +check will report the fault. diff --git a/content/docs/smallrye-reactive-messaging/latest/amqp/rabbitmq.adoc b/content/docs/smallrye-reactive-messaging/latest/amqp/rabbitmq.adoc new file mode 100644 index 000000000..06631ded9 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/amqp/rabbitmq.adoc @@ -0,0 +1,61 @@ += Using RabbitMQ +:version-root: ../ +include::{version-root}_attributes.adoc[] + +RabbitMQ 4.0{plus} provides native AMQP 1.0 support, making it fully compatible with this connector. +For older versions (3.x), RabbitMQ implements AMQP 0.9.1 and requires the +https://github.com/rabbitmq/rabbitmq-amqp1.0/blob/v3.8.x/README.md[AMQP 1.0 plugin] to be enabled. + +== Address format + +RabbitMQ 4.0{plus} uses the v2 address format with `/queues/` and `/exchanges/` prefixes. +When using this connector with RabbitMQ, prefix your addresses accordingly: + +[source,properties] +---- +mp.messaging.incoming.prices.connector=smallrye-amqp +mp.messaging.incoming.prices.address=/queues/prices + +mp.messaging.outgoing.generated-price.connector=smallrye-amqp +mp.messaging.outgoing.generated-price.address=/queues/prices +mp.messaging.outgoing.generated-price.use-anonymous-sender=false +---- + +== Recommendations + +RabbitMQ support for AMQP 1.0 may differ from other AMQP 1.0 supporting brokers. +We recommend the following configurations. + +To receive messages from RabbitMQ: + +* Set `durable` to `false` + +[source,properties] +---- +mp.messaging.incoming.prices.connector=smallrye-amqp +mp.messaging.incoming.prices.durable=false +---- + +To send messages to RabbitMQ: + +* set the destination `address` (anonymous senders are not supported) +* set `use-anonymous-sender` to `false` + +[source,properties] +---- +mp.messaging.outgoing.generated-price.connector=smallrye-amqp +mp.messaging.outgoing.generated-price.address=prices +mp.messaging.outgoing.generated-price.use-anonymous-sender=false +---- + +It’s not possible to change the destination dynamically (using message +metadata) when using RabbitMQ. The connector automatically detects that +the broker does not support anonymous sender (See +http://docs.oasis-open.org/amqp/anonterm/v1.0/anonterm-v1.0.html). + +== Request/Reply with RabbitMQ + +This connector supports the request-reply pattern with RabbitMQ. +See xref:amqp/request-reply.adoc#using-with-rabbitmq[AMQP Request/Reply] for details. + +Alternatively, you can use the xref:rabbitmq/rabbitmq.adoc[RabbitMQ connector]. diff --git a/content/docs/smallrye-reactive-messaging/latest/amqp/receiving-amqp-messages.adoc b/content/docs/smallrye-reactive-messaging/latest/amqp/receiving-amqp-messages.adoc new file mode 100644 index 000000000..751056c73 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/amqp/receiving-amqp-messages.adoc @@ -0,0 +1,257 @@ += Receiving messages from AMQP +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The AMQP connector lets you retrieve messages from an https://www.amqp.org/product/architecture[AMQP broker or +router]. The AMQP connector +retrieves _AMQP Messages_ and maps each of them into Reactive Messaging +`Messages`. + +== Example + +Let’s imagine you have an AMQP broker (such as https://activemq.apache.org/components/artemis/[Apache ActiveMQ +Artemis]) running, and +accessible using the `amqp:5672` address (by default it would use +`localhost:5672`). Configure your application to receive AMQP Messages +on the `prices` channel as follows: + +[source,properties] +---- +amqp-host=amqp # <1> +amqp-port=5672 # <2> +amqp-username=my-username # <3> +amqp-password=my-password # <4> + +mp.messaging.incoming.prices.connector=smallrye-amqp # <5> +---- + +[arabic] +. Configures the broker/router host name. You can do it per channel +(using the `host` attribute) or globally using `amqp-host` +. Configures the broker/router port. You can do it per channel (using +the `port` attribute) or globally using `amqp-port`. The default is +`5672`. +. Configures the broker/router username if required. You can do it per +channel (using the `username` attribute) or globally using +`amqp-username`. +. Configures the broker/router password if required. You can do it per +channel (using the `password` attribute) or globally using +`amqp-password`. +. Instructs the `prices` channel to be managed by the AMQP connector + +[NOTE] +==== + +You don’t need to set the AMQP _address_. By default, it uses the +channel name (`prices`). You can configure the `address` attribute to +override it. + +==== + +Then, your application receives `Message++<++Double++>++`. You can consume the +payload directly: + +[source,java] +---- +include::{examples-dir}/amqp/inbound/AmqpPriceConsumer.java[indent=0] +---- + +Or, you can retrieve the `Message++<++Double++>++`: + +[source,java] +---- +include::{examples-dir}/amqp/inbound/AmqpPriceMessageConsumer.java[indent=0] +---- + +== Deserialization + +The connector converts incoming AMQP Messages into Reactive Messaging +`Message++<++T++>++` instances. `T` depends on the _body_ of the received AMQP +Message. + +The http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-types-v1.0-os.html[AMQP Type +System] +defines the supported types. + +[width="100%",cols="66%,34%",options="header",] +|=== +|AMQP Body Type |`++<++T++>++` +|AMQP Value containing a http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-types-v1.0-os.html#section-primitive-type-definitions[AMQP Primitive Type] |the corresponding Java type +|AMQP Value using the `Binary` type |`byte++[]++` +|AMQP Sequence |`List` +|AMQP Data (with binary content) and the `content-type` is set to `application/json` |https://vertx.io/docs/apidocs/io/vertx/core/json/JsonObject.html[`JsonObject`] +|AMQP Data with a different `content-type` |`byte++[]++` +|=== + +If you send objects with this AMQP connector (outbound connector), it +gets encoded as JSON and sent as binary. The `content-type` is set to +`application/json`. You can receive this payload using (Vert.x) JSON +Objects, and then map it to the object class you want: + +[source,java] +---- +@ApplicationScoped +public static class Generator { + + @Outgoing("to-amqp") + public Multi prices() { // <1> + AtomicInteger count = new AtomicInteger(); + return Multi.createFrom().ticks().every(Duration.ofMillis(1000)) + .map(l -> new Price().setPrice(count.incrementAndGet())) + .onOverflow().drop(); + } + +} + +@ApplicationScoped +public static class Consumer { + + List prices = new CopyOnWriteArrayList<>(); + + @Incoming("from-amqp") + public void consume(JsonObject p) { // <2> + Price price = p.mapTo(Price.class); // <3> + prices.add(price); + } + + public List list() { + return prices; + } +} +---- + +[arabic] +. The `Price` instances are automatically encoded to JSON by the +connector +. You can receive it using a `JsonObject` +. Then, you can reconstruct the instance using the `mapTo` method + +== Inbound Metadata + +Messages coming from AMQP contains an instance of link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-amqp/{javadoc-version}/io/smallrye/reactive/messaging/amqp/IncomingAmqpMetadata.html[`IncomingAmqpMetadata`] + +[source,java] +---- +include::{examples-dir}/amqp/inbound/AmqpMetadataExample.java[tags=code,indent=0] +---- + +== Acknowledgement + +When a Reactive Messaging `Message` associated with an AMQP Message is +acknowledged, it informs the broker that the message has been +_accepted_. + +== Failure Management + +If a message produced from an AMQP message is _nacked_, a failure +strategy is applied. The AMQP connector supports six strategies: + +* `fail` - fail the application; no more AMQP messages will be +processed (default). The AMQP message is marked as rejected. +* `accept` - this strategy marks the AMQP message as _accepted_. The +processing continues ignoring the failure. Refer to the http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-messaging-v1.0-os.html#type-accepted[accepted +delivery state +documentation]. +* `release` - this strategy marks the AMQP message as _released_. The +processing continues with the next message. The broker can redeliver +the message. Refer to the http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-messaging-v1.0-os.html#type-released[released delivery state +documentation]. +* `reject` - this strategy marks the AMQP message as rejected. The +processing continues with the next message. Refer to the http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-messaging-v1.0-os.html#type-rejected[rejected +delivery state +documentation]. +* `modified-failed` - this strategy marks the AMQP message as +_modified_ and indicates that it failed (with the `delivery-failed` +attribute). The processing continues with the next message, but the +broker may attempt to redeliver the message. Refer to the http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-messaging-v1.0-os.html#type-modified[modified +delivery state +documentation] +* `modified-failed-undeliverable-here` - this strategy marks the AMQP +message as _modified_ and indicates that it failed (with the +`delivery-failed` attribute). It also indicates that the application +cannot process the message, meaning that the broker will not attempt +to redeliver the message to this node. The processing continues with +the next message. Refer to the http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-messaging-v1.0-os.html#type-modified[modified delivery state +documentation] + +== Configuration Reference + +include::{generated-dir}/smallrye-amqp-incoming.adoc[indent=0] + +You can also pass any property supported by the https://vertx.io/docs/vertx-amqp-client/java/[Vert.x AMQP +client] as attribute. + +To use an existing _address_ or _queue_, you need to configure the +`address`, `container-id` and, optionally, the `link-name` attributes. +For example, if you have an Apache Artemis broker configured with: + +[source,xml] +---- + + +
people
+ true + artemis +
+
+---- + +You need the following configuration: + +[source,properties] +---- +mp.messaging.incoming.people.connector=smallrye-amqp +mp.messaging.incoming.people.durable=true +mp.messaging.incoming.people.address=people +mp.messaging.incoming.people.container-id=people +---- + +You may need to configure the `link-name` attribute, if the queue name +is not the channel name: + +[source,properties] +---- +mp.messaging.incoming.people-in.connector=smallrye-amqp +mp.messaging.incoming.people-in.durable=true +mp.messaging.incoming.people-in.address=people +mp.messaging.incoming.people-in.container-id=people +mp.messaging.incoming.people-in.link-name=people +---- + +== Receiving Cloud Events + +The AMQP connector supports https://cloudevents.io/[Cloud Events]. +When the connector detects a _structured_ or _binary_ Cloud Events, it +adds a link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/io/smallrye/reactive/messaging/ce/IncomingCloudEventMetadata.html[`IncomingCloudEventMetadata`] +into the metadata of the `Message`. `IncomingCloudEventMetadata` +contains accessors to the mandatory and optional Cloud Event attributes. + +If the connector cannot extract the Cloud Event metadata, it sends the +Message without the metadata. + +=== Binary Cloud Events + +For `binary` Cloud Events, *all* mandatory Cloud Event attributes must +be set in the AMQP application properties, prefixed by `cloudEvents:` +(as mandated by the https://github.com/cloudevents/spec/blob/v1.0.1/amqp-protocol-binding.md[protocol +binding]). +The connector considers headers starting with the `cloudEvents:` prefix +but not listed in the specification as extensions. You can access them +using the `getExtension` method from `IncomingCloudEventMetadata`. + +The `datacontenttype` attribute is mapped to the `content-type` header +of the record. + +=== Structured Cloud Events + +For `structured` Cloud Events, the event is encoded in the record’s +value. Only JSON is supported, so your event must be encoded as JSON in +the record’s value. + +Structured Cloud Event must set the `content-type` header of the record +to `application/cloudevents{plus}json; charset=UTF-8`. The message body must +be a valid JSON object containing at least all the mandatory Cloud +Events attributes. + +If the record is a structured Cloud Event, the created Message’s payload +is the Cloud Event `data`. diff --git a/content/docs/smallrye-reactive-messaging/latest/amqp/request-reply.adoc b/content/docs/smallrye-reactive-messaging/latest/amqp/request-reply.adoc new file mode 100644 index 000000000..4b7bbaad1 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/amqp/request-reply.adoc @@ -0,0 +1,202 @@ += AMQP Request/Reply +:version-root: ../ +include::{version-root}_attributes.adoc[] + +[WARNING] +.Experimental +==== + +AMQP Request Reply Emitter is an experimental feature. + +==== + +The AMQP https://www.enterpriseintegrationpatterns.com/patterns/messaging/RequestReply.html[Request-Reply] pattern allows you to publish a message to an AMQP address and then await for a reply message that responds to the initial request. + +The `AmqpRequestReply` emitter implements the requestor (or the client) of the request-reply pattern for AMQP 1.0 outbound channels: + +[source,java] +---- +include::{examples-dir}/amqp/outbound/AmqpRequestReplyEmitter.java[indent=0] +---- + +The `request` method publishes the request message to the configured target address of the outgoing channel, +and listens on a reply address (by default, the channel name with `-reply` suffix) for a reply message. +When the reply is received the returned `Uni` is completed with the message payload. + +The request send operation generates a correlation id and sets the AMQP `message-id` property, +which it expects to be sent back in the reply message's `correlation-id` property. + +The replier (or the server) can be implemented using a Reactive Messaging processor: + +[source,java] +---- +include::{examples-dir}/amqp/outbound/AmqpReplier.java[indent=0] +---- + +When you need more control over the reply message, such as setting the reply-to address and correlation id explicitly, you can use the `Message` type: + +[source,java] +---- +include::{examples-dir}/amqp/outbound/AmqpReplierWithMetadata.java[indent=0] +---- + +Given the following configuration example: + +[source,properties] +---- +mp.messaging.outgoing.my-request.connector=smallrye-amqp +mp.messaging.outgoing.my-request.address=requests +mp.messaging.outgoing.my-request.reply.address=my-request-reply + +mp.messaging.incoming.request.connector=smallrye-amqp +mp.messaging.incoming.request.address=requests + +mp.messaging.outgoing.reply.connector=smallrye-amqp +mp.messaging.outgoing.reply.address=my-request-reply +mp.messaging.outgoing.reply.use-anonymous-sender=false +---- + +== Requesting with `Message` types + +Like the core Emitter's `send` methods, `request` method also can receive a `Message` type and return a message: + +[source,java] +---- +include::{examples-dir}/amqp/outbound/AmqpRequestReplyMessageEmitter.java[indent=0] +---- + +[NOTE] +==== + +The ingested reply type of the `AmqpRequestReply` is discovered at runtime, +in order to configure a `MessageConverter` to be applied on the incoming message before returning the `Uni` result. + +==== + +== Requesting multiple replies + +You can use the `requestMulti` method to expect any number of replies represented by the `Multi` return type. + +For example this can be used to aggregate multiple replies to a single request. + +[source,java] +---- +include::{examples-dir}/amqp/outbound/AmqpRequestReplyMultiEmitter.java[indent=0] +---- + +Like the other `request` you can also request `Message` types. + +[NOTE] +==== + +The channel attribute `reply.timeout` will be applied between each message, if reached the returned `Multi` will +fail. + +==== + +== Pending replies and reply timeout + +By default, the `Uni` returned from the `request` method is configured to fail with timeout exception if no reply is received after 5 seconds. +This timeout is configurable with the channel attribute `reply.timeout` (in milliseconds). + +[source,properties] +---- +mp.messaging.outgoing.my-request.reply.timeout=10000 +---- + +A snapshot of the list of pending replies is available through the `AmqpRequestReply#getPendingReplies` method. + +== Scaling Request/Reply + +If multiple requestor instances are configured on the same outgoing address, and the same reply address, +each requestor instance will receive replies of all instances. If an observed correlation id doesn't match +the id of any pending replies, the reply is simply discarded. +With the additional network traffic this allows scaling requestors, (and repliers) dynamically. + +== Correlation Ids + +The AMQP Request/Reply allows configuring the correlation id mechanism completely through a `CorrelationIdHandler` implementation. +The default handler is based on randomly generated UUID strings, set as the AMQP `message-id` on the request message. +The reply message is expected to carry the same value in the `correlation-id` property. + +The correlation id handler implementation can be configured using the `reply.correlation-id.handler` attribute. +The default configuration is `uuid`, which uses randomly generated UUID strings as correlation ids. +An alternative `bytes` implementation can be used to generate random binary correlation ids. +The `bytes` id handler generates 12 random bytes, +but the length can be configured with the `smallrye.amqp.request-reply.correlation-id.bytes.length` attribute. + +Custom handlers can be implemented by proposing a CDI-managed bean with `@Identifier` qualifier. + +== Reply Error Handling + +If the reply server produces an error, it can propagate the error back to the requestor, failing the returned `Uni`. + +If configured using the `reply.failure.handler` channel attribute, +the `ReplyFailureHandler` implementations are discovered through CDI, matching the `@Identifier` qualifier. + +A sample reply error handler can lookup application properties and return the error to be thrown by the reply: + +[source,java] +---- +include::{examples-dir}/amqp/outbound/MyAmqpReplyFailureHandler.java[indent=0] +---- + +`null` return value indicates that no error has been found in the reply message, and it can be delivered to the application. + +== Connection Sharing + +Multiple AMQP channels can share the same underlying connection when configured with the same `container-id`. +This reduces resource consumption and is particularly useful for request-reply patterns +where the sender and reply receiver can share a single connection. + +[source,properties] +---- +mp.messaging.outgoing.my-request.connector=smallrye-amqp +mp.messaging.outgoing.my-request.address=requests +mp.messaging.outgoing.my-request.container-id=my-connection +mp.messaging.outgoing.my-request.reply.address=replies + +mp.messaging.incoming.other-channel.connector=smallrye-amqp +mp.messaging.incoming.other-channel.address=other +mp.messaging.incoming.other-channel.container-id=my-connection +---- + +Both channels above will share the same AMQP connection because they use the same `container-id`. + +[NOTE] +==== + +Connection sharing requires all channels with the same `container-id` to have compatible connection settings (host, port, credentials, etc.). +If the settings differ, the connector will detect the conflict and raise an error. + +==== + +== Using with RabbitMQ + +RabbitMQ 4.0{plus} with native AMQP 1.0 support is compatible with the request-reply pattern. +When using RabbitMQ, remember to use `/queues/` prefixed addresses (v2 address format) +and set `use-anonymous-sender=false` as anonymous senders are not supported. + +[source,properties] +---- +mp.messaging.outgoing.my-request.connector=smallrye-amqp +mp.messaging.outgoing.my-request.address=/queues/requests +mp.messaging.outgoing.my-request.reply.address=/queues/replies +mp.messaging.outgoing.my-request.use-anonymous-sender=false + +mp.messaging.incoming.request.connector=smallrye-amqp +mp.messaging.incoming.request.address=/queues/requests + +mp.messaging.outgoing.reply.connector=smallrye-amqp +mp.messaging.outgoing.reply.address=/queues/replies +mp.messaging.outgoing.reply.use-anonymous-sender=false +---- + +See xref:amqp/rabbitmq.adoc[Using RabbitMQ] for more details on RabbitMQ-specific configuration. + +== Limitations + +The following features are not yet supported: + +* *Direct reply-to* (`amq.rabbitmq.reply-to`): RabbitMQ's direct reply-to mechanism requires link pairing, which is not yet implemented. +* *Link pairing*: Pairing sender and receiver links on the same connection for request-reply routing is not yet available. diff --git a/content/docs/smallrye-reactive-messaging/latest/amqp/sending-amqp-messages.adoc b/content/docs/smallrye-reactive-messaging/latest/amqp/sending-amqp-messages.adoc new file mode 100644 index 000000000..f89a2a0b6 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/amqp/sending-amqp-messages.adoc @@ -0,0 +1,254 @@ += Sending messages to AMQP +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The AMQP connector can write Reactive Messaging `Messages` as AMQP +Messages. + +== Example + +Let’s imagine you have an AMQP broker (such as https://activemq.apache.org/components/artemis/[Apache ActiveMQ +Artemis]) running, and +accessible using the `amqp:5672` address (by default it would use +`localhost:5672`). Configure your application to send the messages from +the `prices` channel as AMQP Message as follows: + +[source,properties] +---- +amqp-host=amqp # <1> +amqp-port=5672 # <2> +amqp-username=my-username # <3> +amqp-password=my-password # <4> + +mp.messaging.outgoing.prices.connector=smallrye-amqp # <5> +---- + +[arabic] +. Configures the broker/router host name. You can do it per channel +(using the `host` attribute) or globally using `amqp-host` +. Configures the broker/router port. You can do it per channel (using +the `port` attribute) or globally using `amqp-port`. The default is +`5672`. +. Configures the broker/router username if required. You can do it per +channel (using the `username` attribute) or globally using +`amqp-username`. +. Configures the broker/router password if required. You can do it per +channel (using the `password` attribute) or globally using +`amqp-password`. +. Instructs the `prices` channel to be managed by the AMQP connector + +[NOTE] +==== + +You don’t need to set the _address_. By default, it uses the channel +name (`prices`). You can configure the `address` attribute to override +it. + +==== + +Then, your application must send `Message++<++Double++>++` to the `prices` +channel. It can use `double` payloads as in the following snippet: + +[source,java] +---- +include::{examples-dir}/amqp/outbound/AmqpPriceProducer.java[indent=0] +---- + +Or, you can send `Message++<++Double++>++`: + +[source,java] +---- +include::{examples-dir}/amqp/outbound/AmqpPriceMessageProducer.java[indent=0] +---- + +== Serialization + +When receiving a `Message++<++T++>++`, the connector convert the message into an +AMQP Message. The payload is converted to the AMQP Message _body_. + +[width="100%",cols="50%,50%",options="header",] +|=== +|`T` |AMQP Message Body +|primitive types or `String` |AMQP Value with the payload +|`Instant` or `UUID` |AMQP Value using the corresponding AMQP Type +|https://vertx.io/docs/apidocs/io/vertx/core/json/JsonObject.html[`JsonObject`] or https://vertx.io/docs/apidocs/io/vertx/core/json/JsonArray.html[`JsonArray`] |AMQP Data using a binary content. The `content-type` is set to `application/json` +|`io.vertx.mutiny.core.buffer.Buffer` |AMQP Data using a binary content. No `content-type` set +|Any other class |The payload is converted to JSON (using a Json Mapper). The result is wrapped into AMQP Data using a *binary* content. The `content-type` is set to `application/json` +|=== + +If the message payload cannot be serialized to JSON, the message is +_nacked_. + +== Outbound Metadata + +When sending `Messages`, you can add an instance of +link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-amqp/{javadoc-version}/io/smallrye/reactive/messaging/amqp/OutgoingAmqpMetadata.html[`OutgoingAmqpMetadata`] +to influence how the message is going to be sent to AMQP. For example, you +can configure the subjects, properties: + +[source,java] +---- +include::{examples-dir}/amqp/outbound/AmqpOutboundMetadataExample.java[tags=code,indent=0] +---- + +== Dynamic address names + +Sometimes it is desirable to select the destination of a message +dynamically. In this case, you should not configure the address inside +your application configuration file, but instead, use the outbound +metadata to set the address. + +For example, you can send to a dynamic address based on the incoming +message: + +[source,java] +---- +include::{examples-dir}/amqp/outbound/AmqpOutboundDynamicAddressExample.java[tags=code,indent=0] +---- + +[NOTE] +==== + +To be able to set the address per message, the connector is using an +_anonymous sender_. + +==== + +== Acknowledgement + +By default, the Reactive Messaging `Message` is acknowledged when the +broker acknowledged the message. When using routers, this +acknowledgement may not be enabled. In this case, configure the +`auto-acknowledgement` attribute to acknowledge the message as soon as +it has been sent to the router. + +If an AMQP message is rejected/released/modified by the broker (or +cannot be sent successfully), the message is nacked. + +== Back Pressure and Credits + +The back-pressure is handled by the `max-inflight-messages` attribute and AMQP _credits_. +The outbound connector requests messages minimum between `max-inflight-messages` and credits allowed by the broker. +When the amount of credits reaches 0, it waits (in a non-blocking fashion) until the broker grants more credits to the AMQP sender. + +When `max-inflight-messages` is set to 0, only AMQP credits apply to limit the requests. + +Note that if an AMQP message send fails, it is retried until `retry-on-fail-attempts` is reached. +If the client reconnects to the broker during the retry, failing messages are sent again but the message order is not preserved. + +To preserve the message order in this case you can set `max-inflight-messages` to + +== Configuration Reference + +include::{generated-dir}/smallrye-amqp-outgoing.adoc[indent=0] + +You can also pass any property supported by the https://vertx.io/docs/vertx-amqp-client/java/[Vert.x AMQP +client] as attribute. + +== Using existing destinations + +To use an existing _address_ or _queue_, you need to configure the +`address`, `container-id` and, optionally, the `link-name` attributes. +For example, if you have an Apache Artemis broker configured with: + +[source,xml] +---- + + +
people
+ true + artemis +
+
+---- + +You need the following configuration: + +[source,properties] +---- +mp.messaging.outgoing.people.connector=smallrye-amqp +mp.messaging.outgoing.people.durable=true +mp.messaging.outgoing.people.address=people +mp.messaging.outgoing.people.container-id=people +---- + +You may need to configure the `link-name` attribute, if the queue name +is not the channel name: + +[source,properties] +---- +mp.messaging.outgoing.people-out.connector=smallrye-amqp +mp.messaging.outgoing.people-out.durable=true +mp.messaging.outgoing.people-out.address=people +mp.messaging.outgoing.people-out.container-id=people +mp.messaging.outgoing.people-out.link-name=people +---- + +To use a `MULTICAST` queue, you need to provide the _FQQN_ +(Fully-qualified queue name) instead of just the name of the queue: + +[source,properties] +---- +mp.messaging.outgoing.people-out.connector=smallrye-amqp +mp.messaging.outgoing.people-out.durable=true +mp.messaging.outgoing.people-out.address=foo +mp.messaging.outgoing.people-out.container-id=foo + +mp.messaging.incoming.people-out.connector=smallrye-amqp +mp.messaging.incoming.people-out.durable=true +mp.messaging.incoming.people-out.address=foo::bar # Note the syntax: address-name::queue-name +mp.messaging.incoming.people-out.container-id=bar +mp.messaging.incoming.people-out.link-name=people +---- + +More details about the AMQP Address model can be found on https://activemq.apache.org/components/artemis/documentation/2.0.0/address-model.html[the Artemis +documentation]. + +== Sending Cloud Events + +The AMQP connector supports https://cloudevents.io/[Cloud Events]. The +connector sends the outbound record as Cloud Events if: + +* the message metadata contains an +`io.smallrye.reactive.messaging.ce.OutgoingCloudEventMetadata` +instance, +* the channel configuration defines the `cloud-events-type` and +`cloud-events-source` attributes. + +You can create +`io.smallrye.reactive.messaging.ce.OutgoingCloudEventMetadata` instances +using: + +[source,java] +---- +include::{examples-dir}/amqp/outbound/AmqpCloudEventProcessor.java[indent=0] +---- + +If the metadata does not contain an id, the connector generates one +(random UUID). The `type` and `source` can be configured per message or +at the channel level using the `cloud-events-type` and +`cloud-events-source` attributes. Other attributes are also +configurable. + +The metadata can be contributed by multiple methods, however, you must +always retrieve the already existing metadata to avoid overriding the +values: + +[source,java] +---- +include::{examples-dir}/amqp/outbound/AmqpCloudEventMultipleProcessors.java[indent=0] +---- + +By default, the connector sends the Cloud Events using the _binary_ +format. You can write _structured_ Cloud Events by setting the +`cloud-events-mode` to `structured`. Only JSON is supported, so the +created records had its `content-type` header set to +`application/cloudevents{plus}json; charset=UTF-8` + +[NOTE] +==== + +you can disable the Cloud Event support by setting the `cloud-events` +attribute to `false` + +==== diff --git a/content/docs/smallrye-reactive-messaging/latest/camel/camel-processor.adoc b/content/docs/smallrye-reactive-messaging/latest/camel/camel-processor.adoc new file mode 100644 index 000000000..20327120f --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/camel/camel-processor.adoc @@ -0,0 +1,37 @@ += The processor pattern using Camel +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Using the processor pattern, you can consume on a channel using a Camel +component, and produce on a channel using another Camel component. In +that case, the headers present in the incoming metadata will be +forwarded in the outgoing metadata. + +== Example + +Let’s imagine you want to read messages from a Nats subject, process it +and produce a message on a Kafka topic. + +[source,properties] +---- +mp.messaging.incoming.mynatssubject.connector=smallrye-camel # <1> +mp.messaging.incoming.mynatssubject.endpoint-uri=nats:mynatssubject # <2> +mp.messaging.outgoing.mykafkatopic.connector=smallrye-camel # <3> +mp.messaging.outgoing.mykafkatopic.endpoint-uri=kafka:mykafkatopic# <4> + +camel.component.nats.servers=127.0.0.1:5555 # <5> +camel.component.kafka.brokers=127.0.0.1:9092 # <6> +---- + +[arabic] +. Sets the connector for the `mynatssubject` channel +. Configures the `endpoint-uri` for nats subject +. Sets the connector for the `mykafkatopic` channel +. Configures the `endpoint-uri` for the kafka topic +. Sets the URL of the nats server to use +. Sets the URL of a kafka broker to use + +[source,java] +---- +include::{examples-dir}/camel/processor/CamelProcessor.java[indent=0] +---- diff --git a/content/docs/smallrye-reactive-messaging/latest/camel/camel.adoc b/content/docs/smallrye-reactive-messaging/latest/camel/camel.adoc new file mode 100644 index 000000000..00eb2828e --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/camel/camel.adoc @@ -0,0 +1,75 @@ += Apache Camel Connector +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The Camel connector adds support for Apache Camel to Reactive Messaging. + +https://camel.apache.org/[Camel] is an open source integration +framework let you integrate various systems consuming or producing data. +Camel implements the Enterprise Integration Patterns and provides +several hundred of components used to access databases, message queues, +APIs or basically https://camel.apache.org/components/latest/[anything under the +sun]. + +[IMPORTANT] +==== + +Smallrye Reactive Messaging 4.x moved from `javax` to `jakarta` APIs therefore only supports Camel 4 releases. +Similarly, this connector drops the Java 11 support as this is the case for Camel 4. + +==== + +== Introduction + +Camel is not a messaging broker. But, it allows your Reactive Messaging +application to retrieve data from almost anything and send data to +almost anything. + +So if you want to send Reactive Messaging `Message` to Telegram or +retrieve data from Salesforce or SAP, this is the connector you need. + +One of the Camel cornerstone is the `endpoint` and its `uri` encoding +the connection to an external system. For example, +`file:orders/?delete=true&charset=utf-8` instructs Camel to read the +files from the `orders` directory. URI format and parameters are listed +on the component documentation, such as the https://camel.apache.org/components/latest/file-component.html[File +component]. + +== Using the camel connector + +To you the camel Connector, add the following dependency to your +project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.reactive + smallrye-reactive-messaging-camel + {project-version} + +---- + +You will also need the dependency of the Camel component you are using. +For example, if you want to process files, you would need to add the +Camel File Component artifact: + +[source,xml,subs="attributes+"] +---- + + org.apache.camel + camel-file + {camel-version} + +---- + +The connector name is: `smallrye-camel`. + +So, to indicate that a channel is managed by this connector you need: + +.... +# Inbound +mp.messaging.incoming.[channel-name].connector=smallrye-camel + +# Outbound +mp.messaging.outgoing.[channel-name].connector=smallrye-camel +.... diff --git a/content/docs/smallrye-reactive-messaging/latest/camel/receiving-messages-from-camel.adoc b/content/docs/smallrye-reactive-messaging/latest/camel/receiving-messages-from-camel.adoc new file mode 100644 index 000000000..65338aedc --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/camel/receiving-messages-from-camel.adoc @@ -0,0 +1,89 @@ += Retrieving data using Camel +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Camel provides many components. To keep this documentation focused on +the integration with Camel, we use the https://camel.apache.org/components/latest/file-component.html[File +component]. +This component let use read files from a directory. So the connector +configured with this component creates a `Message` for each file located +in the directory. As soon as a file is dropped in the directory, a new +`Message` is created. + +== Example + +Let’s imagine you want to read the files from the `orders` directory and +send them to the `files` channel. Configuring the Camel connector to +gets the _file_ from this directory only requires 2 properties: + +[source,properties] +---- +mp.messaging.incoming.files.connector=smallrye-camel # <1> +mp.messaging.incoming.files.endpoint-uri=file:orders/?delete=true&charset=utf-8 # <2> +---- + +[arabic] +. Sets the connector for the `files` channel +. Configures the `endpoint-uri` + +Then, your application receives `Message++<++GenericFile++<++File++>>++`. + +[NOTE] +==== + +The Camel File component produces +`org.apache.camel.component.file.GenericFile` instances. You can +retrieve the actual `File` using `getFile()`. + +==== + +You can consume the payload directly: + +[source,java] +---- +include::{examples-dir}/camel/inbound/CamelFileConsumer.java[indent=0] +---- + +You can also retrieve the `Message++<++GenericFile++<++File++>>++`: + +[source,java] +---- +include::{examples-dir}/camel/inbound/CamelFileMessageConsumer.java[indent=0] +---- + +== Deserialization + +Each Camel component is producing specific objects. As we have seen, the +File component produces `GenericFile`. + +Refer to the component documentation to check which type is produced. + +== Inbound Metadata + +Messages coming from Camel contains an instance of +link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-camel/{javadoc-version}/io/smallrye/reactive/messaging/camel/IncomingExchangeMetadata.html[`IncomingExchangeMetadata`] +in the metadata. + +[source,java] +---- +include::{examples-dir}/camel/inbound/IncomingCamelMetadataExample.java[indent=0] +---- + +This object lets you retrieve the Camel `Exchange`. + +== Failure Management + +If a message produced from a Camel exchange is _nacked_, a failure +strategy is applied. The Camel connector supports 3 strategies: + +* `fail` - fail the application, no more MQTT messages will be +processed. (default) The offset of the record that has not been +processed correctly is not committed. +* `ignore` - the failure is logged, but the processing continue. + +In both cases, the `exchange` is marked as rollback only and the nack +reason is attached to the exchange. + +== Configuration Reference + +include::{generated-dir}/smallrye-camel-incoming.adoc[indent=0] diff --git a/content/docs/smallrye-reactive-messaging/latest/camel/sending-messages-to-camel.adoc b/content/docs/smallrye-reactive-messaging/latest/camel/sending-messages-to-camel.adoc new file mode 100644 index 000000000..8bf614150 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/camel/sending-messages-to-camel.adoc @@ -0,0 +1,76 @@ += Sending data with Camel +:version-root: ../ +include::{version-root}_attributes.adoc[] + +You can use the Camel connector to send data to almost any type of +system. + +To keep this document focused on the Camel connector, we use the Camel +File component. However, the connector can be used with any Camel +component. + +== Example + +Let’s imagine you want to write generated prices into files. Configure +your application to write the messages from the `prices` channel into a +files as follows: + +[source,properties] +---- +mp.messaging.outgoing.prices.connector=smallrye-camel # <1> +mp.messaging.outgoing.prices.endpoint-uri=file:prices/?fileName=${date:now:yyyyMMddssSS}.txt&charset=utf-8 # <2> +---- + +[arabic] +. Sets the connector for the `prices` channel +. Configure the `endpoint-uri` to write into files in the `prices` +directory + +[IMPORTANT] +==== + +Depending on your implementation of MicroProfile Reactive Messaging, the +`$` may need to be escaped as follows: `$$++{++...}` + +==== + +Then, your application must send `Message++<++String++>++` to the `prices` +channel. It can use `String` payloads as in the following snippet: + +[source,java] +---- +include::{examples-dir}/camel/outbound/CamelPriceProducer.java[indent=0] +---- + +Or, you can send `Message++<++Double++>++`: + +[source,java] +---- +include::{examples-dir}/camel/outbound/CamelPriceMessageProducer.java[indent=0] +---- + +== Serialization + +The serialization is handled by the Camel component. Refer to the Camel +documentation to check which payload type is supported by the component. + +== Outbound Metadata + +When sending `Messages`, you can add an instance +of link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-camel/{javadoc-version}/io/smallrye/reactive/messaging/camel/OutgoingExchangeMetadata.html[`OutgoingExchangeMetadata`] +to the message metadata. You can then influence how the outbound Camel +`Exchange` is created, for example by adding properties: + +[source,java] +---- +include::{examples-dir}/camel/outbound/CamelOutboundMetadataExample.java[tags=code,indent=0] +---- + +== Acknowledgement + +The incoming messages are acknowledged when the underlying Camel +exchange completes. If the exchange fails, the message is nacked. + +== Configuration Reference + +include::{generated-dir}/smallrye-camel-outgoing.adoc[indent=0] diff --git a/content/docs/smallrye-reactive-messaging/latest/camel/using-existing-routes.adoc b/content/docs/smallrye-reactive-messaging/latest/camel/using-existing-routes.adoc new file mode 100644 index 000000000..ed8b2cfd7 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/camel/using-existing-routes.adoc @@ -0,0 +1,67 @@ += Using the Camel API +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The Camel connector is based on the https://camel.apache.org/components/latest/reactive-streams-component.html[Reactive Streams +support] +from Camel. If you have an application already using the Camel API +(routes, `from`...), you can integrate it with Reactive Messaging. + +== Getting the CamelReactiveStreamsService + +Once you add the Camel connector to your application, you can retrieve +the +`org.apache.camel.component.reactive.streams.api.CamelReactiveStreamsService` +object: + +[source,java] +---- +include::{examples-dir}/camel/api/CamelApi.java[tags=reactive,indent=0] +---- + +This `CamelReactiveStreamsService` lets you create `Publisher` and +`Subscriber` instances from existing routes. + +== Using Camel Route with @Outgoing + +If you have an existing Camel route, you can transform it as a +`Publisher` using the `CamelReactiveStreamsService`. Then, you can +return this `Publisher` from a method annotated with `@Outgoing`: + +[source,java] +---- +include::{examples-dir}/camel/api/CamelApi.java[tags=source,indent=0] +---- + +You can also expose a `RouteBuilder` bean, making sure to use the `Singleton` scope, +as `RouteBuilder` is no longer proxyable: + +[source,java] +---- +include::{examples-dir}/camel/api/CamelApi.java[tags=source-route-builder,indent=0] +---- + +Alternatively you can use the `LambdaRouteBuilder`: + +[source,java] +---- +include::{examples-dir}/camel/api/CamelApi.java[tags=source-lambda-route-builder,indent=0] +---- + +Using Camel Route with @Incoming + +If you have an existing Camel route, you can transform it as a +`Subscriber` using the `CamelReactiveStreamsService`. Then, you can +return this `Subscriber` from a method annotated with `@Incoming`: + +[source,java] +---- +include::{examples-dir}/camel/api/CamelApi.java[tags=sink,indent=0] +---- + +You can also use a producer: + +[source,java] +---- +include::{examples-dir}/camel/api/CamelApi.java[tags=producer,indent=0] +---- diff --git a/content/docs/smallrye-reactive-messaging/latest/concepts/acknowledgement.adoc b/content/docs/smallrye-reactive-messaging/latest/concepts/acknowledgement.adoc new file mode 100644 index 000000000..782ec0e81 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/concepts/acknowledgement.adoc @@ -0,0 +1,183 @@ += Acknowledgement +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Acknowledgment is an essential concept in messaging. A message is +acknowledged when its processing or reception has been successful. It +allows the broker to move to the next message. + +How acknowledgment is used, and the exact behavior in terms of retry and +resilience depends on the broker. For example, for Kafka, it would +commit the offset. For AMQP, it would inform the broker that the message +has been _accepted_. + +Reactive Messaging supports acknowledgement. The default acknowledgement +depends on the method signature. Also, the acknowledgement policy can be +configured using the `@Acknowledgement` annotation. + +== Chain of acknowledgment + +If we reuse this example: + +[source,java] +---- +include::{examples-dir}/beans/Chain.java[tags=chain,indent=0] +---- + +The framework automatically acknowledges the message received from the +`sink` channel when the `consume` method returns. As a consequence, the +message received by the `process` method is acknowledged, and so on. In +other words, it creates a chain of acknowledgement - from the outbound +channel to the inbound channel. + +When using connectors to receive and consume messages, the outbound +connector acknowledges the messages when they are dispatched +successfully to the broker. The acknowledgment chain would, as a result, +acknowledges the inbound connector, which would be able to send an +acknowledgment to the broker. + +This chain of acknowledgment is automatically implemented when +processing payloads. + +== Acknowledgment when using Messages + +When using `Messages`, the user controls the acknowledgment, and so the +chain is not formed automatically. It gives you more flexibility about +when and how the incoming messages are acknowledged. + +If you create a `Message` using the `with` method, is copy the +acknowledgment function from the incoming message: + +[source,java] +---- +include::{examples-dir}/ack/MessageAckExamples.java[tags=with-payload,indent=0] +---- + +To have more control over the acknowledgment, you can create a brand new +`Message` and pass the acknowledgment function: + +[source,java] +---- +include::{examples-dir}/ack/MessageAckExamples.java[tags=message-creation,indent=0] +---- + +However, you may need to create the acknowledgment chain, to acknowledge +the incoming message: + +[source,java] +---- +include::{examples-dir}/ack/MessageAckExamples.java[tags=process,indent=0] +---- + +To trigger the acknowledgment of the incoming message, use the `ack()` +method. It returns a `CompletionStage`, receiving `null` as value when +the acknowledgment has completed. + +== Acknowledgment when using streams + +When transforming streams of `Message`, the acknowledgment is delegated +to the user. It means that it’s up to the user to acknowledge the +incoming messages: + +[source,java] +---- +include::{examples-dir}/ack/StreamAckExamples.java[tags=message,indent=0] +---- + +In the previous example, we only generate a single message per incoming +message so that we can use the `with` method. It becomes more +sophisticated when grouping incoming messages or when each incoming +message produces multiple messages. + +In the case of a stream of payloads, the default strategy acknowledges +the incoming messages before being processed by the method (regardless +of the outcome). + +[source,java] +---- +include::{examples-dir}/ack/StreamAckExamples.java[tags=payload,indent=0] +---- + +For method receiving a single payload and producing a stream of payloads, it defaults to pre-processing acknowledgement. +However, in this case, post-processing is supported. +It waits for all the produced messages to be acknowledged before acknowledging the received one. +If one of the produced message is nacked, the received one is nacked immediately. + +[source,java] +---- +include::{examples-dir}/ack/StreamAckExamples.java[tags=payload-to-multi,indent=0] +---- + +== Controlling acknowledgement + +The link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/org/eclipse/microprofile/reactive/messaging/Acknowledgment.html[`Acknowledgment`] +annotation lets you customize the default strategy presented in the +previous sections. The `@Acknowledgement` annotation takes a _strategy_ +as parameter. Reactive Messaging proposed 4 strategies: + +* `POST++_++PROCESSING` - the acknowledgement of the incoming message is +executed once the produced message is acknowledged. +* `PRE++_++PROCESSING` - the acknowledgement of the incoming message is +executed before the message is processed by the method. +* `MANUAL` - the acknowledgement is done by the user. +* `NONE` - No acknowledgment is performed, neither manually or +automatically. + +It is recommended to use `POST++_++PROCESSING` as it guarantees that the +full processing has completed before acknowledging the incoming message. +However, sometimes it’s not possible, and this strategy is not available +if you manipulate streams of `Messages` or payloads. + +The `PRE++_++PROCESSING` strategy can be useful to acknowledge a message +early in the process: + +[source,java] +---- +include::{examples-dir}/ack/PreAckExamples.java[tags=pre,indent=0] +---- + +It cuts the acknowledgment chain, meaning that the rest of the +processing is not linked to the incoming message anymore. This strategy +is the default strategy when manipulating streams of payloads. + +Refer to the xref:concepts/signatures.adoc[signature list] to determine +which strategies are available for a specific method signature and +what’s the default strategy. + +== Negative acknowledgement + +Messages can also be _nacked_, which indicates that the message was not +processed correctly. The `Message.nack` method indicates failing +processing (and supply the reason), and, as for successful +acknowledgment, the _nack_ is propagated through the chain of messages. + +If the message has been produced by a connector, this connector +implements specific behavior when receiving a _nack_. It can fail +(default), or ignore the failing, or implement a dead-letter queue +mechanism. Refer to the connector documentation for further details +about the available strategies. + +If the message is sent by an emitter using the `send(P)` method, the +returned `CompletionStage` is completed _exceptionally_ with the _nack_ +reason. + +[source,java] +---- +include::{examples-dir}/ack/NackOnEmitter.java[tags=emitter,indent=0] +---- + +Negative acknowledgment can be manual or automatic. If your method +handles instances of `Message` and the acknowledgment strategy is +`MANUAL`, you can nack a message explicitly. You must indicate the +reason (an exception) when calling the `nack` method. As for successful +acknowledgment, the `nack` returns a `CompletionStage` completed when +the `nack` has been processed. + +If your method uses the `POST++_++PROCESSING` acknowledgment strategy, and +the method fails (either by throwing an exception or by producing a +failure), the message is automatically nacked with the caught exception: + +[source,java] +---- +include::{examples-dir}/ack/AutoNack.java[tags=auto-nack,indent=0] +---- diff --git a/content/docs/smallrye-reactive-messaging/latest/concepts/advanced-config.adoc b/content/docs/smallrye-reactive-messaging/latest/concepts/advanced-config.adoc new file mode 100644 index 000000000..89aa2cd08 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/concepts/advanced-config.adoc @@ -0,0 +1,43 @@ += Advanced configuration +:version-root: ../ +include::{version-root}_attributes.adoc[] + +== Strict Binding Mode + +By default, SmallRye Reactive Messaging does not enforce whether all +_mediators_ are connected. It just prints a warning message. The strict +mode fails the deployment if some "incomings" are not bound to +"outgoings". To enable this mode, you can pass the +`-Dsmallrye-messaging-strict-binding=true` via the command line, or you +can set the `smallrye-messaging-strict-binding` attribute to `true` in +the configuration: + +[source,text] +---- +smallrye-messaging-strict-binding=true +---- + +== Disabling channels + +You can disable a channel in the configuration by setting the `enabled` +attribute to `false`: + +[source,text] +---- +mp.messaging.outgoing.dummy-sink.connector=dummy +mp.messaging.outgoing.dummy-sink.enabled=false # Disable this channel +---- + +SmallRye Reactive Messaging does not register disabled channels, so make +sure the rest of the application does not rely on them. + +== Publisher metrics + +SmallRye Reactive Messaging integrates MicroProfile Metrics and +Micrometer for registering counter metrics (named +`mp.messaging.message.count`) of published messages per channel. + +Both MicroProfile and Micrometer publisher metrics are enabled by +default if found on the classpath. They can be disabled with +`smallrye.messaging.metrics.mp.enabled` and +`smallrye.messaging.metrics.micrometer.enabled` properties respectively. diff --git a/content/docs/smallrye-reactive-messaging/latest/concepts/blocking.adoc b/content/docs/smallrye-reactive-messaging/latest/concepts/blocking.adoc new file mode 100644 index 000000000..078af3ac4 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/concepts/blocking.adoc @@ -0,0 +1,105 @@ += @Blocking +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The `io.smallrye.reactive.messaging.annotations.Blocking` annotation can +be used on a method annotated with `@Incoming`, or `@Outgoing` to +indicate that the method should be executed on a worker pool: + +[source,java] +---- +@Outgoing("Y") +@Incoming("X") +@Blocking +public String process(String s) { + return s.toUpperCase(); +} +---- + +If method execution does not need to be ordered, it can be indicated on +the `@Blocking` annotation: + +[source,java] +---- +@Outgoing("Y") +@Incoming("X") +@Blocking(ordered = false) +public String process(String s) { + return s.toUpperCase(); +} +---- + +When unordered, the invocation can happen concurrently. + +By default, use of `@Blocking` results in the method being executed in +the Vert.x worker pool. If it’s desired to execute methods on a custom +worker pool, with specific concurrency needs, it can be defined on +`@Blocking`: + +[source,java] +---- +@Outgoing("Y") +@Incoming("X") +@Blocking("my-custom-pool") +public String process(String s) { + return s.toUpperCase(); +} +---- + +Specifying the concurrency for the above worker pool requires the +following configuration property to be defined: + +.... +smallrye.messaging.worker.my-custom-pool.max-concurrency=3 +.... + +== Custom worker pool shutdown timeout + +When using a custom worker pool, it is possible to define a shutdown timeout in order to wait for the scheduled tasks to complete before shutting down the pool. +This can be done by setting the following configuration property: + +[source,properties] +---- +smallrye.messaging.worker.my-custom-pool.max-concurrency=3 +smallrye.messaging.worker.my-custom-pool.shutdown-timeout=10000 +smallrye.messaging.worker.my-custom-pool.shutdown-check-interval=2000 +---- + +The `shutdown-timeout` property defines the maximum time to wait in milliseconds for the scheduled tasks to complete before shutting down the pool, defaults to 60 seconds. +The `shutdown-check-interval` property defines the interval in milliseconds at which the pool will check if the tasks have completed, defaults to 5 seconds. + +== Supported signatures + +`@Blocking` does not support every signature. The following table lists +the supported ones. + +[width="100%",cols="7%,36%,57%",options="header",] +|=== +|Shape |Signature |Comment +|Publisher |`@Outgoing("in") @Blocking O generator()` |Invokes the generator from a worker thread. If `ordered` is set to `false`, the generator can be called concurrently. +|Publisher |`@Outgoing("in") @Blocking Message++<++O++>++ generator()` |Invokes the generator from a worker thread. If `ordered` is set to `false`, the generator can be called concurrently. +|Processor |`@Incoming("in") @Outgoing("bar") @Blocking O process(I in)` |Invokes the method on a worker thread. If `ordered` is set to `false`, the method can be called concurrently. +|Processor |`@Incoming("in") @Outgoing("bar") @Blocking Message++<++O++>++ process(I in)` |Invokes the method on a worker thread. If `ordered` is set to `false`, the method can be called concurrently. +|Subscriber |`@Incoming("in") @Blocking void consume(I in)` |Invokes the method on a worker thread. If `ordered` is set to `false`, the method can be called concurrently. +|Subscriber |`@Incoming("in") @Blocking Uni++<++Void++>++ consume(I in)` |Invokes the method on a worker thread. If `ordered` is set to `false`, the method can be called concurrently. +|Subscriber |`@Incoming("in") @Blocking Uni++<++Void++>++ consume(Message++<++I++>++ msg)` |Invokes the method on a worker thread. If `ordered` is set to `false`, the method can be called concurrently. +|Subscriber |`@Incoming("in") @Blocking CompletionStage++<++Void++>++ consume(I in)` |Invokes the method on a worker thread. If `ordered` is set to `false`, the method can be called concurrently. +|Subscriber |`@Incoming("in") @Blocking CompletionStage++<++Void++>++ consume(Message++<++I++>++ msg)` |Invokes the method on a worker thread. If `ordered` is set to `false`, the method can be called concurrently. +|=== + +When a method can be called concurrently, the max concurrency depends on +the number of threads from the worker thread pool. + +== Using io.smallrye.common.annotation.Blocking + +`io.smallrye.common.annotation.Blocking` is another annotation with the +same semantic. `io.smallrye.common.annotation.Blocking` is used by +multiple SmallRye projects and Quarkus. + +SmallRye Reactive Messaging also supports +`io.smallrye.common.annotation.Blocking`. However, +`io.smallrye.common.annotation.Blocking` does not allow configuring the +ordering (it defaults to `ordered=true`). + +When both annotations are used, +`io.smallrye.reactive.messaging.annotations.Blocking` is preferred. diff --git a/content/docs/smallrye-reactive-messaging/latest/concepts/broadcast.adoc b/content/docs/smallrye-reactive-messaging/latest/concepts/broadcast.adoc new file mode 100644 index 000000000..a10dc2c86 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/concepts/broadcast.adoc @@ -0,0 +1,49 @@ += Broadcast +:version-root: ../ +include::{version-root}_attributes.adoc[] + +[WARNING] +.Experimental +==== + +`@Broadcast` is an experimental feature. + +==== + +By default, messages transiting in a channel are only dispatched to a +single consumer. Having multiple consumers is considered as an error, +and is reported at deployment time. + +The link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/io/smallrye/reactive/messaging/annotations/Broadcast.html[`Broadcast`] +annotation changes this behavior and indicates that messages transiting +in the channel are dispatched to all the consumers. `@Broadcast` must be +used with the `@Outgoing` annotation: + +[source,java] +---- +include::{examples-dir}/broadcast/BroadcastExamples.java[tags=chain,indent=0] +---- + +In the previous example, both consumers get the messages. + +You can also control the number of consumers to wait before starting to +dispatch the messages. This allows waiting for the complete graph to be +woven: + +[source,java] +---- +include::{examples-dir}/broadcast/BroadcastWithCountExamples.java[tags=chain,indent=0] +---- + +[NOTE] +==== + +Inbound connectors also support a `broadcast` attribute that allows +broadcasting the messages to multiple downstream subscribers. + +==== + +Use with Emitter + +For details on how to use `@Broadcast` with `Emitter` see the +xref:concepts/emitter.adoc#emitter-and-broadcast[documentation]. diff --git a/content/docs/smallrye-reactive-messaging/latest/concepts/client-customizers.adoc b/content/docs/smallrye-reactive-messaging/latest/concepts/client-customizers.adoc new file mode 100644 index 000000000..2ca59c240 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/concepts/client-customizers.adoc @@ -0,0 +1,28 @@ += Client Customizers +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Client customizers allow to customize the client instance created by the connector. +Only connectors which create their own client instance support customizers. + +To define a client customizer, you need to provide a CDI bean that implements the `ClientCustomizer++<++T++>++` interface, +parameterized with the config type: + +[source,java] +---- +include::{examples-dir}/customizers/MyClientCustomizer.java[indent=0] +---- + +Connectors which support client customizers will discover all beans and call the `customize` method, +with the _channel name_, the _channel configuration_ and the configuration that'll be used to create the client instance. +If the customizer returns `null` it'll be skipped. + +If you have multiple customizers, customizers can override the `getPriority` method to define the order in which they are called. + +Currently, the following core connectors support client customizers: + +* Kafka: `ClientCustomizer++<++Map++<++String, Object++>>++` +* RabbitMQ: `ClientCustomizer++<++RabbitMQOptions++>++` +* AMQP 1.0: `ClientCustomizer++<++AmqpClientOptions++>++` +* MQTT: `ClientCustomizer++<++MqttClientSessionOptions++>++` +* Pulsar: `ClientCustomizer++<++ClientBuilder++>++`, `ClientCustomizer++<++ConsumerBuilder++<++?++>>++`, `ClientCustomizer++<++ProducerBuilder++<++?++>>++` diff --git a/content/docs/smallrye-reactive-messaging/latest/concepts/concepts.adoc b/content/docs/smallrye-reactive-messaging/latest/concepts/concepts.adoc new file mode 100644 index 000000000..8b9079d3e --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/concepts/concepts.adoc @@ -0,0 +1,97 @@ += Concepts +:version-root: ../ +include::{version-root}_attributes.adoc[] + +When dealing with event-driven or data streaming applications, there are +a few concepts and vocabulary to introduce. + +== Messages, Payload, Metadata + +A `Message` is an _envelope_ around a `payload`. Your application is +going to receive, process, and send `Messages`. + +Your application’s logic can generate these `Messages` or receive them +from a message broker. They can also be consumed by your application or +sent to a message broker. + +.An application can receive a message, process it and send a resulting message +image::images/messages.png[An application can receive a message, process it and send a resulting message] + +In Reactive Messaging, `Message` are represented by the link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/io/eclipse/microprofile/reactive/messaging/Message.html[`Message`] interface. +Each `Message++<++T++>++` contains a _payload_ of type `++<++T++>++`. This payload can be retrieved using `message.getPayload()`: + +[source,java] +---- +include::{examples-dir}/messages/MessageExamples.java[tags=message,indent=0] +---- + +As you can see in the previous snippet, `Messages` can also have +_metadata_. _Metadata_ is a way to extend messages with additional data. +It can be metadata related to the message broker +(like link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/io/smallrye/reactive/messaging/kafka/api/KafkaMessageMetadata.html[`KafkaMessageMetadata`]), or contain operational data (such as tracing metadata), or business-related data. + +[NOTE] +==== + +When retrieving metadata, you get an `Optional` as it may not be present. + +==== + +[TIP] +==== + +Metadata is also used to influence the outbound dispatching (how the message will be sent to the broker). + +==== + +== Channels and Streams + +Inside your application, `Messages` transit on _channel_. A _channel_ is +a virtual destination identified by a name. + +.The application is a set of channels +image::images/channels.png[The application is a set of channels] + +SmallRye Reactive Messaging connects the component to the channel they read and to the channel they populate. +The resulting structure is a stream: Messages flow between components through channels. + +[NOTE] +.What about Reactive Streams? +==== + +You may wonder why _Reactive Messaging_ has _Reactive_ in the name. The +Messaging part is kind of obvious. The Reactive part comes from the +streams that are created by binding components. These streams are +https://www.reactive-streams.org/[Reactive Streams]. They follow the +subscription and request protocol and implement back-pressure. It also +means that xref:#connectors[Connectors] are intended to use non-blocking IO +to interact with the various message brokers. + +==== + +== Connectors + +Your application is interacting with messaging brokers or event backbone using _connectors_. +A _connector_ is a piece of code that connects to a broker and: + +[arabic] +. subscribe/poll/receive messages from the broker and propagate them to the application +. send/write/dispatch messages provided by the application to the broker + +Connectors are configured to map incoming messages to a specific +_channel_ (consumed by the application) and collect outgoing messages +sent to a specific channel. These collected messages are sent to the +external broker. + +.Connectors manages the communication between the application and the brokers +image::images/connectors.png[Connectors manages the communication between the application and the brokers] + +Each connector is dedicated to a specific technology. +For example, a Kafka Connector only deals with Kafka. + +You don’t necessarily need a connector. +When your application does not use connectors, everything happens _in-memory_, and the streams are created by chaining methods altogether. +Each chain is still a reactive stream and enforces the back-pressure protocol. +When you don’t use connectors, you need to make sure the chain is complete, meaning it starts with a message source, and it ends with a sink. +In other words, you need to generate messages from within the application (using a method with only `@Outgoing`, or an `Emitter`) and consume the messages +from within the application (using a method with only `@Incoming` or using an unmanaged stream). diff --git a/content/docs/smallrye-reactive-messaging/latest/concepts/connectors.adoc b/content/docs/smallrye-reactive-messaging/latest/concepts/connectors.adoc new file mode 100644 index 000000000..5a10eafca --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/concepts/connectors.adoc @@ -0,0 +1,199 @@ += Connectors +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Reactive Messaging can handle messages generated from within the +application but also interact with remote _brokers_. Reactive Messaging +Connectors interacts with these remote _brokers_ to retrieve messages +and send messages using various protocols and technology. + +Each connector handles to a specific technology. For example, a Kafka +Connector is responsible for interacting with Kafka, while an MQTT +Connector is responsible for MQTT interactions. + +== Connector name + +Each connector has a name. This name is referenced by the application to +indicate that this connector manages a specific channel. + +For example, the SmallRye Kafka Connector is named: `smallrye-kafka`. + +== Inbound and Outbound connectors + +Connector can: + +[arabic] +. retrieve messages from a remote broker (inbound) +. send messages to a remote broker (outbound) + +A connector can, of course, implement both directions. + +Inbound connectors are responsible for: + +[arabic] +. Getting messages from the remote broker, +. Creating a Reactive Messaging `Message` associated with the +retrieved message. +. Potentially associating technical metadata with the message. It +includes unmarshalling the payload. +. Associating an acknowledgment callback to acknowledge the incoming +message when the Reactive Messaging message is +processed/acknowledged. + +[IMPORTANT] +.Reactive matters +==== + +The first step should follow the reactive streams principle: uses +non-blocking technology, respects downstream requests. + +==== + +Outbound connectors are responsible for: + +[arabic] +. Receiving Reactive Messaging `Message` and transform it into a +structure understood by the remote broker. It includes marshaling +the payload. +. If the `Message` contains outbound metadata (metadata set during the +processing to influence the outbound structure and routing), taking +them into account. +. Sending the message to the remote broker. +. Acknowledging the Reactive Messaging `Message` when the broker has +accepted/acknowledged the message. + +== Configuring connectors + +Applications need to configure the connector used by expressing which +channel is managed by which connector. Non-mapped channels are local / +in-memory. + +To configure connectors, you need to have an implementation of +MicroProfile Config. If you don’t have one, add an implementation of +MicroProfile Config in your _classpath_, such as: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.config + smallrye-config + {smallrye-config-version} + +---- + +Then edit the application configuration, generally +`src/main/resources/META-INF/microprofile-config.properties`. + +The application configures the connector with a set of properties +structured as follows: + +.... +mp.messaging.[incoming|outgoing].[channel-name].[attribute]=[value] +.... + +For example: + +.... +mp.messaging.incoming.dummy-incoming-channel.connector=dummy +mp.messaging.incoming.dummy-incoming-channel.attribute=value +mp.messaging.outgoing.dummy-outgoing-channel.connector=dummy +mp.messaging.outgoing.dummy-outgoing-channel.attribute=value +.... + +You configure each channel (both incoming and outgoing) individually. + +The `++[++incoming++|++outgoing++]++` segment indicates the direction. + +* an `incoming` channel consumes data from a message broker or +something producing data. It’s an inbound interaction. It relates to +methods annotated with an `@Incoming` using the same channel name. +* an `outgoing` consumes data from the application and forwards it to +a message broker or something consuming data. It’s an outbound +interaction. It relates to methods annotated with an `@Outgoing` +using the same channel name. + +The `++[++channel-name++]++` is the name of the channel. If the channel name +contains a `.` (dot), you would need to use `"` (double-quote) around +it. For example, to configure the `dummy.incoming.channel` channel, you +would need: + +.... +mp.messaging.incoming."dummy.incoming.channel".connector=dummy +mp.messaging.incoming."dummy.incoming.channel".attribute=value +.... + +The `++[++attribute++]++=++[++value++]++` sets a specific connector attribute to the +given value. Attributes depend on the used connector. So, refer to the +connector documentation to check the supported attributes. + +The `connector` attribute must be set for each mapped channel and +indicates the name of the connector responsible for the channel. + +Here is an example of a channel using an MQTT connector, consuming data +from a MQTT broker, and a channel using a Kafka connector (writing data +to Kafka): + +[source,properties] +---- +# Configure the incoming health channel +mp.messaging.incoming.health.topic=neo +mp.messaging.incoming.health.connector=smallrye-mqtt +mp.messaging.incoming.health.host=localhost +mp.messaging.incoming.health.broadcast=true + +# Configure outgoing data channel +mp.messaging.outgoing.data.connector=smallrye-kafka +mp.messaging.outgoing.data.bootstrap.servers=localhost:9092 +mp.messaging.outgoing.data.key.serializer=org.apache.kafka.common.serialization.StringSerializer +mp.messaging.outgoing.data.value.serializer=io.vertx.kafka.client.serialization.JsonObjectSerializer +mp.messaging.outgoing.data.acks=1 +---- + +[IMPORTANT] +==== + +To use a connector, you need to add it to your _CLASSPATH_. Generally, +adding the dependency to your project is enough. Then, you need to know +the connector’s name and set the `connector` attribute for each channel +managed by this connector. + +==== + +[TIP] +.Connector names +==== + +Connector managed by the SmallRye Reactive Messaging project are typically +prefixed with `smallrye-`. For example, the SmallRye Kafka Connector is +named `smallrye-kafka`. When using Smallrye connectors, you can omit the +prefix and just use `kafka` as the connector name: + +[source,properties] +---- +mp.messaging.incoming.my-channel.connector=kafka +mp.messaging.connector.kafka.bootstrap.servers=localhost:9092 +---- + +==== + +== Connector attribute table + +In the connector documentation, you will find a table listing the +attribute supported by the connector. Be aware that attributes for +inbound and outbound interactions may be different. + +These tables contain the following entries: + +[arabic] +. The name of the attribute, and potentially an _alias_. The name of +the attribute is used with the +`mp.messaging.++[++incoming++|++outgoing++]++.++[++channel-name++]++.++[++attribute++]++=++[++value++]++` +syntax (the `attribute` segment). The _alias_ (if set) is the name +of a global MicroProfile Config property that avoids having to +configure the attribute for each managed channel. For example, to +set the location of your Kafka broker globally, you can use the +`kafka.bootstrap.servers` alias. +. The description of the attribute, including the type. +. Whether that attribute is mandatory. If so, it fails the +deployment if not set +. The default value, if any. diff --git a/content/docs/smallrye-reactive-messaging/latest/concepts/contributing-connectors.adoc b/content/docs/smallrye-reactive-messaging/latest/concepts/contributing-connectors.adoc new file mode 100644 index 000000000..2af522fc2 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/concepts/contributing-connectors.adoc @@ -0,0 +1,457 @@ += Connector Contribution Guide +:version-root: ../ +include::{version-root}_attributes.adoc[] + +A connector implementation is a CDI-managed bean, typically an `@ApplicationScoped` bean, +which is identified by the `@Connector` identifier. +In order to provide inbound and outbound channels, the connector implements two interfaces `InboundConnector` and `OutboundConnector` respectively. +In addition to that, the connector bean is annotated with `@ConnectorAttribute`, which describes attributes to configure channels. + +[NOTE] +.Maven Archetype +==== + +Smallrye Reactive Messaging provides a Maven archetype to bootstrap a new connector. +You can generate a new connector project with the code described in this guide using: + +[source,shell,subs="attributes+"] +---- +mvn -N archetype:generate \ +-DarchetypeGroupId=io.smallrye.reactive \ +-DarchetypeArtifactId=smallrye-reactive-messaging-connector-archetype \ +-DarchetypeVersion={project-version} \ +-DgroupId=io.smallrye.reactive \ +-Dpackage=io.smallrye.reactive.messaging.my \ +-Dversion={project-version} \ +-DartifactId=smallrye-reactive-messaging-my \ +-DconnectorName=my +---- + +==== + +The following is an example of a connector skeleton : + +[source,java] +---- +include::{examples-dir}/connectors/MyConnector.java[indent=0] +---- + +Note that the `getPublisher` and `getSubscriber` methods receive MicroProfile Config `Config` instance and wrap it with +`MyConnectorIncomingConfiguration` and `MyConnectorOutgoingConfiguration` objects. + +These custom channel configuration types ease getting channel configuration, including the optional or default values. +They are generated by the `smallrye-connector-attribute-processor` annotation processor, and can be configured in project pom like the following: + +[source,xml] +---- + + + ${project.groupId} + smallrye-reactive-messaging-provider + ${project.version} + + + io.smallrye.reactive + smallrye-connector-attribute-processor + ${project.version} + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + ${project.build.directory}/generated-sources/ + + + io.smallrye.reactive.messaging.connector.ConnectorAttributeProcessor + + + org.jboss.logging.processor.apt.LoggingToolsProcessor + + + + + + +---- + +The `smallrye-reactive-messaging-provider` is the minimum required dependency for a connector implementation. +You'll also note that the `LoggingToolsProcessor` annotation processor is also configured. +This enables generating internationalized log statements and exceptions. +Typically, you would create following interfaces in `i18n` sub-package: `++[++Connector++]++Exceptions`, `++[++Connector++]++Logging` and `++[++Connector++]++Messages`. +More information can be found in https://jboss-logging.github.io/jboss-logging-tools/[JBoss Logging Tools documentation]. + +== Implementing Inbound Channels + +The `InboundConnector` implementation returns, for a given channel configuration, a reactive stream of `Message`s. +The returned reactive stream is an instance of `Flow.Publisher` and typically can be implemented using Mutiny `Multi` type. + +[NOTE] +.`IncomingConnectorFactory` +==== + +The inbound channels can also implement the `IncomingConnectorFactory` from the MicroProfile Reactive Messaging specification. +However, the `PublisherBuilder` type can be more challenging to work with and +Smallrye Reactive Messaging converts the provided stream to Mutiny types to do the wiring anyway. + +==== + +The returned `Flow.Publisher` would allow controlling the flow of ingestion using backpressure. +It would be preferable to use pull-based APIs of the underlying messaging library to receive messages from the message broker. +You can refer to the https://smallrye.io/smallrye-mutiny/latest/guides/polling/[`Mutiny` "How to use polling?" guide] +to construct a `Multi` using Mutiny APIs, or implement the `Flow.Subscription` from scratch and wrap it in an `AbstractMulti`. + +Here is an example channel implementation which constructs the reactive stream using the polling API: + +[source,java] +---- +include::{examples-dir}/connectors/MyIncomingChannel.java[indent=0] +---- + +=== Connector Threading and Vert.x + +Whether the external API call is blocking or non-blocking, managing the thread on which the message processing will be dispatched can be challenging. +Smallrye Reactive Messaging depends on https://vertx.io/[Eclipse Vert.x] to consistently dispatch messages in event-loop or worker threads, +propagating the message context along different processing stages. +You can read more on xref:concepts/message-context.adoc[Message Context] and https://vertx.io/blog/an-introduction-to-the-vert-x-context-object/[Vert.x Context]. + +Some connectors already use Vert.x clients, such as RabbitMQ, AMQP 1.0 or MQTT. +Other connectors such as Kafka or Pulsar directly use the client library of the messaging technology, +therefore they create a Vert.x Context per channel to dispatch messages on that context. +Connectors can access the `Vertx` instance by injecting the `ExecutionHolder` bean. +https://smallrye.io/smallrye-mutiny/latest/guides/emit-on-vs-run-subscription-on/[Mutiny operators `runSubscribtionOn` and `emitOn`] can be used to switch threads the events are dispatched on. + +=== Custom Message and Metadata implementations + +Reactive messaging link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/org/eclipse/microprofile/reactive/messaging/Message.html[`Message`] +type is a thin wrapper around a payload and some metadata, which lets implementing acknowledgment and negative acknowledgment of that message. + +Messages received from the underlying library very often return with a custom type, +wrapping the payload and some properties such as key, timestamp, schema information, or any other metadata. + +[TIP] +==== + +The client may propose different strategies for consuming messages individually or in batches. +If a batch consuming is available the incoming channel may receive wrap and dispatch message as a batch or individually. + +==== + +While it is possible to use `Message.of` builder methods to wrap the incoming message type, +a custom type implementing `Message` interface helps to deal with different aspects we'll cover later, +such as deserialization, message acknowledgment or tracing. + +An example message implementation would be like the following: + +[source,java] +---- +include::{examples-dir}/connectors/MyMessage.java[indent=0] +---- + +Note that `MyMessage` implements the `ContextAwareMessage`. +In the constructor `captureContextMetadata` helper method is used to capture the Vert.x context which created the object and capturing it into the `LocalContextMetadata`. +This metadata allows running xref:concepts/message-context.adoc[each message in its own Vert.x context, supporting context propagation]. + +The `MyMessage` type implements the accessors for the `metadata` and `payload` from the `Message` interface. +If the messaging technology doesn't have a built-in unmarshalling mechanism, the message can deserialize the raw payload to a primitive or a complex object. + +[WARNING] +==== + +The custom message implementation is usually not the type consumed by the application injecting channels. +Applications usually inject in the payload, the raw consumed type (in the above example the `ConsumedMessage`), +or some other type provided by the xref:concepts/converters.adoc[`MessageConverter`s]. +Handling of `Message` types by the application is restricted only to advanced use cases, because handling of message acknowledgment is manual +Even then the message may be intercepted before and changed, conserving the `metadata`, `ack` and `nack` handlers but not the original type created by the connector. + +==== + +The `MyIncomingMetadata` gives access to the underlying consumed message attributes, and applications can inject this object for accessing message details: + +[source,java] +---- +include::{examples-dir}/connectors/MyIncomingMetadata.java[indent=0] +---- + +Also note that `ack` and `nack` method implementations are delegated to handler objects. +This allows configuring different strategies at channel level. + +=== Acknowledgment strategies + +The xref:concepts/acknowledgement.adoc[acknowledgement] is the way for message consumers to inform the broker that the message has been successfully received and processed. +Depending on the messaging technology the broker then can decide to remove the message from the queue, flag as consumed or purge it completely. +In Reactive Messaging there are different policies to trigger the acknowledgement but the canonical one is to acknowledge a message when the processing (potentially asynchronous) has completed (`POST++_++PROCESSING`). + +The Reactive Messaging defines `Message#ack` method as non-blocking asynchronous, returning a `CompletionStage++<++Void++>++`, +because potentially the acknowledgement action sends a network call to the broker. + +The following example simply calls the client `ack` method using the Mutiny `Uni` and switch the emitter to the Message context. +Returning back to the message context is essential for chaining asynchronous actions without losing the context and for keeping the consistency on message consumption flow. + +[source,java] +---- +include::{examples-dir}/connectors/MyAckHandler.java[indent=0] +---- + +While this ack handler strategy acknowledges each message to the broker, +the messaging technology can allow employing different strategies for acknowledging messages. +For example an ack strategy can track processed messages and acknowledge them altogether or call a different client side endpoint to acknowledge the message batch. + +=== Failure handling strategies + +The failure handling, or the xref:concepts/acknowledgement.adoc#negative-acknowledgement[negative acknowledgment] allows indicating that a message was not processed correctly. +Similar to the acknowledgment the Reactive Messaging defines `Message#nack(Throwable reason, Metadata metadata)` method as non-blocking asynchronous, returning a `CompletionStage++<++Void++>++`. + +[source,java] +---- +include::{examples-dir}/connectors/MyFailureHandler.java[indent=0] +---- + +Different failure handling strategies can, for example, +- Ignore the failure, log and call the `ack` instead +- Send the message to a dead letter queue and call the `ack` +- Employ a different strategy depending on the `Metadata` associated with the `nack` method call. + +=== Message Converters + +The connector can propose default xref:concepts/converters.adoc[`MessageConverter`] implementations for converting the payload to a custom type. +As an example the following converter extracts the `CustomMessage` and puts it in the payload: + +[source,java] +---- +include::{examples-dir}/connectors/MyMessageConverter.java[indent=0] +---- + +== Implementing Outbound Channels + +The `OutboundConnector` implementation returns, for a given channel configuration, a `Flow.Subscriber` of messages. +This is typically implemented by a custom `Flow.Processor` and using the `MultiUtils.via` helper methods to apply message transformations. + +[NOTE] +.`OutgoingConnectorFactory` +==== + +The outbound channels can also implement the `OutgoingConnectorFactory` from the MicroProfile Reactive Messaging specification. +However, it is usually more friendly to work with the `MultiUtils.via` methods to construct and transform outgoing messages. + +==== + +Here is an example outgoing channel implementation: + +[source,java] +---- +include::{examples-dir}/connectors/MyOutgoingChannel.java[indent=0] +---- + +The `MultiUtils.via` helper method allows using the `Multi` chaining methods and in the same time provides a `Flow.Subscriber`. +However, this implementation allows sending messages one at a time: +one only after the previous outgoing message send is completed. + +Some messaging technologies provide publish receipt, +a message back from the broker to the sender that asynchronously acknowledges the sent operation. +In this case the connector can only be sure of the send operation when it receives the publish receipt of that message. +Some technologies may provide blocking sending calls, +in that case the connector needs to delegate the sending call to a worker thread. + +Depending on whether the client supports multiple in-flight outgoing messages, you can also use a `SenderProcessor`, +which allows receiving configuration for the maximum number of in-flight messages and whether it waits for completion (publish receipt from the broker): + +[source,java] +---- +include::{examples-dir}/connectors/MyOutgoingChannelWithPartials.java[tags=sender-processor,indent=0] +---- + +Other more advanced scenarios can be implemented to retry the transmission in case of a retryable failure, +or batch multiple outgoing messages to a single send operation. + +=== Outgoing `Message` Builder + +In order to convey all attributes of the outgoing message to the client library +connectors provide outgoing `Message` implementation and a corresponding outgoing message metadata. +These allow the application developer to build the message attributes that will be sent to the broker. + +[source,java] +---- +include::{examples-dir}/connectors/MyOutgoingMessage.java[indent=0] +---- + +[source,java] +---- +include::{examples-dir}/connectors/MyOutgoingMetadata.java[indent=0] +---- + +The outgoing channel implementation then will construct the client library object that represents the outbound message, `SendMessage` in this example: + +[source,java] +---- +include::{examples-dir}/connectors/MyOutgoingChannelWithPartials.java[tags=send-message,indent=0] +---- + +It is a best practice to also allow the application to return a payload of the client outbound library object (`SendMessage`). + +=== Outgoing message acknowledgement + +Because the Reactive Messaging xref:concepts/acknowledgement.adoc#chain-of-acknowledgment[chains acknowledgements] from incoming message until the outgoing message, +it is crucial for the outgoing channel to correctly ack and nack the message. + +== Smallrye Health Integration + +Smallrye Reactive Messaging allows connectors to integrate with Smallrye Health to contribute channel state to the health reports. +Connectors need to implement the `HealthReporter` interface and implement some or all of the `getReadiness`, `getLiveness` and `getStartup` methods: + +[source,java] +---- +include::{examples-dir}/connectors/MyConnectorWithPartials.java[tags=health-report,indent=0] +---- + +Implementing health reports per channel depends on what information is available to the connector. +For more information on different health check probes you can check out https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/[Configure Liveness, Readiness and Startup Probes] + +You may want to add a connector attribute to enable/disable the health reporting per channel: + +[source,java] +---- +import io.smallrye.reactive.messaging.annotations.ConnectorAttribute; + +@ConnectorAttribute(name = "health-enabled", type = "boolean", direction = ConnectorAttribute.Direction.INCOMING_AND_OUTGOING, description = "Whether health reporting is enabled (default) or disabled", defaultValue = "true") +---- + +== OpenTelemetry Tracing Integration + +Smallrye Reactive Messaging allows connectors to easily integrate with OpenTelemetry tracing. +It propagates the tracing context from inbound messages and to outbound messages. +The `smallrye-reactive-messaging-otel` module provides necessary dependencies to the OpenTelemetry artifacts and also provides `TracingUtils` helper class for setting up the tracing. + +[source,xml] +---- + + io.smallrye.reactive.messaging + smallrye-reactive-messaging-otel + +---- + +For integrating tracing you'd need to create a couple of classes: + +* Holder class for tracing information +* Implementation of `io.opentelemetry.context.propagation.TextMapGetter`, which retrieves for a given key the value of a tracing attributed from the holder object +* Implementation of `io.opentelemetry.context.propagation.TextMapSetter`, which sets on the holder object the key and value of tracing attributes +* Implementations of `io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor` and `io.opentelemetry.instrumentation.api.instrumenter.messaging.MessagingAttributesGetter` + +Then you'd need to configure instrumenters per incoming and outgoing channel: + +[source,java] +---- +include::{examples-dir}/connectors/tracing/MyOpenTelemetryInstrumenter.java[tags=create-instrumenter,indent=0] +---- + +Finally, you'd need to configure instrumenters per incoming and outgoing channels and wire the call to instrumenter using `TracingUtils`. + +For an incoming channel, you'd need to call the instrumenter on an inbound message: + +[source,java] +---- +include::{examples-dir}/connectors/MyIncomingChannelWithPartials.java[tags=incoming-tracing,indent=0] +---- + +For an outgoing channel, you'd need to call the instrumenter on constructing the outbound message: + +[source,java] +---- +include::{examples-dir}/connectors/MyOutgoingChannelWithPartials.java[tags=outgoing-tracing,indent=0] +---- + +You may want to add a connector attribute to enable/disable the tracing per channel: + +[source,java] +---- +import io.smallrye.reactive.messaging.annotations.ConnectorAttribute; + +@ConnectorAttribute(name = "tracing-enabled", type = "boolean", direction = ConnectorAttribute.Direction.INCOMING_AND_OUTGOING, description = "Whether tracing is enabled (default) or disabled", defaultValue = "true") +---- + +== Testing the connector + +While unit tests are highly encouraged for validating ad-hoc logic in connector code, +by nature connector tests are mostly integration tests validating the correct configuration and functioning of channels. +Most of the time tests need to run against a broker instance. +This instance can be mocked or embedded in the test JVM, +or provisioned in a container runtime using https://testcontainers.org[Testcontainers]. +The testcontainers approach is encouraged as it'll provide a testing environment closest to reality. + +It may take too much time and resources to start a broker per test method or per test class, +so may want to share the same broker instance between all test classes. +In that case you can checkout how to write a https://junit.org/junit5/docs/current/user-guide/#extensions[JUnit 5 Extension] +and start only one container instance in the beginning of tests and stop it at the end of all the tests. + +There are essentially two ways of creating the connector behavior to test against: + +[arabic] +. Instantiating channels directly by passing the custom configuration. +With this you can get the Reactive stream directly from the channel implementation and send/receive messages. +You can use https://smallrye.io/smallrye-mutiny/2.5.1/guides/testing/[AssertSubscriber from Mutiny] to regulate demand and write assertions. +. CDI-based tests which write configuration and instantiate application beans. +You can use https://weld.cdi-spec.org/[Weld, the reference implementation of CDI specification] with configured set of beans and extensions: + +[source,java] +---- +include::{examples-dir}/connectors/test/WeldTestBase.java[indent=0] +---- + +You would need following test dependencies for enabling Weld in tests: + +[source,xml] +---- + + io.smallrye.reactive + test-common + test + + + org.jboss.weld.se + weld-se-shaded + ${weld.version} + test + + + org.jboss.weld + weld-core-impl + ${weld.version} + test + + + org.awaitility + awaitility + test + +---- + +Your test classes can therefore extend the `WeldTestBase` and provide configuration and application beans: + +[source,java] +---- +include::{examples-dir}/connectors/test/MyConnectorTest.java[indent=0] +---- + +[TIP] +.Awaitility +==== + +Because connector tests are usually asynchronous, https://github.com/awaitility/awaitility[awaitility] +provides a DSL to await on expressed assertions. + +==== + +=== Common tests for validating the connector + +* Message consumption through incoming channels +* Message producing through outgoing channels +* Ack and failure handler strategies test +* Message Context propagation test `LocalPropagationTest` +* `HealthCheckTest` +* `MessageConverterTest` +* `TracingPropagationTest` +* Configuration test +* Authentication test +* Tests for diff --git a/content/docs/smallrye-reactive-messaging/latest/concepts/converters.adoc b/content/docs/smallrye-reactive-messaging/latest/concepts/converters.adoc new file mode 100644 index 000000000..ff3f6148e --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/concepts/converters.adoc @@ -0,0 +1,43 @@ += Message Converters +:version-root: ../ +include::{version-root}_attributes.adoc[] + +SmallRye Reactive Messaging supports _message converters_, allowing to +transform an incoming message into a version accepted by the method. If +the incoming messages or payload does not match the invoked method’s +expectation, SmallRye Reactive Messaging looks for a suitable converter. +If found, it converts the incoming message with this converter. + +Converters can have multiple purposes, but the main use case is about +transforming the message’s payload: + +[source,java] +---- +include::{examples-dir}/converters/MyConverter.java[tags=code,indent=0] +---- + +To provide a converter, implement a bean exposing the `MessageConverter` +interface. The `canConvert` method is called during the lookup and +verifies if it can handle the conversion. The `target` type is the +expected payload type. If the converter returns `true` to `canConvert`, +SmallRye Reactive Messaging calls the `convert` method to proceed to the +conversion. + +The previous converter can be used in application like the following, to +convert `Message++<++String++>++` to `Message++<++Person++>++`: + +[source,java] +---- +include::{examples-dir}/converters/ConverterExample.java[tags=code,indent=0] +---- + +Converters work for all supported method signatures. However, the +signature must be well-formed to allow the extraction of the expected +payload type. Wildcards and raw types do not support conversion. If the +expected payload type cannot be extracted, or no converter fits, the +message is passed as received. + +If multiple suitable converters are present, implementations should +override the `getPriority` method returning the priority. The default +priority is `100`. The converter lookup invokes converters with higher +priority (from the least value to the greatest) first. diff --git a/content/docs/smallrye-reactive-messaging/latest/concepts/decorators.adoc b/content/docs/smallrye-reactive-messaging/latest/concepts/decorators.adoc new file mode 100644 index 000000000..0cdb8b7a9 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/concepts/decorators.adoc @@ -0,0 +1,96 @@ += Channel Decorators +:version-root: ../ +include::{version-root}_attributes.adoc[] + +SmallRye Reactive Messaging supports decorating reactive streams +of incoming and outgoing channels for implementing cross-cutting +concerns such as monitoring, tracing or message interception. + +Two symmetrical APIs are proposed for decorating publisher and subscriber channels, +link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/io/smallrye/reactive/messaging/PublisherDecorator.html[`PublisherDecorator`] +and link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/io/smallrye/reactive/messaging/SubscriberDecorator.html[`SubscriberDecorator`] respectively. + +[IMPORTANT] +==== + +`@Incoming` channels and channels bound to an outbound connector are both `Subscriber`s. +Conversely `@Outgoing` channels and channels bound to an inbound connector are `Publisher`s. + +==== + +For example, to provide a decorator which counts consumed messages from incoming connector, +implement a bean exposing the interface `PublisherDecorator`: + +[source,java] +---- +include::{examples-dir}/decorators/ConsumedMessageDecorator.java[tags=code,indent=0] +---- + +Decorators' `decorate` method is called only once per channel at application deployment when graph wiring is taking place. +Decorators are very powerful because they receive the stream of messages (Mutiny `Multi++<++Message++<++?++>>++`) +and potentially return a new stream of messages. + +Note that if a decorator is available it will be called on every channel. +The `decorate` method receives the channel name and whether the channel is a connector or not as parameters. +Decorators are called ordered from highest to lowest priority (from the least value to the greatest), +obtained using the `jakarta.enterprise.inject.spi.Prioritized#getPriority` method. + +[NOTE] +==== + +The `SubscriberDecorator` receive a list of channel names because `@Incoming` annotation is repeatable +and consuming methods can be linked to multiple channels. + +==== + +== Intercepting Incoming and Outgoing Messages + +Decorators (`PublisherDecorator` and `SubscriberDecorator`) can be used to intercept and alter messages, both on incoming and outgoing channels. + +Smallrye Reactive Messaging allows defining intercepting incoming and outgoing messages for a specific channel, using +`IncomingInterceptor` and `OutgoingInterceptor` respectively. + +Only one interceptor is allowed to be bound for interception per outgoing channel. +If no interceptors are found with a `@Identifier` but a `@Default` one is available, it is used. +When multiple interceptors are available, the bean with the highest priority is used. + +=== `IncomingInterceptor` + +To provide an incoming interceptor implement a bean exposing the interface link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/io/smallrye/reactive/messaging/IncomingInterceptor.html[`IncomingInterceptor`], qualified with a `@Identifier` with the channel name to intercept. + +[source,java] +---- +include::{examples-dir}/interceptors/MyIncomingInterceptor.java[indent=0] +---- + +An `IncomingInterceptor` can implement these three methods: + +* `Message++<++?++>++ afterMessageReceive(Message++<++?++>++ message)` : Called after receiving the message from an incoming connector. +The message can be altered by returning a new message from this method. The modified message will be consumed in incoming channels. +* `void onMessageAck(Message++<++?++>++ message)` : Called after message acknowledgment. +* `void onMessageNack(Message++<++?++>++ message, Throwable failure)` : Called after message negative-acknowledgment. + +[NOTE] +==== + +If you are willing to adapt an incoming message payload to fit a consuming method receiving type, +you can use xref:concepts/converters.adoc[`MessageConverter`]s. + +==== + +=== `OutgoingInterceptor` + +To provide an outgoing interceptor implement a bean exposing the interface link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/io/smallrye/reactive/messaging/OutgoingInterceptor.html[`OutgoingInterceptor`], qualified with a `@Identifier` with the channel name to intercept. + +[source,java] +---- +include::{examples-dir}/interceptors/MyOutgoingInterceptor.java[indent=0] +---- + +An `OutgoingInterceptor` can implement these three methods: + +* `Message++<++?++>++ beforeMessageSend(Message++<++?++>++ message)` : Called before passing the message to the outgoing connector for transmission. +The message can be altered by returning a new message from this method. +* `void onMessageAck(Message++<++?++>++ message)` : Called after message acknowledgment. +This callback can access `OutgoingMessageMetadata` which will hold the result of the message transmission to the broker, if supported by the connector. This is only supported by MQTT and Kafka connectors. +* `void onMessageNack(Message++<++?++>++ message, Throwable failure)` : Called after message negative-acknowledgment. diff --git a/content/docs/smallrye-reactive-messaging/latest/concepts/emitter.adoc b/content/docs/smallrye-reactive-messaging/latest/concepts/emitter.adoc new file mode 100644 index 000000000..a59761323 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/concepts/emitter.adoc @@ -0,0 +1,270 @@ += Emitter and Channels +:version-root: ../ +include::{version-root}_attributes.adoc[] + +It is not rare to combine in a single application imperative parts +(Jax-RS, regular CDI _beans_) and reactive parts (_beans_ with +`@Incoming` and `@Outgoing` annotations). In these case, it’s often +required to send _messages_ from the imperative part to the reactive +part. In other words, send messages to channels handled by reactive +messaging and how can you retrieve messages. + +== Emitter and @Channel + +To send _things_ (payload or `Message`) from imperative code to a +specific channel you need to use: + +[arabic] +. the link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/org/eclipse/microprofile/reactive/messaging/Channel.html[`org.eclipse.microprofile.reactive.messaging.Channel`] annotations +. the link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/org/eclipse/microprofile/reactive/messaging/Emitter.html[`org.eclipse.microprofile.reactive.messaging.Emitter`] type + +The `@Channel` lets you indicate to which channel you are going to send +your payloads or messages. The `Emitter` is the object to use to send +these payloads or messages. + +[source,java] +---- +include::{examples-dir}/emitter/MyImperativeBean.java[tags=intro,indent=0] +---- + +The `Emitter` class takes a type parameter. It’s the type of payload. +Even if you want to send `Messages`, the type is the payload type. + +[IMPORTANT] +==== + +You must have a `@Incoming("prices")` somewhere in your application +(meaning a method consuming messages transiting on the channel +`prices`), or an outbound connector configured to manage the `prices` +channel (`mp.messaging.outgoing.prices...`) + +==== + +== Sending payloads + +Sending payloads is done as follows: + +[source,java] +---- +include::{examples-dir}/emitter/EmitterExamples.java[tags=payload,indent=0] +---- + +When sending a payload, the emitter returns a `CompletionStage`. This +`CompletionStage` gets completed once the message created from the +payload is acknowledged: + +[source,java] +---- +include::{examples-dir}/emitter/EmitterExamples.java[tags=cs,indent=0] +---- + +If the processing fails, the `CompletionStage` gets completed +exceptionally (with the reason of the nack). + +== Sending messages + +You can also send `Messages`: + +[source,java] +---- +include::{examples-dir}/emitter/EmitterExamples.java[tags=message,indent=0] +---- + +When sending a `Message`, the emitter does not return a +`CompletionStage`, but you can pass the ack/nack callback, and be called +when the message is acked/nacked. + +[source,java] +---- +include::{examples-dir}/emitter/EmitterExamples.java[tags=message-ack,indent=0] +---- + +Sending messages also let you pass metadata. + +[source,java] +---- +include::{examples-dir}/emitter/EmitterExamples.java[tags=message-meta,indent=0] +---- + +Metadata can be used to propagate some context objects with the message. + +== Overflow management + +When sending messages from imperative code to reactive code, you must be +aware of back-pressure. Indeed, messages sent using the emitter and +stored in a _queue_. If the consumer does not process the messages +quickly enough, this queue can become a memory hog and you may even run +out of memory. + +To control what need to happen when the queue becomes out of control, +use the link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/org/eclipse/microprofile/reactive/messaging/OnOverflow.html[`OnOverflow`] annotation. `@OnOverflow` lets you configure: + +* the maximum size of the queue (default is 256) +* what needs to happen when this size is reached (fail, drop...) + +[source,java] +---- +include::{examples-dir}/emitter/EmitterExamples.java[tags=overflow,indent=0] +---- + +The supported strategies are: + +* `OnOverflow.Strategy.BUFFER` - use a buffer to store the elements +until they are consumed. If the buffer is full, a failure is +propagated (and the thread using the emitted gets an exception) +* `OnOverflow.Strategy.UNBOUNDED++_++BUFFER` - use an unbounded buffer to +store the elements +* `OnOverflow.Strategy.DROP` - drops the most recent value if the +downstream can’t keep up. It means that new value emitted by the +emitter are ignored. +* `OnOverflow.Strategy.FAIL` - propagates a failure in case the +downstream can’t keep up. +* `OnOverflow.Strategy.LATEST` - keeps only the latest value, dropping +any previous value if the downstream can’t keep up. +* `OnOverflow.Strategy.NONE` - ignore the back-pressure signals +letting the downstream consumer to implement a strategy. + +=== Defensive emission + +Having an emitter injected into your code does not guarantee that +someone is ready to consume the message. For example, a subscriber may +be connecting to a remote broker. If there are no subscribers, using the +`send` method will throw an exception. + +The `emitter.hasRequests()` method indicates that a subscriber +subscribes to the channel and requested items. So, you can wrap your +emission with: + +[source,java] +---- +if (emitter.hasRequests()) { + emitter.send("hello"); +} +---- + +If you use the `OnOverflow.Strategy.DROP`, you can use the `send` method +even with no subscribers nor demands. The message will be nacked +immediately. + +== Retrieving channels + +You can use the `@Channel` annotation to inject in your bean the +underlying stream. Note that in this case, you will be responsible for +the subscription: + +[source,java] +---- +include::{examples-dir}/emitter/ChannelExamples.java[tags=channel,indent=0] +---- + +[IMPORTANT] +==== + +You must have a `@Outgoing("my-channel")` somewhere in your application +(meaning a method generating messages transiting on the channel +`my-channel`), or an inbound connector configured to manage the `prices` +channel (`mp.messaging.incoming.prices...`) + +==== + +Injected _channels_ merge all the matching _outgoing_ - so if you have +multiple `@Outgoing("out")`, `@Inject @Channel("out")` gets all the +messages. + +If your injected _channel_ receives _payloads_ (`Multi++<++T++>++`), it +acknowledges the message automatically, and support multiple +subscribers. If you injected _channel_ receives `Message` +(`Multi++<++Message++<++T++>>++`), you will be responsible for the acknowledgement +and broadcasting. + +== Emitter and @Broadcast + +When using an `Emitter`, you can now `@Broadcast` what is emitted to all +subscribers. + +Here is an example of emitting a price with two methods marked +`@Incoming` to receive the broadcast: + +[source,java] +---- +include::{examples-dir}/emitter/EmitterExamples.java[tags=broadcast,indent=0] +---- + +For more details see xref:concepts/broadcast.adoc[@Broadcast] +documentation. + +== Mutiny Emitter + +If you prefer to utilize `Uni` in all your code, there is now a +`MutinyEmitter` that will return `Uni++<++Void++>++` instead of `void`. + +[source,java] +---- +include::{examples-dir}/emitter/MutinyExamples.java[tags=uni,indent=0] +---- + +There’s also the ability to block on sending the event to the emitter. +It will only return from the method when the event is acknowledged, or +nacked, by the receiver: + +[source,java] +---- +include::{examples-dir}/emitter/MutinyExamples.java[tags=uni-await,indent=0] +---- + +And if you don’t need to worry about the success or failure of sending +an event, you can `sendAndForget`: + +[source,java] +---- +include::{examples-dir}/emitter/MutinyExamples.java[tags=uni-forget,indent=0] +---- + +== Custom Emitter Implementations + +[WARNING] +.Experimental +==== + +Custom emitter implementations is an experimental feature. + +==== + +`Emitter` and `MutinyEmitter` are two implementations of the emitter concept, +where imperative code in your application can send messages to Reactive Messaging channels. + +With `EmitterFactory` it is possible to provide custom implementations, and application facing emitter interfaces. + +In the following example, the injectable custom emitter interface is `CustomEmitter`, +and it is implemented by `CustomEmitterImpl`: + +[source,java] +---- +include::{examples-dir}/emitter/EmitterFactoryExamples.java[tags=custom-emitter-declaration,indent=0] +---- + +Note that `CustomEmitter` interface extends `EmitterType`, which is a marker interface for discovering custom emitter types. +Also, `CustomEmitterImpl` implements the `MessagePublisherProvider`, which is used by the framework to transform this emitter to a channel. + +Then we need to provide an implementation of the `EmitterFactory` interface: + +[source,java] +---- +include::{examples-dir}/emitter/EmitterFactoryExamples.java[tags=custom-emitter-factory,indent=0] +---- + +The `CustomEmitterFactory` is a CDI managed bean, which implements the `EmitterFactory`. +It is qualified with `EmitterFactoryFor` annotation which is configured with the emitter interface `CustomEmitter` that this factory provides. + +Smallrye Reactive Messaging discovers the emitter factory during the CDI deployment validation +and verifies that custom emitters used by the application have corresponding emitter factories. +It'll use the emitter factory to create the emitter implementation and will register the implementation into the `ChannelRegistry`. + +Note that the `CustomEmitterFactory` also uses the `ChannelRegistry` and provides the custom emitter with `@Produces`. + +Finally, the application can inject and use the `CustomEmitter` as a normal emitter channel: + +[source,java] +---- +include::{examples-dir}/emitter/EmitterFactoryExamples.java[tags=custom-emitter-usage,indent=0] +---- diff --git a/content/docs/smallrye-reactive-messaging/latest/concepts/generic-payloads.adoc b/content/docs/smallrye-reactive-messaging/latest/concepts/generic-payloads.adoc new file mode 100644 index 000000000..fd7193fd7 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/concepts/generic-payloads.adoc @@ -0,0 +1,55 @@ += Generic Payloads +:version-root: ../ +include::{version-root}_attributes.adoc[] + +[WARNING] +.Experimental +==== + +Generic payloads are an experimental feature and the API is subject to change. + +==== + +When using reactive messaging, `Message` flow in your system +each message has a payload but can also contain _metadata_, as explained in xref:concepts/concepts.adoc#messages-payload-metadata[Messages, Payload, Metadata]. +The metadata can hold information, for example in an outgoing channel, additional properties of the outgoing message to be sent to the broker. + +It is sometimes preferable to continue using the xref:concepts/model.adoc#messages-vs-payloads[payload signatures], +and also being able to attach metadata. +Using `GenericPayload` allows customizing metadata when handling payloads in SmallRye Reactive Messaging `@Incoming` and `@Outgoing` methods. +`GenericPayload` is a wrapper type, like the `Message`, containing a payload and metadata, +without requiring handling acknowledgments manually. + +[source,java] +---- +include::{examples-dir}/genericpayload/GenericPayloadExample.java[tags=code,indent=0] +---- + +You can combine generic payloads with xref:concepts/incoming-metadata-injection.adoc[metadata injection] : + +[source,java] +---- +include::{examples-dir}/genericpayload/GenericPayloadExample.java[tags=injection,indent=0] +---- + +Note that the metadata provided with the outgoing generic payload is merged with the incoming message metadata. + +`GenericPayload` can also be used with emitters to attach metadata without dealing with `Message` directly: + +[source,java] +---- +include::{examples-dir}/genericpayload/GenericPayloadExample.java[tags=emitter,indent=0] +---- + +The inner payload is extracted and the metadata from the `GenericPayload` is merged into the outgoing message. +Note that existing metadata entries are not overwritten by the `GenericPayload` metadata. +If a metadata entry of the same type already exists, it is preserved unless the metadata class implements `MergeableMetadata`. + +[WARNING] +.Limitations +==== + +While `GenericPayload++<++T++>++` can be used as an incoming payload type, +xref:concepts/converters.adoc[message converters] are not applied to the payload type `T`. + +==== diff --git a/content/docs/smallrye-reactive-messaging/latest/concepts/incoming-concurrency.adoc b/content/docs/smallrye-reactive-messaging/latest/concepts/incoming-concurrency.adoc new file mode 100644 index 000000000..058eb070f --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/concepts/incoming-concurrency.adoc @@ -0,0 +1,73 @@ += Incoming Channel Concurrency +:version-root: ../ +include::{version-root}_attributes.adoc[] + +[WARNING] +.Experimental +==== + +Incoming channel `concurrency` config is an experimental feature. + +==== + +The `concurrency` attribute for incoming channels provides a mechanism to enable concurrent non-blocking processing of incoming messages. +When applied to a channel, this attribute specifies the number of copies of that channel to be created and wired to the processing method, +allowing multiple messages to be processed concurrently. + +For example, concurrency configuration for a Kafka incoming channel the configuration will look like: + +[source,properties] +---- +mp.messaging.incoming.my-channel.connector=smallrye-kafka +mp.messaging.incoming.my-channel.topic=orders +mp.messaging.incoming.my-channel.value.deserializer=org.apache.kafka.common.serialization.DoubleDeserializer +mp.messaging.incoming.my-channel.concurrency=4 +---- + +In this example, there will be 4 copies of the `my-channel` running concurrently, with distinctive internal channel names, +`my-channel$1`, `my-channel$2`, etc. but all registered with the name `my-channel` to the `ChannelRegistry`. + +[NOTE] +.Kafka connector `partitions` +==== + +This is essentially very similar to the Kafka connector `partitions` configuration, but addresses some its limitations. +Using `partitions` config in Kafka connector, channels are merged into the downstream message processor +(method annotated with `@Incoming` or an injected channel) which is therefore called sequentially. +This prevents concurrently processing messages from multiple partitions. + +The `concurrency` mechanism effectively allows polling Kafka partitions from separate clients +and concurrently processing records while preserving the in-partition order. + +==== + +Copy channels inherit all configuration attributes of the main channel config. +Per-copy channel attributes can be configured separately using the `$` separated channel names: `mp.messaging.incoming.my-channel$1.attribute`. + +For example, the following AMQP 1.0 channel defines 3 channels each with a different selector: + +[source,properties] +---- +mp.messaging.incoming.data.connector=smallrye-amqp +mp.messaging.incoming.data.address=address +mp.messaging.incoming.data.durable=false +mp.messaging.incoming.data.concurrency=3 +mp.messaging.incoming.data$1.selector=x='foo' +mp.messaging.incoming.data$2.selector=x='bar' +mp.messaging.incoming.data$3.selector=x='baz' +---- + +While the `concurrency` attribute is applicable to channels of any connector type, +the channel implementation may need to take this configuration into account and adjust the threading accordingly. +Connectors based on Vert.x event loop create a new event loop context per copy-channel to dispatch messages on distinct contexts. + +[IMPORTANT] +.Non-blocking processing +==== + +Note that while this allows concurrent processing, messages are still dispatched on Vert.x event loop threads, and should not be blocked. + +==== + +Otherwise, connectors treat copy channels as independent channels. +For example, health check reports are registered separately for each copy-channel. diff --git a/content/docs/smallrye-reactive-messaging/latest/concepts/incoming-metadata-injection.adoc b/content/docs/smallrye-reactive-messaging/latest/concepts/incoming-metadata-injection.adoc new file mode 100644 index 000000000..fb27198b2 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/concepts/incoming-metadata-injection.adoc @@ -0,0 +1,29 @@ += Incoming Metadata Injection +:version-root: ../ +include::{version-root}_attributes.adoc[] + +[WARNING] +.Experimental +==== + +Metadata injection is an experimental feature. + +==== + +When using reactive messaging, `Message` flow in your system. +Each message has a payload but can also contain _metadata_, as explained in xref:concepts/concepts.adoc#messages-payload-metadata[Messages, Payload, Metadata]. + +You can inject the incoming message metadata as a parameter depending on the method signature. +*Only methods receiving the payload as a parameter can also receive the metadata.* +For example, `@Incoming("in") void consume(String s)` can also receive the metadata using the following signature: +`@Incoming("in") void consume(String s, MyMetadata m)`. +Note that the payload must be the first parameter. + +The metadata can be injected using the class of the metadata to inject. +In this case, the method is invoked with `null` if the metadata is missing. +You can also inject the metadata using `Optional++<++MetadataClass++>++,` which would be empty if the incoming message does not contain metadata of type `MetadataClass.` + +[source,java] +---- +include::{examples-dir}/incomings/MetadataInjectionExample.java[tags=code,indent=0] +---- diff --git a/content/docs/smallrye-reactive-messaging/latest/concepts/incomings.adoc b/content/docs/smallrye-reactive-messaging/latest/concepts/incomings.adoc new file mode 100644 index 000000000..8fc179d3a --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/concepts/incomings.adoc @@ -0,0 +1,20 @@ += Multiple Incoming Channels +:version-root: ../ +include::{version-root}_attributes.adoc[] + +[WARNING] +.Experimental +==== + +Multiple `@Incomings` is an experimental feature. + +==== + +The `@Incoming` annotation is repeatable. It means that the method +receives the messages transiting on every listed channels, in no +specific order: + +[source,java] +---- +include::{examples-dir}/incomings/IncomingsExamples.java[tags=code,indent=0] +---- diff --git a/content/docs/smallrye-reactive-messaging/latest/concepts/keyed-multi.adoc b/content/docs/smallrye-reactive-messaging/latest/concepts/keyed-multi.adoc new file mode 100644 index 000000000..cc1824499 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/concepts/keyed-multi.adoc @@ -0,0 +1,52 @@ += Using `KeyedMulti` +:version-root: ../ +include::{version-root}_attributes.adoc[] + +[WARNING] +.Experimental +==== + +`KeyedMulti` is an experimental feature. + +==== + +When implementing a data streaming application, it's common to handle messages partitioned using a _key_. +In this case, your stream manipulation often has to 1) group by key and 2) do the manipulation. +Reactive Messaging can do the first step for you and reduce the code complexity. +To do this, it injects `io.smallrye.reactive.messaging.keyed.KeyedMulti` in your method instead of a _bare_ `Multi`. + +For example, imagine the following stream, represented as `key:value`: `"a:1", "b:1", "a:2", "b:2", "b:3"...` +Then, let's consider the following method: + +[source,java] +---- +include::{examples-dir}/keyed/KeyedMultiExample.java[tags=code,indent=0] +---- + +Reactive Messaging automatically extracts the key and value from the incoming stream and invokes the method for each key. +The received `KeyedMulti` represent the stream for each key. +The `key()` method returns the extracted key. + +The key and value can be extracted from the payload but also (and often) from the message's metadata. + +When using Kafka, it automatically extracts the key/value from the Kafka records. +In the other cases, or if you need custom extraction, you can implement your own `io.smallrye.reactive.messaging.keyed.KeyValueExtractor`. +Implementations are exposed `ApplicationScoped` beans, and are used to extract the key and value. +The following implementation extracts the key and value from payloads structured as "key:value": + +[source,java] +---- +include::{examples-dir}/keyed/KeyValueExtractorFromPayload.java[tags=code,indent=0] +---- + +The extractor selection uses the `canExtract` method. +When multiple extractors are available, you can implement the `getPriority()` method to give a lower priority. +Default extractors have the priority 100. +So, if you have a custom extractor with the priority 99, it will be used (if it replies `true` to the `canExtract` call). +In addition, you can use the `io.smallrye.reactive.messaging.keyed.Keyed` annotation to indicate the class of the extractor to use. +The extractor must still be a CDI bean, but the `canExtract` method is not called, and priority does not matter: + +[source,java] +---- +include::{examples-dir}/keyed/KeyedExample.java[tags=code,indent=0] +---- diff --git a/content/docs/smallrye-reactive-messaging/latest/concepts/logging.adoc b/content/docs/smallrye-reactive-messaging/latest/concepts/logging.adoc new file mode 100644 index 000000000..2a8fc8df3 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/concepts/logging.adoc @@ -0,0 +1,269 @@ += Logging +:version-root: ../ +include::{version-root}_attributes.adoc[] + +SmallRye Reactive Messaging uses https://github.com/jboss-logging[JBoss +Logging] as logging API. This section +explains how to configure the loggers for various logging backends. + +[TIP] +==== + +If you are developing SmallRye Reactive Messaging and wonder about how +the logs are managed, it uses https://jboss-logging.github.io/jboss-logging-tools/#introduction[JBoss Logging +Tools]. + +==== + +== Logging Backends + +SmallRye Reactive Messaging uses the JBoss Logging library to write +messages to a log file. This library is a logging _bridge_ that +integrates different log frameworks. You can decide which of the +following frameworks you want to use for your application: + +* JBoss LogManager (`jboss`) +* Log4j 2 (`log4j2`) +* Log4j 1 (`log4j`) +* Slf4j (`slf4j`) +* JDK logging (`jul`) + +You only need to add the chosen framework to the classpath, and the +JBoss Logging library will pick it up. If there are multiple frameworks +available on the classpath, it picks the first found (in the order from +the list). Alternatively, you can set the `org.jboss.logging.provider` +system property is one of the values given above. + +The concepts and log categories are the same for all frameworks. +However, the format of the configuration file and the names of the log +levels differ. Check the documentation of your logging library to find +out which _dependencies_ are required, the exact name of the log levels, +and where the configuration should be written. + +== Log Categories + +As all applications and frameworks, SmallRye Reactive Messaging writes +log messages in different categories and log levels. The categories +group messages from specific connectors, classes or components. The +following table shows the essential log categories used by SmallRye +Reactive Messaging: + +[width="100%",cols="36%,64%",options="header",] +|=== +|Category |Description +|`io.smallrye.reactive.messaging` |This category contains all the messages written by SmallRye Reactive Messaging. +|`io.smallrye.reactive.messaging.provider` |This category contains all the messages generated by the _core_ (provider). +|`io.smallrye.reactive.messaging.kafka` |This category contains all the messages generated by the Kafka Connector. +|`io.smallrye.reactive.messaging.amqp` |This category contains all the messages generated by the AMQP Connector. +|`io.smallrye.reactive.messaging.jms` |This category contains all the messages generated by the JMS Connector. +|`io.smallrye.reactive.messaging.camel` |This category contains all the messages generated by the Camel Connector. +|`io.smallrye.reactive.messaging.mqtt` |This category contains all the messages generated by the MQTT (Client) Connector. +|`io.smallrye.reactive.messaging.mqtt-server` |This category contains all the messages generated by the MQTT (Server) Connector. +|=== + +The names of the log levels are defined by your logging framework and +determine the amount and granularity of the log messages. You can assign +a log level to each category. If you do not specify a specific +category’s log level, it will inherit the level from its parent +category. Thus, setting the log level of +`io.smallrye.reactive.messaging` influences every loggers from SmallRye +Reactive Messaging. + +== Message Code + +Each message has an identifier code. They are all prefixed with `SRMSG`, +followed with the numeric code. + +In the following output, the code is `SRMSG00229`: + +.... +[2020-06-15 13:35:07] [INFO ] SRMSG00229: Channel manager initializing... +.... + +== Recommended logging configurations + +=== Development + +==== Log4J 1 + +_log4j.properties_ + +[source,properties] +---- +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.Target=System.out +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d{HH:mm:ss,SSS} %-5p [%c] - %m%n + +log4j.rootLogger=info, stdout +log4j.logger.io.smallrye.reactive.messaging=info +log4j.logger.org.jboss.weld=warn +---- + +==== Log4J 2 + +_log4j2.xml_ + +[source,xml] +---- + + + PropertiesConfiguration + + + + + + [%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %c{1} - %msg%n + > + + + + + + + + + + + + + + + +---- + +==== JDK (JUL) + +_logging.properties_ + +[source,properties] +---- +handlers=java.util.logging.ConsoleHandler + +java.util.logging.ConsoleHandler.level=FINEST +java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter +java.util.logging.SimpleFormatter.format=[%1$tF %1$tT] [%4$-7s] %5$s %n + +.level=INFO +io.smallrye.reactive.messaging.level=INFO +org.jboss.weld.level=WARNING +---- + +==== LogBack via SLF4J++*++ + +_logback.xml_ + +[source,xml] +---- + + + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + +---- + +=== Production + +==== Log4J 1 + +_log4j.properties_ + +[source,properties] +---- +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.Target=System.out +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d{HH:mm:ss,SSS} %-5p [%c] - %m%n + +log4j.rootLogger=info, stdout +log4j.logger.io.smallrye.reactive.messaging=warn +log4j.logger.org.jboss.weld=error +---- + +==== Log4J 2 + +_log4j2.xml_ + +[source,xml] +---- + + + PropertiesConfiguration + + + + + + [%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %c{1} - %msg%n + > + + + + + + + + + + + + + + + +---- + +==== JDK (JUL) + +_logging.properties_ + +[source,properties] +---- +handlers=java.util.logging.ConsoleHandler + +java.util.logging.ConsoleHandler.level=INFO +java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter +java.util.logging.SimpleFormatter.format=[%1$tF %1$tT] [%4$-7s] %5$s %n + +.level=INFO +io.smallrye.reactive.messaging.level=WARNING +org.jboss.weld.level=SEVERE +---- + +_logback.xml_ + +[source,xml] +---- + + + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + +---- diff --git a/content/docs/smallrye-reactive-messaging/latest/concepts/merge.adoc b/content/docs/smallrye-reactive-messaging/latest/concepts/merge.adoc new file mode 100644 index 000000000..d7bd6cbd7 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/concepts/merge.adoc @@ -0,0 +1,47 @@ += Merge channels +:version-root: ../ +include::{version-root}_attributes.adoc[] + +[WARNING] +.Experimental +==== + +`@Merge` is an experimental feature. + +==== + +By default, messages transiting in a channel can arise from a single +producer. Having multiple producers is considered erroneous and is +reported at deployment time. + +The link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/io/smallrye/reactive/messaging/annotations/Merge.html[`Merge`] +annotation changes this behavior and indicates that a channel can have +multiple producers. `@Merge` must be used with the `@Incoming` +annotation: + +[source,java] +---- +include::{examples-dir}/merge/MergeExamples.java[tags=chain,indent=0] +---- + +In the previous example, the consumer gets all the messages (from both +producers). + +The `@Merge` annotation allows configuring how the incoming messages +(from the different producers) are merged into the channel. The `mode` +attribute allows configuring this behavior: + +* `ONE` picks a single producer, discarding the other producer; +* `MERGE` (default) gets all the messages as they come, without any +defined order. Messages from different producers may be interleaved. +* `CONCAT` concatenates the producers. The messages from one producer +are received until the messages from other producers are received. + +[NOTE] +==== + +Outbound connectors also support a `merge` attribute that allows +consuming the messages to multiple upstreams. It will dispatch all the +received messages. + +==== diff --git a/content/docs/smallrye-reactive-messaging/latest/concepts/message-context.adoc b/content/docs/smallrye-reactive-messaging/latest/concepts/message-context.adoc new file mode 100644 index 000000000..411587683 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/concepts/message-context.adoc @@ -0,0 +1,83 @@ += Message Contexts +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Message context provides a way to propagate data along the processing of a message. +It can be used to propagate message specific objects in an implicit manner and be able to retrieve them later, such as the user, session or transaction. + +[IMPORTANT] +==== + +Message contexts are only support by Kafka, AMQP, RabbitMQ and MQTT connectors. + +==== + +[NOTE] +==== + +Message context support is an experimental and SmallRye only feature. + +==== + +== What's a message context + +A message context is execution context on which a message is processed. +Each stage of the processing is going to use the same execution context. +Thus, it allows storing _data_ which can later be restored. +For example, you can imagine storing some authentication (`User` in the following example) data in one part of your processing and restore it in a later stage. + +[source,java] +---- +@Incoming("data") +@Outgoing("process") +public Message process(Message input) { + // Extract some data from the message and store it in the context + User user = ...; + // Store the extracted data into the message context. + ContextLocals.put("user", user); + record input; +} + +@Incoming("process") +@Outgoing("after-process") +public String handle(String payload) { + // You can retrieve the store data using + User user = ContextLocals.get("user", null); + + // ... + return payload; +} +---- + +The Message context is also available when using blocking or asynchronous stages (stage returning `Uni` or `CompletionStage`) + +== The difference with metadata + +Message metadata can be used to provide a similar feature. +However, it requires using `Messages` which can be inconvenient (need to handle the acknowledgement manually). +Message Contexts provide a simpler API, closer to a _Message CDI scope_: you can save data, and restore it later. +The implicit propagation avoid having to deal with `Messages`. + +== Supported signatures + +Message context works with: + +* methods consuming or producing `Messages`, `Uni++<++Message++<++T++>>++` and `CompletionStage++<++Message++<++T++>>++` +* methods consuming or producing payloads, `Uni++<++Payload++>++` and `CompletionStage++<++Payload++>++`. +* blocking and non-blocking methods + +However, message context are *NOT* enforced when using methods consuming or producing: + +* `Multi`, `Flow.Publisher`, `Publisher` and `PublisherBuilder` +* `Subscriber`, `Flow.Subscriber`, and `SubscriberBuilder` +* `Processor`, `Flow.Processor`, and `ProcessorBuilder` + +== Under the hood + +Under the hood, the message context feature uses Vert.x _duplicated contexts_. +A duplicated context is a view of the "root" (event loop) context, which is restored at each stage of the message processing. + +Each time that a compatible connector receives a message from a broker, it creates a new _duplicated context_ and attaches it to the message. +So the context is stored in the metadata of the message. + +When the message is processed, SmallRye Reactive Messaging makes sure that this processing is executed on the stored duplicated context. diff --git a/content/docs/smallrye-reactive-messaging/latest/concepts/model.adoc b/content/docs/smallrye-reactive-messaging/latest/concepts/model.adoc new file mode 100644 index 000000000..c7adc4e70 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/concepts/model.adoc @@ -0,0 +1,422 @@ += Development Model +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Reactive Messaging proposes a CDI-based programming model to implement event-driven applications. +Following the CDI principles, _beans_ are forming the main building block of your application. +Reactive Messaging provides a set of annotations and types to implement beans that generate, consume or process messages. + +== @Incoming and @Outgoing + +Reactive Messaging provides two main annotations: + +* link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/org/eclipse/microprofile/reactive/messaging/Incoming.html[`org.eclipse.microprofile.reactive.messaging.Incoming`] - indicates the consumed channel +* link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/org/eclipse/microprofile/reactive/messaging/Outgoing.html[`org.eclipse.microprofile.reactive.messaging.Outgoing`] - indicates the populated channel + +These annotations are used on _methods_: + +[source,java] +---- +include::{examples-dir}/beans/MessageProcessingBean.java[indent=0] +---- + +[NOTE] +==== + +Reactive Messaging beans can either be in the _application_ scope (`@ApplicationScoped`) or dependent scope (`@Dependent`). + +==== + +Manipulating messages can be cumbersome. +When you are only interested in the payload, you can use the following syntax: The following code is equivalent to the snippet from above: + +[source,java] +---- +include::{examples-dir}/beans/PayloadProcessingBean.java[indent=0] +---- + +[IMPORTANT] +==== + +You should not call methods annotated with `@Incoming` and/or +`@Outgoing` directly from your code. They are invoked by the framework. +Having user code invoking them would not have the expected outcome. + +==== + +SmallRye Reactive Messaging automatically binds matching `@Outgoing` to +`@Incoming` to form a chain: + +.A chain of components +image::images/chain.png[A chain of components] + +If we consider the following code: + +[source,java] +---- +include::{examples-dir}/beans/Chain.java[tags=chain,indent=0] +---- + +It would generate the following chain: + +[source,text] +---- +generate --> [ source ] --> process --> [ sink ] --> consume +---- + +Methods annotated with `@Incoming` or `@Outgoing` don’t have to be in the same _bean_ (_class_). +You can distribute them among a set of beans. +Remote interactions are also possible when using connectors. + +Methods annotated with: + +* only `@Outgoing` are used to generate messages or payloads +* only `@Incoming` are used to consume messages or payloads +* both `@Incoming` and `@Outgoing` are used to process messages or payloads; or transform the stream + +== Creating messages + +Messages are envelopes around payload. +They are the vehicle. +While manipulating payload is convenient, messages let you add metadata, handle acknowledgement... + +Creating `Messages` is done using the link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/io/eclipse/microprofile/reactive/messaging/Message.html[`Message`] interface directly: + +[source,java] +---- +include::{examples-dir}/messages/MessageExamples.java[tags=creation,indent=0] +---- + +Messages accept `null` as payload. +Channels connected to outbound connectors interpret messages with `null` payload differently depending on the technology. + +[source,java] +---- +include::{examples-dir}/messages/MessageExamples.java[tags=null-payload,indent=0] +---- + +You can also create new instance of `Message` from an existing one: + +[source,java] +---- +include::{examples-dir}/messages/MessageExamples.java[tags=copy,indent=0] +---- + +[WARNING] +.Acknowledgement? +==== + +Acknowledgement is an important part of messaging systems. This will be covered in the xref:concepts/acknowledgement.adoc[acknowledgement] section. + +==== + +[NOTE] +.Connector Metadata +==== + +Most connectors are providing metadata to let you extract technical +details about the message, but also customize the outbound dispatching. + +==== + +== Messages vs. Payloads + +Reactive messaging offers flexibility when it comes to handling messages and their acknowledgements. +The application developer can choose to finely handle acknowledgements per-message basis, by handling the +`Message`-based signatures. Otherwise, when handling payloads, acknowledgements +(and negative-acknowledgements) are handled by the framework. +The following sections in this documentation detail both development models. + +While being the easier development model, in the past using payload-based signatures did not allow associating connector-specific metadata, +making the `Message`-based signatures the de-facto choice even for the most common scenarios. +This lead to using the connector custom-message implementation types, +such as `IncomingKafkaRecord`, `KafkaRecord` or `IncomingRabbitMQMessage`, +as a convenience for accessing connector-specific metadata. + +Not only this forces to handle acknowledgements manually, +it also doesn't allow for incoming or outgoing `Messages` to be xref:concepts/decorators.adoc#intercepting-incoming-and-outgoing-messages[intercepted] +or xref:concepts/observability.adoc[observed]. + +[WARNING] +.Custom `Message` types & Message interception +==== + +Custom `Message` types such as `IncomingKafkaRecord`, `KafkaRecord` or `IncomingRabbitMQMessage` +are not compatible with features intercepting messages. +Therefore, it is no longer recommended to use custom `Message` implementations in consumptions methods. +Instead, you can either use the generic `Message` type and access specific metadata, +or use the payload with xref:concepts/incoming-metadata-injection.adoc[metadata injection] + +==== + +Since xref:concepts/incoming-metadata-injection.adoc[incoming metadata injection] and xref:concepts/generic-payloads.adoc[generic payloads] +features added to SmallRye Reactive Messaging, +it is easier to use payload signatures and benefit from acknowledgement handling and still access connector-specific metadata. + +== Generating Messages + +To produce messages to a channel, you need to use the `@Outgoing` +annotation. This annotation takes a single parameter: the name of the +populated channel. + +=== Generating messages synchronously + +You can generate messages synchronously. In this case, the method is +called for every _request_ from the downstream: + +[source,java] +---- +include::{examples-dir}/generation/GenerationExamples.java[tags=message-sync,indent=0] +---- + +[NOTE] +.Requests? +==== + +Reactive Messaging connects components to build a reactive stream. +In a reactive stream, the emissions are controlled by the consumer +(downstream) indicating to the publisher (upstream) how many items it +can consume. With this protocol, the consumers are never flooded. + +==== + +=== Generating messages using CompletionStage + +You can also return a `CompletionStage` / `CompletableFuture`. +In this case, Reactive Messaging waits until the `CompletionStage` gets completed before calling it again. + +For instance, this signature is useful to poll messages from a source using an asynchronous client: + +[source,java] +---- +include::{examples-dir}/generation/GenerationExamples.java[tags=message-cs,indent=0] +---- + +=== Generating messages using Uni + +You can also return a https://smallrye.io/smallrye-mutiny/#_uni_and_multi[Uni] instance. +In this case, Reactive Messaging waits until the `Uni` emits its item before calling it again. + +This signature is useful when integrating asynchronous clients providing a Mutiny API. + +[source,java] +---- +include::{examples-dir}/generation/GenerationExamples.java[tags=message-uni,indent=0] +---- + +=== Generating Reactive Streams of messages + +Instead of producing the message one by one, you can return the stream +directly. If you have a data source producing Reactive Streams +`Publisher` (or sub-types, such as `Multi`), this is the signature you are looking for: + +[source,java] +---- +include::{examples-dir}/generation/GenerationExamples.java[tags=message-stream,indent=0] +---- + +In this case, the method is called once to retrieve the `Publisher`. + +== Generating Payloads + +Instead of `Message`, you can produce _payloads_. In this case, Reactive +Messaging produces a simple message from the _payload_ using +`Message.of`. + +=== Generating payload synchronously + +You can produce _payloads_ synchronously. The framework calls the method +upon request and create `Messages` around the produced payloads. + +[source,java] +---- +include::{examples-dir}/generation/GenerationExamples.java[tags=payload-sync,indent=0] +---- + +=== Generating payload using CompletionStage + +You can also return `CompletionStage` or `CompletableFuture`. For +example, if you have an asynchronous client returning `CompletionStage`, +you can use it as follows, to poll the data one by one: + +[source,java] +---- +include::{examples-dir}/generation/GenerationExamples.java[tags=payload-cs,indent=0] +---- + +=== Generating payload by producing Unis + +You can also return a `Uni` if you have a client using Mutiny types: + +[source,java] +---- +include::{examples-dir}/generation/GenerationExamples.java[tags=payload-uni,indent=0] +---- + +=== Generating Reactive Streams of payloads + +Finally, you can return a `Publisher` (or a sub-type such as a `Multi`): + +[source,java] +---- +include::{examples-dir}/generation/GenerationExamples.java[tags=payload-stream,indent=0] +---- + +In this case, Reactive Messaging calls the method only once to retrieve +the `Publisher`. + +== Consuming Messages + +To consume messages from a channel, you need to use the `@Incoming` +annotation. This annotation takes a single parameter: the name of the +consumed channel. + +Because `Messages` must be acknowledged, consuming messages requires +returning _asynchronous results_ that would complete when the incoming +message get acknowledged. + +For example, you can receive the `Message`, process it and return the +acknowledgement as result: + +[source,java] +---- +include::{examples-dir}/consumption/ConsumptionExamples.java[tags=message-cs,indent=0]' +---- + +You can also return a `Uni` if you need to implement more complicated +processing: + +[source,java] +---- +include::{examples-dir}/consumption/ConsumptionExamples.java[tags=message-uni,indent=0] +---- + +== Consuming Payloads + +Unlike consuming messages, consuming payloads support both synchronous +and asynchronous consumption. + +For example, you can consume a payload as follows: + +[source,java] +---- +include::{examples-dir}/consumption/ConsumptionExamples.java[tags=payload-sync,indent=0] +---- + +In this case, you don’t need to deal with the acknowledgement yourself. +The framework acknowledges the incoming message (that wrapped the +payload) once your method returns successfully. + +If you need to achieve asynchronous actions, you can return a +`CompletionStage` or a `Uni`: + +[source,java] +---- +include::{examples-dir}/consumption/ConsumptionExamples.java[tags=payload-cs,indent=0] +---- + +[source,java] +---- +include::{examples-dir}/consumption/ConsumptionExamples.java[tags=payload-uni,indent=0] +---- + +In these 2 cases, the framework acknowledges the incoming message when +the returned construct gets _completed_. + +== Processing Messages + +You can process `Message` both synchronously or asynchronously. +This later case is useful when you need to execute an asynchronous action during your processing such as invoking a remote service. + +Do process `Messages` synchronously uses: + +[source,java] +---- +include::{examples-dir}/processing/ProcessingExamples.java[tags=process-message,indent=0] +---- + +This method transforms the `int` payload to a `String`, and wraps it +into a `Message`. + +'''important "Using `Message.withX` methods" +You may be surprised by the usage of `Message.withX` methods. It allows +metadata propagation as the metadata would be copied from the incoming +message and so dispatched to the next method. + +You can also process `Messages` asynchronously: + +[source,java] +---- +include::{examples-dir}/processing/ProcessingExamples.java[tags=process-message-cs,indent=0] +---- + +Or using Mutiny: + +[source,java] +---- +include::{examples-dir}/processing/ProcessingExamples.java[tags=process-message-uni,indent=0] +---- + +In general, you want to create the new `Message` from the incoming one. +It enables metadata propagation and post-acknowledgement. For this, use +the `withX` method from the `Message` class returning a new `Message` +instance but copy the _content_ (metadata, ack/nack...). + +== Processing payloads + +If you don’t need to manipulate the envelope, you can process payload +directly either synchronously or asynchronously: + +[source,java] +---- +include::{examples-dir}/processing/ProcessingExamples.java[tags=process-payload,indent=0] +---- + +[source,java] +---- +include::{examples-dir}/processing/ProcessingExamples.java[tags=process-payload-cs,indent=0] +---- + +[source,java] +---- +include::{examples-dir}/processing/ProcessingExamples.java[tags=process-payload-uni,indent=0] +---- + +[NOTE] +.What about metadata? +==== + +With these methods, the metadata are automatically propagated. + +==== + +== Processing streams + +The previous processing method were taking single `Message` or payload. +Sometimes you need more advanced manipulation. For this, SmallRye +Reactive Messaging lets you process the stream of `Message` or the +stream of payloads directly: + +[source,java] +---- +include::{examples-dir}/processing/StreamExamples.java[tags=processing-stream-message,indent=0] +---- + +[source,java] +---- +include::{examples-dir}/processing/StreamExamples.java[tags=processing-stream-payload,indent=0] +---- + +You can receive either a (Reactive Streams) `Publisher`, a +`PublisherBuilder` or (Mutiny) `Multi`. You can return any subclass of +`Publisher` or a `Publisher` directly. + +[IMPORTANT] +==== + +These signatures do not support metadata propagation. In the case of a +stream of `Message`, you need to propagate the metadata manually. In the +case of a stream of payload, propagation is not supported, and incoming +metadata are lost. + +==== diff --git a/content/docs/smallrye-reactive-messaging/latest/concepts/observability.adoc b/content/docs/smallrye-reactive-messaging/latest/concepts/observability.adoc new file mode 100644 index 000000000..aa57e8238 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/concepts/observability.adoc @@ -0,0 +1,38 @@ += Observability API +:version-root: ../ +include::{version-root}_attributes.adoc[] + +[IMPORTANT] +==== + +Observability API is experimental and SmallRye only feature. + +==== + +Smallrye Reactive Messaging proposes an observability API that allows to observe messages received and send through inbound and outbound channels. + +For any observation to happen, you need to provide an implementation of the `MessageObservationCollector`, discovered as a CDI-managed bean. + +At wiring time the discovered `MessageObservationCollector` implementation `initObservation` method is called once per channel to initialize the `ObservationContext`. +The default `initObservation` implementation returns a default `ObservationContext` object, +but the collector implementation can provide a custom per-channel `ObservationContext` object that'll hold information necessary for the observation. +The `ObservationContext#complete` method is called each time a message observation is completed – message being acked or nacked. +The collector implementation can decide at initialization time to disable the observation per channel by returning a `null` observation context. + +For each new message, the collector is on `onNewMessage` method with the channel name, the `Message` and the `ObservationContext` object initialized beforehand. +This method can react to the creation of a new message but also is responsible for instantiating and returning a `MessageObservation`. +While custom implementations can augment the observability capability, SmallRye Reactive Messaging provides a default implementation `DefaultMessageObservation`. + +So a simple observability collector can be implemented as such: + +[source,java] +---- +include::{examples-dir}/observability/SimpleMessageObservationCollector.java[indent=0] +---- + +A collector with a custom `ObservationContext` can be implemented as such : + +[source,java] +---- +include::{examples-dir}/observability/ContextMessageObservationCollector.java[indent=0] +---- diff --git a/content/docs/smallrye-reactive-messaging/latest/concepts/outgoings.adoc b/content/docs/smallrye-reactive-messaging/latest/concepts/outgoings.adoc new file mode 100644 index 000000000..64adca658 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/concepts/outgoings.adoc @@ -0,0 +1,79 @@ += Multiple Outgoing Channels +:version-root: ../ +include::{version-root}_attributes.adoc[] + +[WARNING] +.Experimental +==== + +Multiple `@Outgoings` is an experimental feature. + +==== + +The `@Outgoing` annotation is repeatable. It means that the method +dispatches outgoing messages to multiple listed channels: + +[source,java] +---- +include::{examples-dir}/outgoings/OutgoingsExample.java[tags=code,indent=0] +---- + +The default behaviour is same as the xref:concepts/broadcast.adoc[@Broadcast annotation], +meaning that outbound messages are dispatched to all listed outgoing channels. + +However, different dispatching mechanism can be employed: + +== Selectively dispatching messages using `Targeted` messages + +You can selectively dispatch messages to multiple outgoings by returning +link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/io/smallrye/reactive/messaging/Targeted.html[`Targeted`] : + +[source,java] +---- +include::{examples-dir}/outgoings/TargetedExample.java[tags=code,indent=0] +---- + +In this example, three outgoing channels are declared on the `process` method +but in some condition channel `out3` does not receive any messages. + +[IMPORTANT] +.Coordinated acknowledgements +==== + +`Targeted` return types coordinate acknowledgements between outgoing messages +and the incoming message, therefore the incoming message will be ack'ed only +when all outgoing messages are ack'ed. + +==== + +In cases where you need to consume `Message` and handle metadata propagation +more finely you can use link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/io/smallrye/reactive/messaging/TargetedMessages.html[`TargetedMessages`] +which is a `Message` type: + +[source,java] +---- +include::{examples-dir}/outgoings/TargetedExample.java[tags=targeted-messages,indent=0] +---- + +Note that in this case coordinated acknowledgements is handled explicitly +using link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/io/smallrye/reactive/messaging/Messages.html[`Messages`] utility. + +== Branching outgoing channels with `MultiSplitter` + +In stream transformer processors it can be useful to branch out an incoming +stream into different sub-streams, based on some conditions. + +When consuming a `Multi`, you can use the `Multi.split` +(see https://smallrye.io/smallrye-mutiny/latest/guides/multi-split/[Mutiny documentation]) +operation to define multiple branches. +The stream transformer method with multiple outgoings must return a +link:https://javadoc.io/doc/io.smallrye.reactive/mutiny/{javadoc-version}/io/smallrye/mutiny/operators/multi/split/MultiSplitter.html[`MultiSplitter`]. + +[source,java] +---- +include::{examples-dir}/outgoings/SplitterMultiExample.java[tags=code,indent=0] +---- + +In this case the number of outgoing channels must match the number of branches given to `split` operation. +Outgoing channels will be tried to be matched to branch identifier enum `toString` ignoring case. +If not all branches are matched, it will fall back to one-by-one matching depending on the order of outgoing channel declarations and enum ordinals. diff --git a/content/docs/smallrye-reactive-messaging/latest/concepts/pausable-channels.adoc b/content/docs/smallrye-reactive-messaging/latest/concepts/pausable-channels.adoc new file mode 100644 index 000000000..2aeae29c4 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/concepts/pausable-channels.adoc @@ -0,0 +1,64 @@ += Pausable Channels +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Based on reactive streams, Smallrye Reactive Messaging ensures that channels are back-pressured. +This means that the flow of messages is controlled by the downstream consumer, +whether it is a processing method or outgoing channel. +Sometimes you may want to pause the flow of messages, for example, when the consumer is not ready to process them. + +Injected xref:concepts/emitter.adoc#retrieving-channels[`@Channel`] streams are not subscribed to by default, so the flow of messages is controlled by the application. +But for xref:concepts/model.adoc#incoming-and-outgoing[`@Incoming`] methods, the flow of messages is controlled by the runtime. + +Pausable channels are useful when you want to control the flow of messages within your application. + +== Creating a Pausable Channel + +To use pausable channels, you need to activate it with the configuration property `pausable` set to `true`. + +[source,properties] +---- +mp.messaging.incoming.my-channel.pausable=true +---- + +=== Configuration Options + +Pausable channels support the following configuration options: + +* `pausable.initially-paused` - Whether the channel starts in a paused state (default: `false`) +* `pausable.late-subscription` - Whether to subscribe to the upstream after pausing (default: `false`) +* `pausable.buffer-size` - Maximum buffer size for items received while the channel is paused (optional) +* `pausable.buffer-strategy` - Strategy for handling already requested items that arrive while paused. Possible values are: +** `BUFFER` - Buffer items received while paused and deliver them when resumed (default) +** `DROP` - Drop already requested items received while paused +** `IGNORE` - Let already requested items flow through even while paused + +Example configuration: + +[source,properties] +---- +mp.messaging.incoming.my-channel.pausable=true +mp.messaging.incoming.my-channel.pausable.initially-paused=true +mp.messaging.incoming.my-channel.pausable.buffer-size=100 +mp.messaging.incoming.my-channel.pausable.buffer-strategy=BUFFER +mp.messaging.incoming.my-channel.pausable.late-subscription=false +---- + +== Controlling the flow of messages + +If a channel is configured to be pausable, +you can inject the `PausableChannel` qualified with the `@Channel("channel-name")` annotation to control the flow of messages. +You can also get the `PausableChannel` by channel name from the `ChannelRegistry` programmatically, +and pause or resume the channel as needed: + +[source,java] +---- +include::{examples-dir}/pausable/PausableController.java[indent=0] +---- + +[WARNING] +==== + +Pausable channels only work with back-pressure aware subscribers, with bounded downstream requests. + +==== diff --git a/content/docs/smallrye-reactive-messaging/latest/concepts/signatures.adoc b/content/docs/smallrye-reactive-messaging/latest/concepts/signatures.adoc new file mode 100644 index 000000000..ef35c854f --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/concepts/signatures.adoc @@ -0,0 +1,105 @@ += Supported signatures +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The following tables list the supported method signatures and indicate +the various supported features. For instance, they indicate the default +and available acknowledgement strategies (when applicable). + +== Method signatures to generate data + +[width="100%",cols="32%,68%",options="header",] +|=== +|Signature |Invocation time +|`@Outgoing Publisher++<++Message++<++O++>>++ method()` `{vbar} Called once at ++*++assembly++*++ time {vbar} {vbar}`@Outgoing Publisher method()`` |Called once at _assembly_ time +|`@Outgoing Multi++<++Message++<++O++>>++ method()` `{vbar} Called once at ++*++assembly++*++ time {vbar} {vbar}`@Outgoing Multi method()`` |Called once at _assembly_ time +|`@Outgoing Flow.Publisher++<++Message++<++O++>>++ method()` `{vbar} Called once at ++*++assembly++*++ time {vbar} {vbar}`@Outgoing Flow.Publisher method()`` |Called once at _assembly_ time +|`@Outgoing PublisherBuilder++<++Message++<++O++>>++ method()` `{vbar} Called once at ++*++assembly++*++ time {vbar} {vbar}`@Outgoing PublisherBuilder method()`` |Called once at _assembly_ time +|`@Outgoing Message++<++O++>++ method()` `{vbar} Called for every downstream request, sequentially {vbar} {vbar}`@Outgoing O method()`` |Called for every downstream request, sequentially +|`@Outgoing CompletionStage++<++Message++<++O++>>++ method()` `{vbar} Called for every downstream request, sequentially (After the completion of the last returned CompletionStage) {vbar} {vbar}`@Outgoing CompletionStage method()`` |Called for every downstream request, , sequentially (After the completion of the last returned CompletionStage) +|`@Outgoing Uni++<++Message++<++O++>>++ method()` `{vbar} Called for every downstream request, sequentially (After the completion of the last returned Uni) {vbar} {vbar}`@Outgoing Uni method()`` |Called for every downstream request, , sequentially (After the completion of the last returned Uni) +|=== + +== Method signatures to consume data + +[width="100%",cols="37%,32%,31%",options="header",] +|=== +|Signature |Invocation time |Supported Acknowledgement Strategies +|`@Incoming void method(I p)` |Called for every incoming payload (sequentially) |_POST++_++PROCESSING_, NONE, PRE++_++PROCESSING +|`@Incoming CompletionStage++<++?++>++ method(Message++<++I++>++ msg)` |Called for every incoming message (sequentially) |_MANUAL_, NONE, PRE++_++PROCESSING +|`@Incoming CompletionStage++<++?++>++ method(I p)` |Called for every incoming payload (sequentially) |_POST++_++PROCESSING_, PRE++_++PROCESSING, NONE +|`@Incoming Uni++<++?++>++ method(Message++<++I++>++ msg)` |Called for every incoming message (sequentially) |_MANUAL_, NONE, PRE++_++PROCESSING +|`@Incoming Uni++<++?++>++ method(I p)` |Called for every incoming payload (sequentially) |_POST++_++PROCESSING_, PRE++_++PROCESSING, NONE +|`@Incoming Subscriber++<++Message++<++I++>>++ method()` |Called once at _assembly_ time |_MANUAL_, POST++_++PROCESSING, NONE, PRE++_++PROCESSING +|`@Incoming Subscriber++<++I++>++ method()` |Called once at _assembly_ time |_POST++_++PROCESSING_, NONE, PRE++_++PROCESSING +|`@Incoming Flow.Subscriber++<++Message++<++I++>>++ method()` |Called once at _assembly_ time |_MANUAL_, POST++_++PROCESSING, NONE, PRE++_++PROCESSING +|`@Incoming Flow.Subscriber++<++I++>++ method()` |Called once at _assembly_ time |_POST++_++PROCESSING_, NONE, PRE++_++PROCESSING +|`@Incoming SubscriberBuilder++<++Message++<++I++>++, ?++>++ method()` |Called once at _assembly_ time |_MANUAL_, POST++_++PROCESSING, NONE, PRE++_++PROCESSING +|`@Incoming SubscriberBuilder++<++I, ?++>++ method()` |Called once at _assembly_ time |_MANUAL_, POST++_++PROCESSING, NONE, PRE++_++PROCESSING +|=== + +== Method signatures to process data + +[width="99%",cols="37%,23%,30%,10%",options="header",] +|=== +|Signature |Invocation time |Supported Acknowledgement Strategies |Metadata Propagation +|`@Outgoing @Incoming Message++<++O++>++ method(Message++<++I++>++ msg)` |Called for every incoming message (sequentially) |POST++_++PROCESSING (Smallrye only), _MANUAL_, NONE, PRE++_++PROCESSING |manual +|`@Outgoing @Incoming Message++<++O++>++ method(I payload)` |Called for every incoming message (sequentially) |_POST++_++PROCESSING_ (Smallrye only), NONE, PRE++_++PROCESSING |automatic +|`@Outgoing @Incoming O method(I payload)` |Called for every incoming payload (sequentially) |_POST++_++PROCESSING_, NONE, PRE++_++PROCESSING |automatic +|`@Outgoing @Incoming CompletionStage++<++Message++<++O++>>++ method(Message++<++I++>++ msg)` |Called for every incoming message (sequentially) |_MANUAL_, NONE, PRE++_++PROCESSING |manual +|`@Outgoing @Incoming CompletionStage++<++O++>++ method(I payload)` |Called for every incoming payload (sequentially) |_POST++_++PROCESSING_, NONE, PRE++_++PROCESSING |automatic +|`@Outgoing @Incoming CompletionStage++<++Message++<++O++>>++ method(I payload)` |Called for every incoming payload (sequentially) |_POST++_++PROCESSING_ (Smallrye only), NONE, PRE++_++PROCESSING |automatic +|`@Outgoing @Incoming Uni++<++Message++<++O++>>++ method(Message++<++I++>++ msg)` |Called for every incoming message (sequentially) |_MANUAL_, NONE, PRE++_++PROCESSING |manual +|`@Outgoing @Incoming Uni++<++Message++<++O++>>++ method(I payload)` |Called for every incoming payload (sequentially) |_POST++_++PROCESSING_ (Smallrye only), NONE, PRE++_++PROCESSING |automatic +|`@Outgoing @Incoming Uni++<++O++>++ method(I payload)` |Called for every incoming payload (sequentially) |_POST++_++PROCESSING_, NONE, PRE++_++PROCESSING |automatic +|`@Outgoing @Incoming Processor++<++Message++<++I++>++, Message++<++O++>>++ method()` |Called once at _assembly_ time |_MANUAL_, PRE++_++PROCESSING, NONE |manual +|`@Outgoing @Incoming Processor++<++I, O++>++ method()` |Called once at _assembly_ time |_PRE++_++PROCESSING_, NONE |not supported +|`@Outgoing @Incoming Flow.Processor++<++Message++<++I++>++, Message++<++O++>>++ method()` |Called once at _assembly_ time |_MANUAL_, PRE++_++PROCESSING, NONE |manual +|`@Outgoing @Incoming Flow.Processor++<++I, O++>++ method()` |Called once at _assembly_ time |_PRE++_++PROCESSING_, NONE |not supported +|`@Outgoing @Incoming ProcessorBuilder++<++Message++<++I++>++, Message++<++O++>>++ method()` |Called once at _assembly_ time |_MANUAL_, PRE++_++PROCESSING, NONE |manual +|`@Outgoing @Incoming ProcessorBuilder++<++I, O++>++ method()` |Called once at _assembly_ time |_PRE++_++PROCESSING_, NONE |not supported +|`@Outgoing @Incoming Publisher++<++Message++<++O++>>++ method(Message++<++I++>++ msg)` |Called for every incoming message (sequentially) |_MANUAL_, PRE++_++PROCESSING, NONE |manual +|`@Outgoing @Incoming Publisher++<++O++>++ method(I payload)` |Called for every incoming payload (sequentially) |_PRE++_++PROCESSING_, POST++_++PROCESSING, NONE |automatic +|`@Outgoing @Incoming Multi++<++Message++<++O++>>++ method(Message++<++I++>++ msg)` |Called for every incoming message (sequentially) |_MANUAL_, PRE++_++PROCESSING, NONE |manual +|`@Outgoing @Incoming Multi++<++O++>++ method(I payload)` |Called for every incoming payload (sequentially) |_PRE++_++PROCESSING_, POST++_++PROCESSING, NONE |automatic +|`@Outgoing @Incoming Flow.Publisher++<++Message++<++O++>>++ method(Message++<++I++>++ msg)` |Called for every incoming message (sequentially) |_MANUAL_, PRE++_++PROCESSING, NONE |manual +|`@Outgoing @Incoming Flow.Publisher++<++O++>++ method(I payload)` |Called for every incoming payload (sequentially) |_PRE++_++PROCESSING_, POST++_++PROCESSING, NONE |automatic +|`@Outgoing @Incoming PublisherBuilder++<++Message++<++O++>>++ method(Message++<++I++>++ msg)` |Called for every incoming message (sequentially) |_MANUAL_, PRE++_++PROCESSING, NONE |manual +|`@Outgoing @Incoming PublisherBuilder++<++O++>++ method(I payload)` |Called for every incoming payload (sequentially) |_PRE++_++PROCESSING_, POST++_++PROCESSING, NONE |automatic +|=== + +Note that in additional to the MicroProfile Reactive Messaging specification, +SmallRye Reactive Messaging supports the post-processing acknowledgment handling with automatic metadata propagation for the following signatures: + +* `@Outgoing @Incoming Message++<++O++>++ method(I payload)` +* `@Outgoing @Incoming CompletionStage++<++Message++<++O++>>++ method(I payload)` +* `@Outgoing @Incoming Uni++<++Message++<++O++>>++ method(I payload)` +* `@Outgoing @Incoming Message++<++O++>++ method(Message++<++I++>++ payload)` : For this signature, the post-processing acknowledgment handling is limited. +It covers cases for nacking incoming messages on caught exceptions at the method body, acking incoming messages when outgoing message is skipped by returning `null`, and chaining acknowlegment from outgoing message to the incoming. +However, if the incoming message has already been (n)acked, you will experience duplicate (n)acks. + +== Method signatures to manipulate streams + +[width="100%",cols="52%,17%,20%,11%",options="header",] +|=== +|Signature |Invocation time |Supported Acknowledgement Strategies |Metadata Propagation +|`@Outgoing @Incoming Publisher++<++Message++<++O++>>++ method(Publisher++<++Message++<++I++>>++ pub)` |Called once at _assembly_ time |_MANUAL_, NONE, PRE++_++PROCESSING |manual +|`@Outgoing @Incoming Publisher++<++O++>++ method(Publisher++<++I++>++ pub)` |Called once at _assembly_ time |_PRE++_++PROCESSING_, NONE |not supported +|`@Outgoing @Incoming Multi++<++Message++<++O++>>++ method(Multi++<++Message++<++I++>>++ pub)` |Called once at _assembly_ time |_MANUAL_, NONE, PRE++_++PROCESSING |manual +|`@Outgoing @Incoming Multi++<++O++>++ method(Multi++<++I++>++ pub)` |Called once at _assembly_ time |_PRE++_++PROCESSING_, NONE |not supported +|`@Outgoing @Incoming Flow.Publisher++<++Message++<++O++>>++ method(Flow.Publisher++<++Message++<++I++>>++ pub)` |Called once at _assembly_ time |_MANUAL_, NONE, PRE++_++PROCESSING |manual +|`@Outgoing @Incoming Flow.Publisher++<++O++>++ method(Flow.Publisher++<++I++>++ pub)` |Called once at _assembly_ time |_PRE++_++PROCESSING_, NONE |not supported +|`@Outgoing @Incoming PublisherBuilder++<++Message++<++O++>>++ method(PublisherBuilder++<++Message++<++I++>>++ pub)` |Called once at _assembly_ time |_MANUAL_, NONE, PRE++_++PROCESSING |manual +|`@Outgoing @Incoming PublisherBuilder++<++O++>++ method(PublisherBuilder++<++I++>++ pub)` |Called once at _assembly_ time |NONE, PRE++_++PROCESSING |not supported +|=== + +[IMPORTANT] +==== + +When processing `Message`, it is often required to _chain_ the incoming +`Message` to enable post-processing acknowledgement and metadata +propagation. Use the `with` (like `withPayload`) methods from the +incoming message, so it copies the metadata and ack/nack methods. It +returns a new `Message` with the right content. + +==== diff --git a/content/docs/smallrye-reactive-messaging/latest/concepts/skipping.adoc b/content/docs/smallrye-reactive-messaging/latest/concepts/skipping.adoc new file mode 100644 index 000000000..2717afe78 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/concepts/skipping.adoc @@ -0,0 +1,29 @@ += Skipping messages +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Sometimes you receive a message and don’t want to produce an output +message. To handle this, you have several choices: + +[arabic] +. for method processing single message or payload, producing `null` +would produce an ignored message (not forwarded) +. for method processing streams, you can generate an _empty_ stream. + +== Skipping a single item + +To skip a single message or payload, return `null`: + +[source,java] +---- +include::{examples-dir}/skip/SingleSkip.java[tags=skip,indent=0] +---- + +== Skipping in a stream + +To skip a message or payload when manipulating a stream, emit an _empty_ `Multi` (or `Publisher`): + +[source,java] +---- +include::{examples-dir}/skip/StreamSkip.java[tags=skip,indent=0] +---- diff --git a/content/docs/smallrye-reactive-messaging/latest/concepts/testing.adoc b/content/docs/smallrye-reactive-messaging/latest/concepts/testing.adoc new file mode 100644 index 000000000..c61cb82d6 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/concepts/testing.adoc @@ -0,0 +1,131 @@ += Testing your application +:version-root: ../ +include::{version-root}_attributes.adoc[] + +SmallRye Reactive Messaging provides a *test connector* that replaces +real broker connectors during tests. It lets you deliver messages to +incoming channels and verify messages sent to outgoing channels — without +requiring any external infrastructure. + +Add the following dependency to your project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.reactive + smallrye-reactive-messaging-testing + {project-version} + test + +---- + +Then, in a test, you can do something like: + +[source,java] +---- +include::{examples-dir}/testing/MyTest.java[indent=0] +---- + +When switching a channel to the test connector, all the +configuration properties are ignored. + +[WARNING] +==== + +This connector has been designed for testing purpose only. +Switching the channel to the test connector means that the +original connector is not invoked at all during tests. +Therefore, if your code depends on a specific connector behaviour +or a custom metadata you need to simulate those in your tests. + +==== + +The _switch_ methods return `Map++<++String, String++>++` instances containing +the set properties. While these system properties are already set, you +can retrieve them and pass them around, for example if you need to start +an external process with these properties: + +[source,java] +---- +include::{examples-dir}/testing/MyTestSetup.java[tags=code,indent=0] +---- + +[NOTE] +==== + +The test connector supports the `broadcast` and `merge` attributes. +So, if your connector is configured with `broadcast: true`, the +connector broadcasts the messages to all the channel consumers. If your +connector is configured with `merge:true`, the connector receives all +the messages sent to the mapped channel even when coming from multiple +producers. + +==== + +== Testing with connector-specific emitters + +Connector-specific emitter types such as `KafkaTransactions` or `KafkaRequestReply` work +with the test connector. When the channel uses the test connector instead of the native +connector, a fallback emitter is created automatically: + +* *Transaction emitters* (`KafkaTransactions`, `PulsarTransactions`): Transaction +semantics (begin/commit/abort) are skipped. Messages sent through the +`TransactionalEmitter` are dispatched normally and can be verified via the `TestOutgoing`. +The `markForAbort()` method is supported and causes the transaction `Uni` to fail with +`TransactionAbortedException`. All `withTransaction` variants work, including +exactly-once methods — the offset/acknowledgement management is skipped but the +work function executes normally. +* *Request-reply emitters* (`KafkaRequestReply`, `AmqpRequestReply`, +`RabbitMQRequestReply`): Requests are sent through the normal emitter path (visible +in the `TestOutgoing`). Replies are resolved via a programmatic reply function that you +configure in your test by casting the emitter to its no-op implementation and calling +`setReplyFunction()`. + +== Simulating connector-specific metadata + +When your application reads connector-specific metadata from incoming messages, +you can attach it using the `deliver(payload, metadata...)` overload: + +[source,java] +---- +incoming.deliver(new Order(...), + new IncomingKafkaRecordMetadata<>( + new ConsumerRecord<>("orders", 0, 42L, "key", "value"), "orders")); +---- + +This creates a `Message` with the given payload and metadata, so your application +code can retrieve it via `message.getMetadata(IncomingKafkaRecordMetadata.class)` +or through metadata injection in `@Incoming` method parameters. + +== Vert.x Context with Test Connector + +For the sake of simplicity, test connector channels dispatch messages on the caller thread of `TestIncoming#deliver` method. +However, most of the other connectors handle context propagation dispatching messages on separate xref:concepts/message-context.adoc[duplicated Vert.x contexts]. + +If this causes a change of behaviour in your tests, +you can configure the test connector channels with `run-on-vertx-context` attribute to dispatch events, +including messages and acknowledgements, on a Vert.x context. +Alternatively you can switch this behaviour using the `TestIncoming#runOnVertxContext` method. + +== Migrating from InMemoryConnector + +The `InMemoryConnector` class and the `smallrye-in-memory` connector name are deprecated. +Use `TestingConnector` with the `smallrye-testing` connector name instead. The old API continues +to work — both connector names are supported simultaneously, and the old classes extend +the new ones. + +[width="100%",cols="50%,50%",options="header",] +|=== +|Deprecated |Replacement +|`InMemoryConnector` |`TestingConnector` +|`InMemorySource` |`TestIncoming` +|`InMemorySink` |`TestOutgoing` +|`connector.source("channel")` |`connector.incoming("channel")` +|`connector.sink("channel")` |`connector.outgoing("channel")` +|`source.send(payload)` |`incoming.deliver(payload)` +|`sink.received()` |`outgoing.sent()` +|`InMemoryConnector.switchIncomingChannelsToInMemory(...)` |`TestingConnector.switchIncomingChannelsToTesting(...)` +|`InMemoryConnector.switchOutgoingChannelsToInMemory(...)` |`TestingConnector.switchOutgoingChannelsToTesting(...)` +|`"smallrye-in-memory"` |`"smallrye-testing"` +|`smallrye-reactive-messaging-in-memory` (Maven) |`smallrye-reactive-messaging-testing` +|=== diff --git a/content/docs/smallrye-reactive-messaging/latest/getting-started.adoc b/content/docs/smallrye-reactive-messaging/latest/getting-started.adoc new file mode 100644 index 000000000..9cc19d837 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/getting-started.adoc @@ -0,0 +1,63 @@ += Getting Started +include::_attributes.adoc[] + +The easiest way to start using SmallRye Reactive Messaging is from https://quarkus.io[Quarkus]. +SmallRye Reactive Messaging can also be used standalone, or with https://openliberty.io/guides/microprofile-reactive-messaging.html[Open Liberty]. + +First, go to https://code.quarkus.io/?g=io.smallrye&a=getting-started-with-reactive-messaging&e=smallrye-reactive-messaging[code.quarkus.io]. +Select the `smallrye-reactive-messaging` extension (already done if you use the link), and then click on the generate button to download the code. + +One downloaded, unzip the project and import it in your IDE. + +If you look at the `pom.xml` file, you will see the following dependency: + +[source,xml] +---- + + io.quarkus + quarkus-smallrye-reactive-messaging + +---- + +It provides the support for SmallRye Reactive Messaging. + +Ok, so far so good, but we need event-driven _beans_. + +Create the `quickstart` package, and copy the following class into it: + +For instance: + +[source,java] +---- +include::{examples-dir}/quickstart/ReactiveMessagingExample.java[indent=0] +---- + +This class contains a set of methods: + +* producing messages (`source`) +* processing messages (`toUpperCase`) +* transforming the stream by skipping messages (`filter`) +* consuming messages (`sink`) + +Each of these methods are connected through _channels_. + +Now, let's see this in action. +For the terminal, run: + +[source,shell] +---- +> ./mvnw quarkus:dev +---- + +Running the previous example should give the following output: + +[source,text] +---- +>> HELLO +>> SMALLRYE +>> REACTIVE +>> MESSAGE +---- + +Of course, this is a very simple example. +To go further, let's have a look to the core xref:concepts/concepts.adoc[concepts] behind SmallRye Reactive Messaging. diff --git a/content/docs/smallrye-reactive-messaging/latest/images/chain.png b/content/docs/smallrye-reactive-messaging/latest/images/chain.png new file mode 100644 index 000000000..eacfc241c Binary files /dev/null and b/content/docs/smallrye-reactive-messaging/latest/images/chain.png differ diff --git a/content/docs/smallrye-reactive-messaging/latest/images/channels.png b/content/docs/smallrye-reactive-messaging/latest/images/channels.png new file mode 100644 index 000000000..8b2052ce4 Binary files /dev/null and b/content/docs/smallrye-reactive-messaging/latest/images/channels.png differ diff --git a/content/docs/smallrye-reactive-messaging/latest/images/connectors.png b/content/docs/smallrye-reactive-messaging/latest/images/connectors.png new file mode 100644 index 000000000..ebd50c302 Binary files /dev/null and b/content/docs/smallrye-reactive-messaging/latest/images/connectors.png differ diff --git a/content/docs/smallrye-reactive-messaging/latest/images/favicon.png b/content/docs/smallrye-reactive-messaging/latest/images/favicon.png new file mode 100644 index 000000000..93f705b9c Binary files /dev/null and b/content/docs/smallrye-reactive-messaging/latest/images/favicon.png differ diff --git a/content/docs/smallrye-reactive-messaging/latest/images/incoming.png b/content/docs/smallrye-reactive-messaging/latest/images/incoming.png new file mode 100644 index 000000000..69e57a8b3 Binary files /dev/null and b/content/docs/smallrye-reactive-messaging/latest/images/incoming.png differ diff --git a/content/docs/smallrye-reactive-messaging/latest/images/logo.png b/content/docs/smallrye-reactive-messaging/latest/images/logo.png new file mode 100644 index 000000000..2d957bd52 Binary files /dev/null and b/content/docs/smallrye-reactive-messaging/latest/images/logo.png differ diff --git a/content/docs/smallrye-reactive-messaging/latest/images/message.png b/content/docs/smallrye-reactive-messaging/latest/images/message.png new file mode 100644 index 000000000..fb38e78b0 Binary files /dev/null and b/content/docs/smallrye-reactive-messaging/latest/images/message.png differ diff --git a/content/docs/smallrye-reactive-messaging/latest/images/messages.png b/content/docs/smallrye-reactive-messaging/latest/images/messages.png new file mode 100644 index 000000000..c0b37e6a2 Binary files /dev/null and b/content/docs/smallrye-reactive-messaging/latest/images/messages.png differ diff --git a/content/docs/smallrye-reactive-messaging/latest/images/outgoing.png b/content/docs/smallrye-reactive-messaging/latest/images/outgoing.png new file mode 100644 index 000000000..f5aaef29d Binary files /dev/null and b/content/docs/smallrye-reactive-messaging/latest/images/outgoing.png differ diff --git a/content/docs/smallrye-reactive-messaging/latest/images/overview.png b/content/docs/smallrye-reactive-messaging/latest/images/overview.png new file mode 100644 index 000000000..42e00e441 Binary files /dev/null and b/content/docs/smallrye-reactive-messaging/latest/images/overview.png differ diff --git a/content/docs/smallrye-reactive-messaging/latest/images/processor.png b/content/docs/smallrye-reactive-messaging/latest/images/processor.png new file mode 100644 index 000000000..faac01281 Binary files /dev/null and b/content/docs/smallrye-reactive-messaging/latest/images/processor.png differ diff --git a/content/docs/smallrye-reactive-messaging/latest/images/redhat_reversed.svg b/content/docs/smallrye-reactive-messaging/latest/images/redhat_reversed.svg new file mode 100644 index 000000000..37fe83c48 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/images/redhat_reversed.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/content/docs/smallrye-reactive-messaging/latest/index.adoc b/content/docs/smallrye-reactive-messaging/latest/index.adoc new file mode 100644 index 000000000..153fa5e3d --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/index.adoc @@ -0,0 +1,16 @@ += SmallRye Reactive Messaging +include::_attributes.adoc[] + +*SmallRye Reactive Messaging is a framework for building event-driven, data streaming, and event-sourcing applications.* + +It lets your application interact using various messaging technologies such as Apache Kafka, AMQP or MQTT. +The framework provides a flexible programming model bridging https://www.cdi-spec.org/[CDI] and event-driven. + +SmallRye Reactive Messaging is an implementation of the Eclipse MicroProfile Reactive Messaging specification {spec-version}. + +.Event-Driven Architectures +image::images/overview.png[Event-Driven Architectures] + +SmallRye Reactive Messaging is used in http://quarkus.io[Quarkus], and https://openliberty.io/[Open Liberty]. + +Start with the xref:getting-started.adoc[Getting Started] guide to know how it can be used and introduce various features. diff --git a/content/docs/smallrye-reactive-messaging/latest/jms/advanced-jms.adoc b/content/docs/smallrye-reactive-messaging/latest/jms/advanced-jms.adoc new file mode 100644 index 000000000..7a33879c2 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/jms/advanced-jms.adoc @@ -0,0 +1,37 @@ += Advanced configuration +:version-root: ../ +include::{version-root}_attributes.adoc[] + +== Underlying thread pool + +Lots of JMS operations are blocking and so not cannot be done on the +_caller_ thread. For this reason, these blocking operations are executed +on a worker thread. + +You can configure the thread pool providing these worker threads using +the following MicroProfile Config properties: + +* `smallrye.jms.threads.max-pool-size` - the max number of threads +(Defaults to 10) +* `smallrye.jms.threads.ttl` - the ttl of the created threads +(Defaults to 60 seconds) + +== Selecting the ConnectionFactory + +The JMS Connector requires a `jakarta.jms.ConnectionFactory` to be exposed +as a CDI bean. The connector looks for a `jakarta.jms.ConnectionFactory` +and delegate the interaction with the JMS server to this factory. + +In case you have several connection factories, you can use the +`@Identifier` qualifier on your factory to specify the name. Then, in +the channel configuration, configure the name as follows: + +[source,properties] +---- +# Configure the connector globally +mp.messaging.connector.smallrye-jms.connection-factory-name=my-factory-name +# Configure a specific incoming channel +mp.messaging.incoming.my-channel.connection-factory-name=my-factory-name +# Configure a specific outgoing channel +mp.messaging.outgoing.my-channel.connection-factory-name=my-factory-name +---- diff --git a/content/docs/smallrye-reactive-messaging/latest/jms/jms.adoc b/content/docs/smallrye-reactive-messaging/latest/jms/jms.adoc new file mode 100644 index 000000000..b94527d64 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/jms/jms.adoc @@ -0,0 +1,74 @@ += The JMS connector +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The JMS connector adds support for https://en.wikipedia.org/wiki/Jakarta_Messaging[Jakarta +Messaging] to Reactive +Messaging. It is designed to integrate with _JakartaEE_ applications +that are sending or receiving Jakarta Messaging Messages. + +Jakarta Messaging is a Java Message Oriented Middleware API for sending +messages between two or more clients. It is a programming model to +handle the producer-consumer messaging problem. It is a messaging +standard that allows application components based on Jakarta EE to +create, send, receive, and read messages. It allows the communication +between different components of a distributed application to be loosely +coupled, reliable, and asynchronous. + +Using the JMS connector + +To you the JMS Connector, add the following dependency to your project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.reactive + smallrye-reactive-messaging-jms + {project-version} + +---- + +The connector name is: `smallrye-jms`. + +So, to indicate that a channel is managed by this connector you need: + +[source,properties] +---- +# Inbound +mp.messaging.incoming.[channel-name].connector=smallrye-jms + +# Outbound +mp.messaging.outgoing.[channel-name].connector=smallrye-jms +---- + +The JMS Connector requires a `jakarta.jms.ConnectionFactory` to be exposed +(as CDI bean). The connector looks for a `jakarta.jms.ConnectionFactory` +and delegate the interaction with the JMS server to this factory. In +other words, it creates the JMS connection and context using this +factory. + +So, in order to use this connector you would need to expose a +`jakarta.jms.ConnectionFactory`: + +[source,java] +---- +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; +import jakarta.jms.ConnectionFactory; + +import org.apache.activemq.artemis.jms.client.ActiveMQJMSConnectionFactory; + +@ApplicationScoped +public class ConnectionFactoryBean { + + @Produces + ConnectionFactory factory() { + return new ActiveMQJMSConnectionFactory( + "tcp://localhost:61616", + null, null); + } + +} +---- + +The factory class may depend on your JMS connector/server. diff --git a/content/docs/smallrye-reactive-messaging/latest/jms/receiving-jms-messages.adoc b/content/docs/smallrye-reactive-messaging/latest/jms/receiving-jms-messages.adoc new file mode 100644 index 000000000..08f9af3f5 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/jms/receiving-jms-messages.adoc @@ -0,0 +1,115 @@ += Receiving messages from JMS +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The JMS Connector retrieves JMS Message and maps each of them into +Reactive Messaging `Messages`. + +== Example + +Let’s imagine you have a `jakarta.jms.ConnectionFactory` bean exposed and +connected to your JMS server. Don’t forget that it’s required to use the +JMS connector. + +Configure your application to receive JMS messages on the `prices` +channel as follows: + +[source,properties] +---- +mp.messaging.incoming.prices.connector=smallrye-jms +---- + +[NOTE] +==== + +You don’t need to set the destination. By default, it uses the channel +name (`prices`). You can configure the `destination` attribute to +override it. + +==== + +[NOTE] +==== + +By default the connector uses a `queue`. You can configure it to use a +`topic` by setting `destination-type=topic`. + +==== + +Then, your application receives `Message++<++Double++>++`. You can consume the +payload directly: + +[source,java] +---- +include::{examples-dir}/jms/inbound/JmsPriceConsumer.java[indent=0] +---- + +Or, you can retrieve the `Message++<++Double++>++`: + +[source,java] +---- +include::{examples-dir}/jms/inbound/JmsPriceMessageConsumer.java[indent=0] +---- + +== Deserialization + +The content of the incoming JMS message is mapped to a Java object. + +By default it extracts the JMS Message _body_ as a `java.lang.Object`. +This can be changed by setting, in the incoming JMS Message: + +[arabic] +. The `++_++classname` property +. the `JMSType` + +The value must be a fully qualified class name. The connector then load +the associated class. + +[NOTE] +==== + +The connector loads the associated `Class` using the `TCCL` and if not +found, the classloader used to load the connector. + +==== + +If the target type is a primitive type ort `String`, the resulting +message contains the mapped payload. + +If the target type is a class, the object is built using included JSON +deserializer (JSON-B and Jackson provided OOB from the `JMSType`. +If not, the default behavior is used (Java deserialization). + +== Inbound Metadata + +Messages coming from JMS contains an instance of link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-jms/{javadoc-version}/io/smallrye/reactive/messaging/jms/IncomingJmsMessageMetadata.html[`io.smallrye.reactive.messaging.jms.IncomingJmsMessageMetadata`] in the metadata. + +[source,java] +---- +include::{examples-dir}/jms/inbound/JmsMetadataExample.java[tags=code,indent=0] +---- + +== Acknowledgement + +When the Reactive Messaging `Message` gets acknowledged, the associated +JMS Message is acknowledged. As JMS acknowledgement is blocking, this +acknowledgement is delegated to a worker thread. + +== Failure Handling + +If a message produced from a JMS message is _nacked_, a failure strategy is applied. The JMS connector supports 3 strategies: + +* `fail` - (default) fail the application, no more messages will be processed. The failing message is not acknowledged and may be redelivered by the JMS broker. The application is marked as unhealthy (impacting liveness checks). +* `ignore` - the failure is logged, but the processing continues. The failing message is acknowledged and will not be redelivered. +* `dead-letter-queue` - the failing message is acknowledged and sent to a JMS _dead letter queue_ destination. The processing continues with the next message. ++ +The dead letter queue destination can be configured using the `dead-letter-queue.destination` attribute. If not specified, it defaults to `dead-letter-queue-$channel`. +Messages sent to the dead letter queue preserve the original message body and properties. In addition, the following properties are added: +** `dead++_++letter++_++exception++_++class++_++name` - the fully qualified class name of the exception +** `dead++_++letter++_++reason` - the exception message +** `dead++_++letter++_++cause++_++class++_++name` - the fully qualified class name of the root cause (if available) +** `dead++_++letter++_++cause` - the root cause exception message (if available) + +== Configuration Reference + +include::{generated-dir}/smallrye-jms-incoming.adoc[indent=0] diff --git a/content/docs/smallrye-reactive-messaging/latest/jms/sending-jms-messages.adoc b/content/docs/smallrye-reactive-messaging/latest/jms/sending-jms-messages.adoc new file mode 100644 index 000000000..0e107b798 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/jms/sending-jms-messages.adoc @@ -0,0 +1,106 @@ += Sending messages to JMS +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The JMS Connector can send Reactive Messaging `Messages` as JMS Message. + +== Example + +Let’s imagine you have a `jakarta.jms.ConnectionFactory` bean exposed and +connected to your JMS server. Don’t forget that it’s required to use the +JMS connector. + +Configure your application to write the messages from the `prices` +channel into a JMS Message as follows: + +[source,properties] +---- +mp.messaging.outgoing.prices.connector=smallrye-jms +---- + +[NOTE] +==== + +You don’t need to set the destination. By default, it uses the channel +name (`prices`). You can configure the `destination` attribute to +override it. + +==== + +[NOTE] +==== + +By default the connector uses a `queue`. You can configure it to use a +`topic` by setting `destination-type=topic`. + +==== + +Then, your application must send `Message++<++Double++>++` to the `prices` +channel. It can use `double` payloads as in the following snippet: + +[source,java] +---- +include::{examples-dir}/jms/outbound/JmsPriceProducer.java[indent=0] +---- + +Or, you can send `Message++<++Double++>++`: + +[source,java] +---- +include::{examples-dir}/jms/outbound/JmsPriceMessageProducer.java[indent=0] +---- + +== Serialization + +The connector serializes the incoming message payload into the body of +the outgoing JMS Message. + +If the payload is a `String` or a primitive type, the payload is encoded +as `String` and the `JMSType` is set to the target class. The +`++_++classname` property is also set. The JMS Message is a `TextMessage`. + +If the payload is a `byte++[]++`, it’s passed as `byte++[]++` in a JMS +`BytesMessage`. + +Otherwise, the payload is encoded using included JSON serializer (JSON-B and Jackson provided OOB. +The `JMSType` is set to the target class. +The `++_++classname` property is also set. +The JMS Message is a `TextMessage`. + +For example, the following code serialize the produced `Person` using +JSON-B. + +[source,java] +---- +@Incoming("...") +@Outgoing("my-channel") +public Person sendToJms(...) { + // ... + return new Person("bob", 42); +} +---- + +It requires that the `Person` class can be serialized to JSON. The +classname is passed in the `JMSType` property and `++_++classname` property. + +== Outbound Metadata + +When sending `Messages`, you can add an instance of link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-jms/{javadoc-version}/io/smallrye/reactive/messaging/jms/OutgoingJmsMessageMetadata.html[`OutgoingJmsMessageMetadata`] +to influence how the message is going to be written to JMS. + +[source,java] +---- +include::{examples-dir}/jms/outbound/JmsOutboundMetadataExample.java[tags=code,indent=0] +---- + +The metadata allow adding properties but also override the destination. + +== Acknowledgement + +Once the JMS message is sent to the JMS server, the message is +acknowledged. Sending a JMS message is a blocking operation. So, sending +is done on a worker thread. + +== Configuration Reference + +include::{generated-dir}/smallrye-jms-outgoing.adoc[indent=0] diff --git a/content/docs/smallrye-reactive-messaging/latest/kafka/avro-configuration.adoc b/content/docs/smallrye-reactive-messaging/latest/kafka/avro-configuration.adoc new file mode 100644 index 000000000..a714f9cb9 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/kafka/avro-configuration.adoc @@ -0,0 +1,152 @@ += Using Apache Avro serializer/deserializer +:version-root: ../ +include::{version-root}_attributes.adoc[] + +If you are using https://avro.apache.org/[Apache Avro] +serializer/deserializer, please note the following configuration +properties. + +== For https://docs.confluent.io/current/schema-registry/serdes-develop/serdes-avro.html[Confluent] Schema Registry + +Confluent Avro library is `io.confluent:kafka-avro-serializer`. Note +that this library is not available in Maven Central, you need to use the +https://docs.confluent.io/clients-kafka-java/current/overview.html[Confluent Maven +repository]. + +=== Consumer + +[width="100%",cols="25%,75%",options="header",] +|=== +|Property |Recommended value +|value.deserializer |io.confluent.kafka.serializers.KafkaAvroDeserializer +|schema.registry.url |http://%7Byour_host%7D:%7Byour_port%7D/ +|specific.avro.reader |true +|=== + +Example: + +.... +mp.messaging.incoming.[channel].value.deserializer=io.confluent.kafka.serializers.KafkaAvroDeserializer +mp.messaging.incoming.[channel].schema.registry.url=http://{your_host}:{your_port}/ +mp.messaging.incoming.[channel].specific.avro.reader=true +.... + +=== Producer + +[width="100%",cols="24%,76%",options="header",] +|=== +|Property |Recommended value +|value.serializer |io.confluent.kafka.serializers.KafkaAvroSerializer +|schema.registry.url |http://%7Byour_host%7D:%7Byour_port%7D/ +|=== + +Example: + +.... +mp.messaging.outgoing.[channel].value.serializer=io.confluent.kafka.serializers.KafkaAvroSerializer +mp.messaging.outgoing.[channel].schema.registry.url=http://{your_host}:{your_port}/ +.... + +== For https://www.apicur.io/registry/[Apicurio] Registry 1.x + +Apicurio Registry 1.x Avro library is +`io.apicurio:apicurio-registry-utils-serde`. + +The configuration properties listed here are meant to be used with the +Apicurio Registry 1.x client library and Apicurio Registry 1.x server. + +=== Consumer + +[width="100%",cols="38%,62%",options="header",] +|=== +|Property |Recommended value +|value.deserializer |io.apicurio.registry.utils.serde.AvroKafkaDeserializer +|apicurio.registry.url |http://%7Byour_host%7D:%7Byour_port%7D/api +|apicurio.registry.avro-datum-provider |io.apicurio.registry.utils.serde.avro.DefaultAvroDatumProvider +|apicurio.registry.use-specific-avro-reader |true +|=== + +Example: + +.... +mp.messaging.incoming.[channel].value.deserializer=io.apicurio.registry.utils.serde.AvroKafkaDeserializer +mp.messaging.incoming.[channel].apicurio.registry.url=http://{your_host}:{your_port}/api +mp.messaging.incoming.[channel].apicurio.registry.avro-datum-provider=io.apicurio.registry.utils.serde.avro.DefaultAvroDatumProvider +mp.messaging.incoming.[channel].apicurio.registry.use-specific-avro-reader=true +.... + +=== Producer + +[width="100%",cols="24%,76%",options="header",] +|=== +|Property |Recommended value +|value.serializer |io.apicurio.registry.utils.serde.AvroKafkaSerializer +|apicurio.registry.url |http://%7Byour_host%7D:%7Byour_port%7D/api +|=== + +To automatically register schemas with the registry, add: + +[width="100%",cols="31%,69%",options="header",] +|=== +|Property |Value +|apicurio.registry.global-id |io.apicurio.registry.utils.serde.strategy.GetOrCreateIdStrategy +|=== + +Example: + +.... +mp.messaging.outgoing.[channel].value.serializer=io.apicurio.registry.utils.serde.AvroKafkaSerializer +mp.messaging.outgoing.[channel].apicurio.registry.url=http://{your_host}:{your_port}/api +mp.messaging.outgoing.[channel].apicurio.registry.global-id=io.apicurio.registry.utils.serde.strategy.GetOrCreateIdStrategy +.... + +== For https://www.apicur.io/registry/[Apicurio] Registry 2.x + +Apicurio Registry 2.x Avro library is +`io.apicurio:apicurio-registry-serdes-avro-serde`. + +The configuration properties listed here are meant to be used with the +Apicurio Registry 2.x client library and Apicurio Registry 2.x server. + +=== Consumer + +[width="100%",cols="31%,69%",options="header",] +|=== +|Property |Recommended value +|value.deserializer |io.apicurio.registry.serde.avro.AvroKafkaDeserializer +|apicurio.registry.url |http://%7Byour_host%7D:%7Byour_port%7D/apis/registry/v2 +|apicurio.registry.use-specific-avro-reader |true +|=== + +Example: + +.... +mp.messaging.incoming.[channel].value.deserializer=io.apicurio.registry.serde.avro.AvroKafkaDeserializer +mp.messaging.incoming.[channel].apicurio.registry.url=http://{your_host}:{your_port}/apis/registry/v2 +mp.messaging.incoming.[channel].apicurio.registry.use-specific-avro-reader=true +.... + +=== Producer + +[width="100%",cols="19%,81%",options="header",] +|=== +|Property |Recommended value +|value.serializer |io.apicurio.registry.serde.avro.AvroKafkaSerializer +|apicurio.registry.url |http://%7Byour_host%7D:%7Byour_port%7D/apis/registry/v2 +|=== + +To automatically register schemas with the registry, add: + +[cols=",",options="header",] +|=== +|Property |Value +|apicurio.registry.auto-register |true +|=== + +Example: + +.... +mp.messaging.outgoing.[channel].value.serializer=io.apicurio.registry.serde.avro.AvroKafkaSerializer +mp.messaging.outgoing.[channel].apicurio.registry.url=http://{your_host}:{your_port}/apis/registry/v2 +mp.messaging.outgoing.[channel].apicurio.registry.auto-register=true +.... diff --git a/content/docs/smallrye-reactive-messaging/latest/kafka/client-service.adoc b/content/docs/smallrye-reactive-messaging/latest/kafka/client-service.adoc new file mode 100644 index 000000000..2487db3de --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/kafka/client-service.adoc @@ -0,0 +1,39 @@ += KafkaClientService +:version-root: ../ +include::{version-root}_attributes.adoc[] + +For advanced use cases, SmallRye Reactive Messaging provides a bean of +type `KafkaClientService` that you can inject: + +[source,java] +---- +@Inject +KafkaClientService kafka; +---- + +From there, you can obtain an +`io.smallrye.reactive.messaging.kafka.KafkaProducer` and an +`io.smallrye.reactive.messaging.kafka.KafkaConsumer`. + +`KafkaProducer` and `KafkaConsumer` expose a non-blocking API on top of +the Kafka client API. They also mediate access to the threads that +SmallRye Reactive Messaging uses to run all Kafka operations: the +_polling thread_, used for consuming records from Kafka topics, and the +_sending thread_, used for producing records to Kafka topics. (Just to +be clear: each _channel_ has its own polling thread and sending thread.) + +The reason why SmallRye Reactive Messaging uses a special thread to run +the poll loop should be obvious: the `Consumer` API is blocking. The +`Producer` API, on the other hand, is documented to be non-blocking. +However, in present versions, Kafka doesn’t guarantee that in all cases; +see https://issues.apache.org/jira/browse/KAFKA-3539[KAFKA-3539] for +more details. That is why SmallRye Reactive Messaging uses a dedicated +thread to run the send operations as well. + +Sometimes, SmallRye Reactive Messaging provides direct access to the +Kafka `Producer` or `Consumer`. For example, a +xref:kafka/consumer-rebalance-listener.adoc[`KafkaConsumerRebalanceListener`] +methods are always invoked on the polling thread, so they give you +direct access to `Consumer`. In such case, you should use the +`Producer`/`Consumer` API directly, instead of the +`KafkaProducer`/`KafkaConsumer` API. diff --git a/content/docs/smallrye-reactive-messaging/latest/kafka/consumer-rebalance-listener.adoc b/content/docs/smallrye-reactive-messaging/latest/kafka/consumer-rebalance-listener.adoc new file mode 100644 index 000000000..fbbdd605f --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/kafka/consumer-rebalance-listener.adoc @@ -0,0 +1,67 @@ += Consumer Rebalance Listener +:version-root: ../ +include::{version-root}_attributes.adoc[] + +To handle offset commit and assigned partitions yourself, you can +provide a consumer rebalance listener. To achieve this, implement the +`io.smallrye.reactive.messaging.kafka.KafkaConsumerRebalanceListener` +interface, make the implementing class a bean, and add the `@Identifier` +qualifier. A usual use case is to store offset in a separate data store +to implement exactly-once semantic, or starting the processing at a +specific offset. + +The listener is invoked every time the consumer topic/partition +assignment changes. For example, when the application starts, it invokes +the `partitionsAssigned` callback with the initial set of +topics/partitions associated with the consumer. If, later, this set +changes, it calls the `partitionsRevoked` and `partitionsAssigned` +callbacks again, so you can implement custom logic. + +Note that the rebalance listener methods are called from the Kafka +_polling_ thread and must block the caller thread until completion. +That’s because the rebalance protocol has synchronization barriers, and +using asynchronous code in a rebalance listener may be executed after +the synchronization barrier. + +When topics/partitions are assigned or revoked from a consumer, it +pauses the message delivery and restarts once the rebalance completes. + +If the rebalance listener handles offset commit on behalf of the user +(using the `ignore` commit strategy), the rebalance listener *must* +commit the offset synchronously in the `partitionsRevoked` callback. We +also recommend applying the same logic when the application stops. + +Unlike the `ConsumerRebalanceListener` from Apache Kafka, the +`io.smallrye.reactive.messaging.kafka.KafkaConsumerRebalanceListener` +methods pass the Kafka `Consumer` and the set of topics/partitions. + +== Example + +In this example we set-up a consumer that always starts on messages from +at most 10 minutes ago (or offset 0). First we need to provide a bean +that implements the +`io.smallrye.reactive.messaging.kafka.KafkaConsumerRebalanceListener` +interface and is annotated with `@Identifier`. We then must configure +our inbound connector to use this named bean. + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaRebalancedConsumerRebalanceListener.java[indent=0] +---- + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaRebalancedConsumer.java[indent=0] +---- + +To configure the inbound connector to use the provided listener we +either set the consumer rebalance listener’s name: + +* `mp.messaging.incoming.rebalanced-example.consumer-rebalance-listener.name=rebalanced-example.rebalancer` + +Or have the listener’s name be the same as the group id: + +* `mp.messaging.incoming.rebalanced-example.group.id=rebalanced-example.rebalancer` + +Setting the consumer rebalance listener’s name takes precedence over +using the group id. diff --git a/content/docs/smallrye-reactive-messaging/latest/kafka/default-configuration.adoc b/content/docs/smallrye-reactive-messaging/latest/kafka/default-configuration.adoc new file mode 100644 index 000000000..a8da3024e --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/kafka/default-configuration.adoc @@ -0,0 +1,80 @@ += Retrieving Kafka default configuration +:version-root: ../ +include::{version-root}_attributes.adoc[] + +If your application/runtime exposes as a CDI _bean_ of type +`Map++<++String, Object` with the identifier `default-kafka-broker`, this +configuration is used to establish the connection with the Kafka broker. + +For example, you can imagine exposing this map as follows: + +[source,java] +---- +@Produces +@ApplicationScoped +@Identifier("default-kafka-broker") +public Map createKafkaRuntimeConfig() { + Map properties = new HashMap<>(); + + StreamSupport + .stream(config.getPropertyNames().spliterator(), false) + .map(String::toLowerCase) + .filter(name -> name.startsWith("kafka")) + .distinct() + .sorted() + .forEach(name -> { + final String key = name.substring("kafka".length() + 1).toLowerCase().replaceAll("[^a-z0-9.]", "."); + final String value = config.getOptionalValue(name, String.class).orElse(""); + properties.put(key, value); + }); + + return properties; +} +---- + +This previous example would extract all the configuration keys from +MicroProfile Config starting with `kafka`. + +[NOTE] +.Quarkus +==== + +Starting with Quarkus 1.5, a map corresponding to the previous example +is automatically provided. + +==== + +In addition to this default configuration, you can configure the name of +the `Map` producer using the `kafka-configuration` attribute: + +[source,properties] +---- +mp.messaging.incoming.my-channel.connector=smallrye-kafka +mp.messaging.incoming.my-channel.kafka-configuration=my-configuration +---- + +In this case, the connector looks for the `Map` associated with the +`my-configuration` name. If `kafka-configuration` is not set, an +optional lookup for a `Map` exposed with the channel name (`my-channel` +in the previous example) is done. + +[IMPORTANT] +==== + +If `kafka-configuration` is set and no `Map` can be found, the +deployment fails. + +==== + +Attribute values are resolved as follows: + +[arabic] +. if the attribute is set directly on the channel configuration +(`mp.messaging.incoming.my-channel.attribute=value`), this value is +used +. if the attribute is not set on the channel, the connector looks for +a `Map` with the channel name or the configured +`kafka-configuration` (if set) and the value is retrieved from that +`Map` +. If the resolved `Map` does not contain the value the default `Map` +is used (exposed with the `default-kafka-broker` name) diff --git a/content/docs/smallrye-reactive-messaging/latest/kafka/health.adoc b/content/docs/smallrye-reactive-messaging/latest/kafka/health.adoc new file mode 100644 index 000000000..2b233b117 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/kafka/health.adoc @@ -0,0 +1,107 @@ += Health reporting +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The Kafka connector reports the startup, readiness and liveness of each channel managed by the connector. + +[NOTE] +==== + +To disable health reporting, set the `health-enabled` attribute for the +channel to `false`. + +==== + +== Startup & Readiness + +=== Metrics-based strategy + +By default, both inbound and outbound channels use underlying Kafka client metrics to check if at least one active connection exists with a broker. +This strategy is lightweight and does not require additional remote interactions with the broker. + +=== Client-based strategy + +You can also enable another strategy by setting the `health-topic-verification-enabled` attribute to `true`. +With this second strategy, the health checks use a Kafka Admin Client to access the broker and retrieve the list of existing topics. +Retrieving this list can be a lengthy and expensive operation. +You can configure a timeout using the `health-topic-verification-timeout` attribute. +The default timeout is set to 2 seconds. +Note that if the timeout is reached, the health check fails. + +[WARNING] +.Deprecated +==== + +`health-readiness-topic-verification` and `health-readiness-timeout` attributes are deprecated and replaced by `health-topic-verification-enabled` and `health-topic-verification-timeout`. + +==== + +For *startup* checks both _inbound_ and _outbound_ side verify that the Kafka topic is created and its partitions are available in the broker. +If multiple topics are consumed using the `topics` attribute, the readiness check verifies that all the consumed topics are available. +If you use a pattern (using the `pattern` attribute), the readiness check verifies that at least one existing topic matches the pattern. + +For *readiness* checks inbound channels verify that the underlying consumer is assigned at least a partition to consume. +On the outbound side (writing records to Kafka) verify that the broker is still accessible. + +[NOTE] +==== + +If `health-topic-verification-enabled` is enabled, both for startup and readiness checks use this strategy. They can be disabled explicitly using `health-topic-verification-startup-disabled` and `health-topic-verification-readiness-disabled` flags. + +==== + +==== Admin Client Pooling + +By default, the client-based strategy creates a dedicated Kafka Admin Client per channel, using client IDs that include the channel name (e.g. `kafka-admin-incoming-prices`, `kafka-admin-outgoing-orders`). +This makes it easy to identify each client in monitoring, logs, and ACLs. + +If your application has many channels sharing the same Kafka broker configuration, you can opt in to admin client pooling to reduce the number of connections: + +[source,properties] +---- +smallrye.messaging.kafka.admin-client.pooling.enabled=true +---- + +When pooling is enabled, channels with identical broker configurations share a single Admin Client instance, using counter-based client IDs (`smallrye-kafka-admin-1`, `smallrye-kafka-admin-2`, ...). + +To summarize for startup and readiness health checks: + +*Startup* + +[width="100%",cols="12%,50%,38%",options="header",] +|=== +| |Inbound |Outbound +|Metrics-based |`connection-count` metric ++>++ 0 or + + no subscribers to the stream |`connection-count` metric ++>++ 0 +|Client-based |Subscribed topic(s) exist in the broker and are available |Produced topic exist in the broker and is available +|=== + +*Readiness* + +[width="100%",cols="11%,56%,33%",options="header",] +|=== +| |Inbound |Outbound +|Metrics-based |`connection-count` metric ++>++ 0 or + + no subscribers to the stream |`connection-count` metric ++>++ 0 +|Client-based |Consumer has at least one partition assignment + + no subscribers to the stream |Cluster is acessible using the Kafka admin API +|=== + +== Liveness + +On the inbound side (receiving records from Kafka), the liveness check +verifies that: + +* no failures have been caught +* the client is connected to the broker + +On the outbound side (writing records to Kafka), the liveness check +verifies that: + +* no failures have been caught + +Note that a message processing failures _nacks_ the message which is +then handled by the failure-strategy. It the responsibility of the +failure-strategy to report the failure and influence the outcome of the +liveness checks. The `fail` failure strategy reports the failure and so +the liveness check will report the failure. diff --git a/content/docs/smallrye-reactive-messaging/latest/kafka/kafka.adoc b/content/docs/smallrye-reactive-messaging/latest/kafka/kafka.adoc new file mode 100644 index 000000000..dbe86b6d0 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/kafka/kafka.adoc @@ -0,0 +1,47 @@ += Apache Kafka Connector +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The Kafka connector adds support for Kafka to Reactive Messaging. With +it you can receive Kafka Records as well as write `message` into Kafka. + +https://kafka.apache.org/[Apache Kafka] is a popular distributed +streaming platform. It lets you: + +* Publish and subscribe to streams of records, similar to a message +queue or enterprise messaging system. +* Store streams of records in a fault-tolerant durable way. +* Process streams of records as they occur. + +The Kafka cluster stores streams of _records_ in categories called +_topics_. Each record consists of a _key_, a _value_, and a _timestamp_. + +For more details about Kafka, check the +https://kafka.apache.org/intro[documentation]. + +== Using the Kafka Connector + +To use the Kafka Connector, add the following dependency to your +project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.reactive + smallrye-reactive-messaging-kafka + {project-version} + +---- + +The connector name is: `smallrye-kafka`. + +So, to indicate that a channel is managed by this connector you need: + +[source,properties] +---- +# Inbound +mp.messaging.incoming.[channel-name].connector=smallrye-kafka + +# Outbound +mp.messaging.outgoing.[channel-name].connector=smallrye-kafka +---- diff --git a/content/docs/smallrye-reactive-messaging/latest/kafka/kerberos.adoc b/content/docs/smallrye-reactive-messaging/latest/kafka/kerberos.adoc new file mode 100644 index 000000000..873617497 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/kafka/kerberos.adoc @@ -0,0 +1,22 @@ += Kerberos authentication +:version-root: ../ +include::{version-root}_attributes.adoc[] + +When using https://en.wikipedia.org/wiki/Kerberos_(protocol)[Kerberos] +authentication, you need to configure the connector with: + +* the security protocol set to `SASL++_++PLAINTEXT` +* the SASL mechanism set to `GSSAPI` +* the Jaas config configured with `Krb5LoginModule` +* the Kerberos service name + +The following snippet provides an example: + +[source,properties] +---- +kafka.bootstrap.servers=ip-192-168-0-207.us-east-2.compute.internal:9094 +kafka.sasl.mechanism=GSSAPI +kafka.security.protocol=SASL_PLAINTEXT +kafka.sasl.jaas.config=com.sun.security.auth.module.Krb5LoginModule required doNotPrompt=true refreshKrb5Config=true useKeyTab=true storeKey=true keyTab="file:/opt/kafka/krb5/kafka-producer.keytab" principal="kafka-producer/ip-192-168-0-207.us-east-2.compute.internal@INTERNAL"; +kafka.sasl.kerberos.service.name=kafka +---- diff --git a/content/docs/smallrye-reactive-messaging/latest/kafka/protobuf-configuration.adoc b/content/docs/smallrye-reactive-messaging/latest/kafka/protobuf-configuration.adoc new file mode 100644 index 000000000..36e025d1e --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/kafka/protobuf-configuration.adoc @@ -0,0 +1,51 @@ += Using Google Protobuf serializer/deserializer +:version-root: ../ +include::{version-root}_attributes.adoc[] + +If you are using https://developers.google.com/protocol-buffers/[Protocol +Buffers] +serializer/deserializer, please note the following configuration +properties. + +== For https://docs.confluent.io/platform/current/schema-registry/serdes-develop/serdes-protobuf.html#serdes-and-formatter-protobuf[Confluent] Schema Registry + +Confluent protobuf library is `io.confluent:kafka-protobuf-serializer`. +Note that this library is not available in Maven Central, you need to +use the https://docs.confluent.io/clients-kafka-java/current/overview.html[Confluent Maven +repository]. + +=== Consumer + +[width="100%",cols="49%,51%",options="header",] +|=== +|Property |Recommended value +|value.deserializer |io.confluent.kafka.serializers.protobuf.KafkaProtobufDeserializer +|schema.registry.url |http://%7Byour_host%7D:%7Byour_port%7D/ +|mp.messaging.incoming.++[++channel++]++.specific.protobuf.value.type |your.package.DomainObjectKey$Key +|mp.messaging.incoming.++[++channel++]++.specific.protobuf.key.type |your.package.DomainObjectValue$Value +|=== + +Example: + +.... +mp.messaging.incoming.[channel].value.deserializer=io.confluent.kafka.serializers.protobuf.KafkaProtobufDeserializer +mp.messaging.incoming.[channel].schema.registry.url=http://{your_host}:{your_port}/ +mp.messaging.incoming.[channel].specific.protobuf.value.type=your.package.DomainObjectKey$Key +mp.messaging.incoming.[channel].specific.protobuf.key.type=your.package.DomainObjectValue$Value +.... + +=== Producer + +[width="100%",cols="24%,76%",options="header",] +|=== +|Property |Recommended value +|value.serializer |io.confluent.kafka.serializers.protobuf.KafkaProtobufSerializer +|schema.registry.url |http://%7Byour_host%7D:%7Byour_port%7D/ +|=== + +Example: + +.... +mp.messaging.outgoing.[channel].value.serializer=io.confluent.kafka.serializers.protobuf.KafkaProtobufSerializer +mp.messaging.outgoing.[channel].schema.registry.url=http://{your_host}:{your_port}/ +.... diff --git a/content/docs/smallrye-reactive-messaging/latest/kafka/receiving-kafka-records.adoc b/content/docs/smallrye-reactive-messaging/latest/kafka/receiving-kafka-records.adoc new file mode 100644 index 000000000..bb16937d6 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/kafka/receiving-kafka-records.adoc @@ -0,0 +1,1057 @@ += Receiving Kafka Records +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The Kafka Connector retrieves Kafka Records from Kafka Brokers and maps +each of them to Reactive Messaging `Messages`. + +== Example + +Let’s imagine you have a Kafka broker running, and accessible using the +`kafka:9092` address (by default it would use `localhost:9092`). +Configure your application to receive Kafka records from a Kafka _topic_ +on the `prices` channel as follows: + +[source,properties] +---- +kafka.bootstrap.servers=kafka:9092 # <1> + +mp.messaging.incoming.prices.connector=smallrye-kafka # <2> +mp.messaging.incoming.prices.value.deserializer=org.apache.kafka.common.serialization.DoubleDeserializer # <3> +mp.messaging.incoming.prices.broadcast=true # <4> +---- + +[arabic] +. Configure the broker location. You can configure it globally or per +channel +. Configure the connector to manage the `prices` channel +. Sets the (Kafka) deserializer to read the record’s value +. Make sure that we can receive from more than one consumer (see +`KafkaPriceConsumer` and `KafkaPriceMessageConsumer` below) + +[NOTE] +==== + +You don’t need to set the Kafka topic. By default, it uses the channel +name (`prices`). You can configure the `topic` attribute to override it. + +==== + +Then, your application receives `Message++<++Double++>++`. You can consume the +payload directly: + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaPriceConsumer.java[indent=0] +---- + +Or, you can retrieve the `Message++<++Double++>++`: + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaPriceMessageConsumer.java[indent=0] +---- + +== Deserialization + +The deserialization is handled by the underlying Kafka Client. You need +to configure the: + +* `mp.messaging.incoming.++[++channel-name++]++.value.deserializer` to +configure the value deserializer (mandatory) +* `mp.messaging.incoming.++[++channel-name++]++.key.deserializer` to configure +the key deserializer (optional, default to `String`) + +If you want to use a custom deserializer, add it to your `CLASSPATH` and +configure the associate attribute. + +In addition, the Kafka Connector also provides a set of _message +converters_. So you can receive _payloads_ representing records from +Kafka using: + +* link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-kafka/{javadoc-version}/io/smallrye/reactive/messaging/kafka/Record.html[`Record`] - a pair key/value +* https://kafka.apache.org/26/javadoc/index.html?org/apache/kafka/clients/consumer/ConsumerRecord.html[ConsumerRecord++<++K,V++>++] - +a structure representing the record with all its metadata + +[source,java] +---- +include::{examples-dir}/kafka/inbound/Converters.java[tags=code,indent=0] +---- + +== Inbound Metadata + +Messages coming from Kafka contains an instance of +link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-kafka/{javadoc-version}/io/smallrye/reactive/messaging/kafka/api/IncomingKafkaRecordMetadata.html[`IncomingKafkaRecordMetadata`] +in the metadata. It provides the key, topic, partitions, +headers and so on: + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaMetadataExample.java[tags=code,indent=0] +---- + +== Acknowledgement + +When a message produced from a Kafka record is acknowledged, the +connector invokes a _commit strategy_. These strategies decide when the +consumer offset for a specific topic/partition is committed. Committing +an offset indicates that all previous records have been processed. It is +also the position where the application would restart the processing +after a crash recovery or a restart. + +Committing every offset has performance penalties as Kafka offset +management can be slow. However, not committing the offset often enough +may lead to message duplication if the application crashes between two +commits. + +The Kafka connector supports three strategies: + +* `throttled` keeps track of received messages and commit to the next +offset after the latest _acked_ message in sequence. This strategy +guarantees _at-least-once delivery_ even if the channel performs +asynchronous processing. The connector tracks the received records +and periodically (period specified by `auto.commit.interval.ms` +(default: 5000)) commits the highest consecutive offset. The +connector will be marked as unhealthy if a message associated with a +record is not acknowledged in +`throttled.unprocessed-record-max-age.ms` (default: 60000). Indeed, +this strategy cannot commit the offset as soon as a single record +processing fails (see failure-strategy to configure what happens on +failing processing). If `throttled.unprocessed-record-max-age.ms` is +set to less than or equal to 0, it does not perform any health check +verification. Such a setting might lead to running out of memory if +there are poison pill messages. This strategy is the default if +`enable.auto.commit` is not explicitly set to `true`. ++ +The `throttled` strategy supports concurrent processing with ordering +guarantees using the `throttled.ordered` configuration. See +xref:#concurrent-processing-with-ordering-guarantees[Concurrent Processing with Ordering Guarantees] +for more details. +* `checkpoint` allows persisting consumer offsets on a "state store", +instead of committing them back to the Kafka broker. Using the +`CheckpointMetadata` API, consumer code can persist a processing +state with the offset to mark the progress of a consumer. +When the processing continues from a previously persisted offset, +it seeks the Kafka consumer to that offset and also restores the +persisted state, continuing the stateful processing from where it +left off. The `checkpoint` strategy holds locally the processing +state associated with the latest offset, and persists it +periodically to the state store (period specified by +`auto.commit.interval.ms` (default: 5000)). The connector will be +marked as unhealthy if no processing state is persisted to the state +store in `checkpoint.unsynced-state-max-age.ms` (default: 10000). +Using the `CheckpointMetadata` API the user code can force to persist +the state on message ack. If `checkpoint.unsynced-state-max-age.ms` +is set to less than or equal to 0, it does not perform any health +check verification. For more information, see +xref:#stateful-processing-with-checkpointing[Stateful processing with Checkpointing] +* `latest` commits the record offset received by the Kafka consumer as +soon as the associated message is acknowledged (if the offset is +higher than the previously committed offset). This strategy provides +_at-least-once_ delivery if the channel processes the message +without performing any asynchronous processing. This strategy should +not be used on high-load as offset commit is expensive. However, it +reduces the risk of duplicates. +* `ignore` performs no commit. This strategy is the default strategy +when the consumer is explicitly configured with `enable.auto.commit` +to `true`. It delegates the offset commit to the Kafka client. When +`enable.auto.commit` is `true` this strategy *DOES NOT* guarantee +at-least-once delivery. However, if the processing failed between +two commits, messages received after the commit and before the +failure will be re-processed. + +[IMPORTANT] +==== + +The Kafka connector disables the Kafka _auto commit_ if not explicitly +enabled. This behavior differs from the traditional Kafka consumer. + +==== + +If high-throughout is important for you, and not limited by the +downstream, we recommend to either: + +* Use the `throttled` policy +* or set `enable.auto.commit` to `true` and annotate the consuming +method with `@Acknowledgment(Acknowledgment.Strategy.NONE)` + +== Concurrent Processing with Ordering Guarantees + +[WARNING] +.Experimental +==== + +Ordered concurrent processing is experimental, and APIs and features are subject to change. + +==== + +When processing Kafka messages concurrently (using `@Blocking(ordered = false)`), +you can configure ordering guarantees using the `ordered` attribute. + +=== Choosing a Commit Strategy + +* *`ordered=key`*: Messages from the same partition but with different keys can be acknowledged +out of order. Use `commit-strategy=throttled`, which tracks out-of-order acknowledgements and +commits offsets correctly. +* *`ordered=partition`*: Messages within a partition are processed sequentially, so +acknowledgements are always in order. This works with any commit strategy, including +`commit-strategy=ignore` for exactly-once transactional processing. + +=== Processing Order Modes + +* *`unordered`* (default): Messages are processed without any ordering guarantees, +allowing maximum concurrency across all partitions and keys. +* *`key`*: Messages with the same key from the same topic-partition are +processed sequentially, while messages with different keys or from different +partitions can be processed concurrently. This ensures per-key ordering within +each partition. +* *`partition`*: Messages from the same topic-partition are processed +sequentially, but messages from different partitions can be processed concurrently. +This ensures partition-level ordering while still allowing parallelism across +partitions. + +=== Examples + +Configure the `ordered` property and use the `@Blocking(ordered = false)` annotation +to enable concurrent processing with ordering guarantees: + +[source,properties] +---- +mp.messaging.incoming.orders.connector=smallrye-kafka +mp.messaging.incoming.orders.topic=orders +mp.messaging.incoming.orders.commit-strategy=throttled +mp.messaging.incoming.orders.ordered=key + +# Configure worker pool concurrency for thread pool size +smallrye.messaging.worker.order-pool.max-concurrency=10 + +# Configure merge concurrency (how many groups can process concurrently) +# Should typically match or be less than worker pool max-concurrency +mp.messaging.incoming.orders.ordered.max-concurrency=10 +---- + +[source,java] +---- +@Incoming("orders") +@Blocking(ordered = false, value = "order-pool") +public void processOrder(Order order) { + // Orders with the same key are processed sequentially + // Orders with different keys can be processed concurrently +} +---- + +For exactly-once transactional processing, use `ordered=partition` with `commit-strategy=ignore` +to avoid dual offset commits: + +[source,properties] +---- +mp.messaging.incoming.data.connector=smallrye-kafka +mp.messaging.incoming.data.topic=data +mp.messaging.incoming.data.commit-strategy=ignore +mp.messaging.incoming.data.ordered=partition +---- + +In order to allow creating more groups (of keys or partitions) that can be processed concurrently, +each group buffers ordered messages to process. +The `max-queue-size-factor` (default: 2) attribute controls the buffer size per group, calculated as +`max.poll.records ++*++ max-queue-size-factor`. +The `ordered.max-concurrency` attribute controls how many groups can be processed concurrently. +If not set, it defaults to `max.poll.records`. +When buffers of all groups are full, backpressure is applied to pause consumption. + +[NOTE] +.Deprecation +==== + +The `throttled.ordered` and `throttled.ordered.max-concurrency` attributes are deprecated. +Use `ordered` and `ordered.max-concurrency` instead, which work with any commit strategy. + +==== + +== Failure Management + +If a message produced from a Kafka record is _nacked_, a failure +strategy is applied. The Kafka connector supports 3 strategies: + +* `fail` - fail the application, no more records will be processed. +(default) The offset of the record that has not been processed +correctly is not committed. +* `ignore` - the failure is logged, but the processing continue. The +offset of the record that has not been processed correctly is +committed. +* `dead-letter-queue` - the offset of the record that has not been +processed correctly is committed, but the record is written to a +(Kafka) _dead letter queue_ topic. +* `delayed-retry-topic` - the offset of the record that has not been +processed correctly is still committed, but the record is written to +a series of Kafka topics for retrying the processing with some delay. +This allows retrying failed records by reconsuming them later without +blocking the processing of the latest records. + +The strategy is selected using the `failure-strategy` attribute. + +=== Dead Letter Queue + +In the case of `dead-letter-queue`, you can configure the following +attributes: + +* `dead-letter-queue.topic`: the topic to use to write the records not +processed correctly, default is `dead-letter-topic-$channel`, with +`$channel` being the name of the channel. +* `dead-letter-queue.producer-client-id`: the client id used by the kafka +producer when sending records to dead letter queue topic. If not specified +it will default to `kafka-dead-letter-topic-producer-$client-id`, with $client-id +being the value obtained from consumer client id. +* `dead-letter-queue.key.serializer`: the serializer used to write the +record key on the dead letter queue. By default, it deduces the +serializer from the key deserializer. +* `dead-letter-queue.value.serializer`: the serializer used to write +the record value on the dead letter queue. By default, it deduces +the serializer from the value deserializer. + +The record written on the dead letter topic contains the original +record’s headers, as well as a set of additional headers about the +original record: + +* `dead-letter-reason` - the reason of the failure (the `Throwable` +passed to `nack()`) +* `dead-letter-cause` - the cause of the failure (the `getCause()` of +the `Throwable` passed to `nack()`), if any +* `dead-letter-topic` - the original topic of the record +* `dead-letter-partition` - the original partition of the record +(integer mapped to String) +* `dead-letter-offset` - the original offset of the record (long +mapped to String) + +When using `dead-letter-queue`, it is also possible to change some +metadata of the record that is sent to the dead letter topic. To do +that, use the `Message.nack(Throwable, Metadata)` method: + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaDeadLetterExample.java[tags=code,indent=0] +---- + +The `Metadata` may contain an instance of `OutgoingKafkaRecordMetadata`. +If the instance is present, the following properties will be used: + +* key; if not present, the original record’s key will be used +* topic; if not present, the configured dead letter topic will be used +* partition; if not present, partition will be assigned automatically +* headers; combined with the original record’s headers, as well as the +`dead-letter-++*++` headers described above + +=== Delayed Retry Topic + +[NOTE] +==== + +Delayed retry topic feature is experimental. + +==== + +The delayed retry topic strategy allows failed records to be automatically retried by forwarding them to a series of retry topics. +Each retry topic is associated with a specific delay time, which is expressed in milliseconds. +When a record processing fails, it is forwarded to the first retry topic. +The failure strategy then consumes these records and dispatches them to be retried again once the delay time of the topic has elapsed. + +If the processing of a record fails again, the message is forwarded to the next topic in the list, with possibly a longer delay time. +If the processing of a record keeps failing, it will eventually be abandoned. +Alternatively, if the `dead-letter-queue.topic` property is configured, the record will be sent to the dead letter queue. + +The Kafka producer client used when forwarding records to retry topics can be configured using the _dead-letter-queue_ properties +namely, `dead-letter-queue.producer-client-id`, `dead-letter-queue.key.serializer` and `dead-letter-queue.value.serializer`. + +Delayed retry topics and delays can be configured with following attributes: + +* `delayed-retry-topic.topics` : +The comma-separated list of retry topics, each one suffixed with `++_[++DELAY++_++IN++_++MILLISECONDS++]++` for indicating the delay time. +For example, `my++_++retry++_++topic++_++2000,my++_++retry++_++topic++_++4000,my++_++retry++_++topic++_++10000` will use three topics +_my++_++retry++_++topic++_++2000_, _my++_++retry++_++topic++_++4000_ and _my++_++retry++_++topic++_++10000_, with 2000ms 4000ms and 10000ms respectively. ++ +If not configured the source channel name is used, with 10, 20 and 50 seconds of delay, +ex. for a channel named `source`, retry topics will be `source++_++retry++_++10000`, `source++_++retry++_++20000`, `source++_++retry++_++50000`. +* `delayed-retry-topic.max-retries` : +The maximum number of retries before abandoning the retries. +If configured higher than the number of retry topics the last topic is used until maximum number of retries is reached. +This can be configured to use a single retry topic with a fixed delay and multiple retries. ++ +For example, `delayed-retry-topic.topics=source++_++retry++_++10000` and `delayed-retry-topic.max-retries=4` will forward failed records +to the topic _source++_++retry++_++10000_ with maximum of 4 retries. +* `delayed-retry-topic.timeout` : +The global timeout in milliseconds for a retried record. +The timeout is calculated from the first failure for a record. +If the next retry will reach the timeout, instead of forwarding to the retry topic the retry is abandoned and, +if configured, the record is forwarded to the dead letter queue. ++ +The default is 120 seconds. + +[IMPORTANT] +==== + +While you can use xref:#retrying-processing[Smallrye Fault Tolerance to retry processing], +it will block the processing of further messages until the retried record is processed successfully, or abandoned. + +Delayed retry topic failure strategy allows effectively implementing non-blocking retries. +But it will not preserve the order of messages inside a topic-partition. + +==== + +The record written on the delayed retry topics will preserve the key and partition of the original record. +It also contains the original record’s headers, as well as a set of additional headers about the original record: + +* `delayed-retry-count` the current number of retries +* `delayed-retry-original-timestamp` the original timestamp of the record +* `delayed-retry-first-processing-timestamp` the first processing timestamp of the record +* `delayed-retry-reason` the reason of the failure (the `Throwable` passed to `nack()`) +* `delayed-retry-cause` the cause of the failure (the `getCause()` of the `Throwable` passed to `nack()`), if any +* `delayed-retry-topic` the original topic of the record +* `delayed-retry-partition` the original partition of the record +* `delayed-retry-offset` the original offset of the record +* `delayed-retry-exception-class-name` the class name of the throwable passed to `nack()` +* `delayed-retry-cause-class-name` the class name of the the `getCause()` of the `Throwable` passed to `nack()`, if any + +As for the dead letter queue it is possible to change forwarded values by providing a `OutgoingKafkaRecordMetadata` +when the message is nacked using `Message.nack(Throwable, Metadata)`. + +[NOTE] +.Multiple partitions +==== + +The delayed retry topic strategy does not create retry topics automatically. +If the source topic has multiple partitions, delayed retry and dead letter queue topics would need to be setup with the same number of partitions. + +It is possible to scale consumer application instances according to the number of partitions. +But it is not guaranteed that the retry topics consumer will be assigned the same partition(s) as the main topic consumer. +Therefore, retry processing of a record can happen in an other instance. + +==== + +== Custom commit and failure strategies + +In addition to provided strategies, it is possible to implement custom +commit and failure strategies and configure Kafka channels with them. + +For example, for a custom commit strategy, implement the +link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-kafka/{javadoc-version}/io/smallrye/reactive/messaging/kafka/commit/KafkaCommitHandler.html[`KafkaCommitHandler`] interface, +and provide a managed bean implementing the `KafkaCommitHandler.Factory` interface, +identified using `@Identifier` qualifier. + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaCustomCommit.java[indent=0] +---- + +Finally, to use the custom commit strategy, +set the `commit-strategy` attribute to the identifier of the commit handler factory: +`mp.messaging.incoming.$channel.commit-strategy=custom`. +Similarly, custom failure strategies can be configured using `failure-strategy` attribute. + +[NOTE] +==== + +If the custom strategy implementation inherits +link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-kafka/{javadoc-version}/io/smallrye/reactive/messaging/kafka/commit/ContextHolder.html[`ContextHolder`] class it can access the +Vert.x event-loop context created for the Kafka consumer + +==== + +== Retrying processing + +You can combine Reactive Messaging with https://github.com/smallrye/smallrye-fault-tolerance[SmallRye Fault +Tolerance], and +retry processing when it fails: + +[source,java] +---- +@Incoming("kafka") +@Outgoing("processed") +@Retry(delay = 10, maxRetries = 5) +public String process(String v) { + // ... retry if this method throws an exception +} +---- + +You can configure the delay, the number of retries, the jitter... + +If your method returns a `Uni`, you need to add the `@NonBlocking` +annotation: + +[source,java] +---- +@Incoming("kafka") +@Outgoing("processed") +@Retry(delay = 10, maxRetries = 5) +@NonBlocking +public Uni process(String v) { + // ... retry if this method throws an exception or the returned Uni produce a failure +} +---- + +The incoming messages are acknowledged only once the processing +completes successfully. So, it commits the offset after the successful +processing. If after the retries the processing still failed, the +message is _nacked_ and the failure strategy is applied. + +You can also use `@Retry` on methods only consuming incoming messages: + +[source,java] +---- +@Incoming("kafka") +@Retry(delay = 10, maxRetries = 5) +public void consume(String v) { + // ... retry if this method throws an exception +} +---- + +== Handling deserialization failures + +Because deserialization happens before creating a `Message`, the failure +strategy presented above cannot be applied. However, when a +deserialization failure occurs, you can intercept it and provide a +fallback value. To achieve this, create a CDI bean implementing the +link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-kafka/{javadoc-version}/io/smallrye/reactive/messaging/kafka/api/DeserializationFailureHandler.html[`DeserializationFailureHandler`] +interface: + +[source,java] +---- +@ApplicationScoped +@Identifier("failure-retry") // Set the name of the failure handler +public class MyDeserializationFailureHandler + implements DeserializationFailureHandler { // Specify the expected type + + @Override + public JsonObject decorateDeserialization(Uni deserialization, + String topic, boolean isKey, String deserializer, byte[] data, + Headers headers) { + return deserialization + .onFailure().retry().atMost(3) + .await().atMost(Duration.ofMillis(200)); + } +} +---- + +The bean must be exposed with the `@Identifier` qualifier specifying the +name of the bean. Then, in the connector configuration, specify the +following attribute: + +* `mp.messaging.incoming.$channel.key-deserialization-failure-handler`: +name of the bean handling deserialization failures happening for the +record’s key +* `mp.messaging.incoming.$channel.value-deserialization-failure-handler`: +name of the bean handling deserialization failures happening for the +record’s value, + +The handler is called with the deserialization action as a `Uni++<++T++>++`, the +record’s topic, a boolean indicating whether the failure happened on a +key, the class name of the deserializer that throws the exception, the +corrupted data, the exception, and the records headers augmented with +headers describing the failure (which ease the write to a dead letter). +On the deserialization `Uni` failure strategies like retry, providing a +fallback value or applying timeout can be implemented. Note that the +method must await on the result and return the deserialized object. +Alternatively, the handler can only implement +`handleDeserializationFailure` method and provide a fallback value, +which may be `null`. + +If you don’t configure a deserialization failure handlers and a +deserialization failure happens, the application is marked unhealthy. +You can also ignore the failure, which will log the exception and +produce a `null` value. To enable this behavior, set the +`mp.messaging.incoming.$channel.fail-on-deserialization-failure` +attribute to `false`. + +If the `fail-on-deserialization-failure` attribute is set to `false` and +the `failure-strategy` attribute is `dead-letter-queue` the failed record +will be sent to the corresponding _dead letter queue_ topic. +The forwarded record will have the original key and value, +and the following headers set: + +* `deserialization-failure-reason`: The deserialization failure message +* `deserialization-failure-cause`: The deserialization failure cause if any +* `deserialization-failure-key`: Whether the deserialization failure happened on a key +* `deserialization-failure-topic`: The topic of the incoming message when a deserialization failure happen +* `deserialization-failure-deserializer`: The class name of the underlying deserializer +* `deserialization-failure-key-data`: If applicable the key data that was not able to be deserialized +* `deserialization-failure-value-data`: If applicable the value data that was not able to be deserialized + +== Receiving Cloud Events + +The Kafka connector supports https://cloudevents.io/[Cloud Events]. +When the connector detects a _structured_ or _binary_ Cloud Events, it +adds a link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-kafka/{javadoc-version}/io/smallrye/reactive/messaging/kafka/IncomingKafkaCloudEventMetadata.html[`IncomingKafkaCloudEventMetadata`] in the metadata of the +Message. `IncomingKafkaCloudEventMetadata` contains the various (mandatory and optional) Cloud Event attributes. + +If the connector cannot extract the Cloud Event metadata, it sends the +Message without the metadata. + +=== Binary Cloud Events + +For `binary` Cloud Events, *all* mandatory Cloud Event attributes must +be set in the record header, prefixed by `ce++_++` (as mandated by the +https://github.com/cloudevents/spec/blob/v1.0/kafka-protocol-binding.md[protocol +binding]). +The connector considers headers starting with the `ce++_++` prefix but not +listed in the specification as extensions. You can access them using the +`getExtension` method from `IncomingKafkaCloudEventMetadata`. You can +retrieve them as `String`. + +The `datacontenttype` attribute is mapped to the `content-type` header +of the record. The `partitionkey` attribute is mapped to the record’s +key, if any. + +Note that all headers are read as UTF-8. + +With binary Cloud Events, the record’s key and value can use any +deserializer. + +=== Structured Cloud Events + +For `structured` Cloud Events, the event is encoded in the record’s +value. Only JSON is supported, so your event must be encoded as JSON in +the record’s value. + +Structured Cloud Event must set the `content-type` header of the record +to `application/cloudevents` or prefix the value with +`application/cloudevents` such as: +`application/cloudevents{plus}json; charset=UTF-8`. + +To receive structured Cloud Events, your value deserializer must be: + +* `org.apache.kafka.common.serialization.StringDeserializer` +* `org.apache.kafka.common.serialization.ByteArrayDeserializer` +* `io.vertx.kafka.client.serialization.JsonObjectDeserializer` + +As mentioned previously, the value must be a valid JSON object +containing at least all the mandatory Cloud Events attributes. + +If the record is a structured Cloud Event, the created Message’s payload +is the Cloud Event `data`. + +The `partitionkey` attribute is mapped to the record’s key if any. + +== Consumer Rebalance Listener + +To handle offset commit and assigned partitions yourself, you can +provide a consumer rebalance listener. To achieve this, implement the +`io.smallrye.reactive.messaging.kafka.KafkaConsumerRebalanceListener` +interface, make the implementing class a bean, and add the `@Identifier` +qualifier. A usual use case is to store offset in a separate data store +to implement exactly-once semantic, or starting the processing at a +specific offset. + +The listener is invoked every time the consumer topic/partition +assignment changes. For example, when the application starts, it invokes +the `partitionsAssigned` callback with the initial set of +topics/partitions associated with the consumer. If, later, this set +changes, it calls the `partitionsRevoked` and `partitionsAssigned` +callbacks again, so you can implement custom logic. + +Note that the rebalance listener methods are called from the Kafka +_polling_ thread and must block the caller thread until completion. +That’s because the rebalance protocol has synchronization barriers, and +using asynchronous code in a rebalance listener may be executed after +the synchronization barrier. + +When topics/partitions are assigned or revoked from a consumer, it +pauses the message delivery and restarts once the rebalance completes. + +If the rebalance listener handles offset commit on behalf of the user +(using the `ignore` commit strategy), the rebalance listener *must* +commit the offset synchronously in the `partitionsRevoked` callback. We +also recommend applying the same logic when the application stops. + +Unlike the `ConsumerRebalanceListener` from Apache Kafka, the +`io.smallrye.reactive.messaging.kafka.KafkaConsumerRebalanceListener` +methods pass the Kafka `Consumer` and the set of topics/partitions. + +=== Example + +In this example we set-up a consumer that always starts on messages from +at most 10 minutes ago (or offset 0). First we need to provide a bean +that implements the +`io.smallrye.reactive.messaging.kafka.KafkaConsumerRebalanceListener` +interface and is annotated with `@Identifier`. We then must configure +our inbound connector to use this named bean. + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaRebalancedConsumerRebalanceListener.java[indent=0] +---- + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaRebalancedConsumer.java[indent=0] +---- + +To configure the inbound connector to use the provided listener we +either set the consumer rebalance listener’s name: + +* `mp.messaging.incoming.rebalanced-example.consumer-rebalance-listener.name=rebalanced-example.rebalancer` + +Or have the listener’s name be the same as the group id: + +* `mp.messaging.incoming.rebalanced-example.group.id=rebalanced-example.rebalancer` + +Setting the consumer rebalance listener’s name takes precedence over +using the group id. + +== Receiving Kafka Records in Batches + +By default, incoming methods receive each Kafka record individually. +Under the hood, Kafka consumer clients poll the broker constantly and +receive records in batches, presented inside the `ConsumerRecords` +container. + +In *batch* mode, your application can receive all the records returned +by the consumer *poll* in one go. + +To achieve this you need to set +`mp.messaging.incoming.$channel.batch=true` *and* specify a compatible +container type to receive all the data: + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaRecordBatchPayloadExample.java[tags=code,indent=0] +---- + +The incoming method can also receive `Message++<++List++<++Payload++>++` or `ConsumerRecords++<++Key, Payload++>++` types, They +give access to record details such as offset or timestamp : + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaRecordBatchExample.java[tags=code,indent=0] +---- + +Note that the successful processing of the incoming record batch will +commit the latest offsets for each partition received inside the batch. +The configured commit strategy will be applied for these records only. + +Conversely, if the processing throws an exception, all messages are +_nacked_, applying the failure strategy for all the records inside the +batch. + +=== Accessing metadata of batch records + +When receiving records in batch mode, the metadata of each record is accessible through the `IncomingKafkaRecordBatchMetadata` : + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaRecordBatchExample.java[tags=batch,indent=0] +---- + +Like in this example, this can be useful to propagate the tracing information of each record. + +== Manual topic-partition assignment + +The default behavior of Kafka incoming channels is to subscribe to one or more topics in order to receive records from the Kafka broker. +Channel attributes `topic` and `topics` allow specifying topics to subscribe to, +or `pattern` attribute allows to subscribe to all topics matching a regular expression. +Subscribing to topics allows partitioning consumption of topics by dynamically assigning (rebalancing) partitions between members of a consumer group. + +The `assign-seek` configuration attribute allows manually assigning topic-partitions to a Kafka incoming channel, +and optionally seek to a specified offset in the partition to start consuming records. +If `assign-seek` is used, the consumer will not be dynamically subscribed to topics, +but instead will statically assign the described partitions. +In manual topic-partition rebalancing doesn't happen and therefore rebalance listeners are never called. + +The attribute takes a list of triplets separated by commas: `++<++topic++>++:++<++partition++>++:++<++offset++>++`. + +For example, the following configuration + +[source,properties] +---- +mp.messaging.incoming.data.assign-seek=topic1:0:10, topic2:1:20 +---- + +assigns the consumer to: +- Partition 0 of topic 'topic1', setting the initial position at offset 10. +- Partition 1 of topic 'topic2', setting the initial position at offset 20. + +The topic, partition, and offset in each triplet can have the following variations: +- If the topic is omitted, the configured `topic` will be used. +- If the offset is omitted, partitions are assigned to the consumer but won't be seeked to offset. +- If offset is 0, it seeks to the beginning of the topic-partition. +- If offset is -1, it seeks to the end of the topic-partition. + +== Stateful processing with Checkpointing + +[CAUTION] +.Deprecated +==== + +Checkpointing is deprecated and will be removed in a future version. + +==== + +The `checkpoint` commit strategy allows for a Kafka incoming channel to +manage topic-partition offsets, not by committing on the Kafka broker, +but by persisting consumers' advancement on a +xref:#implementing-state-stores[_state store_]. + +In addition to that, if the consumer builds an internal state as +a result of consumed records, the topic-partition offset persisted +to the state store can be associated with a _processing state_, +saving the local state to the persistent store. When a consumer +restarts or consumer group instances scale, i.e. when new partitions +get assigned to the consumer, the checkpointing works by resuming the +processing from the latest offset and its saved state. + +The `@Incoming` channel consumer code can manipulate the processing +state through the `CheckpointMetadata` API: + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaCheckpointExample.java[tags=code,indent=0] +---- + +The `transform` method allows applying a transformation function to +the current state, producing a changed state and registering it +locally for checkpointing. By default, the local state is synced +(persisted) to the state store periodically, period specified by +`auto.commit.interval.ms`, (default: 5000). If `persistOnAck` flag +is given, the latest state is persisted to the state store eagerly +on message acknowledgment. The `setNext` method works similarly +directly setting the latest state. + +The `checkpoint` commit strategy tracks when a processing state +is last persisted for each topic-partition. If an outstanding state +change can not be persisted for `checkpoint.unsynced-state-max-age.ms` +(default: 10000), the channel is marked unhealthy. + +Where and how processing states are persisted is decided by the +state store implementation. This can be configured on the incoming +channel using `checkpoint.state-store` configuration property, +using the state store factory identifier name. +The serialization of state objects depends on the state store +implementation. In order to instruct state stores for serialization +can require configuring the type name of state objects +using `checkpoint.state-type` property. + +In order to keep Smallrye Reactive Messaging free of persistence-related +dependencies, this library includes only a default state store named `file`. +It is based on Vert.x Filesystem API and stores the processing state +in Json formatted files, in a local directory configured by the +`checkpoint.file.state-dir` property. State files follow the naming +scheme `++[++consumer-group-id++]++:++[++topic++]++:++[++partition++]++`. + +=== Implementing State Stores + +State store implementations are required to implement `CheckpointStateStore` +interface, and provide a managed bean implementing +`CheckpointStateStore.Factory`, identified with `@Identifier` bean +qualifier indicating the name of the state-store. +The factory bean identifier indicates the name to configure on +`checkpoint.state-store` config property. +The factory is discovered as a CDI managed bean and state store is +created once per channel: + +[source,java] +---- +include::{examples-dir}/kafka/inbound/MyCheckpointStateStore.java[indent=0] +---- + +The checkpoint commit strategy calls the state store in following events: + +* `fetchProcessingState` : on partitions assigned, to seek the consumer to the latest offset. +* `persistProcessingState` on partitions revoked, to persist the state of last processed record. +* `persistProcessingState` on message acknowledgement, if a new state is set during the processing and `persistOnAck` flag is set. +* `persistProcessingState` on `auto.commit.interval.ms` intervals, if a new state is set during processing. +* `persistProcessingState` on channel shutdown. +* `close` on channel shutdown. + +== Receiving Kafka Records Using Kafka Queues (Share Groups) + +[WARNING] +.Experimental +==== + +Kafka Queues (Share Groups) support is experimental. + +==== + +https://cwiki.apache.org/confluence/display/KAFKA/KIP-932[Kafka Share Groups] provide a new consumption model where records from a topic are cooperatively distributed across consumers without explicit partition assignment. +Unlike traditional consumer groups, share groups do not require managing offsets or partition rebalancing -- the broker handles record distribution and acquisition locks automatically. + +This is useful for *queue-like* workloads where you want records to be processed by exactly one consumer in a group, without worrying about partition count or consumer coordination. + +=== Enabling Share Groups + +To enable share group consumption on a channel, set the `share-group.enabled` attribute to `true`: + +[source,properties] +---- +mp.messaging.incoming.queue.connector=smallrye-kafka +mp.messaging.incoming.queue.topic=prices +mp.messaging.incoming.queue.value.deserializer=org.apache.kafka.common.serialization.DoubleDeserializer +mp.messaging.incoming.queue.share-group.enabled=true # <1> +---- + +[arabic] +. Enables share group consumption for this channel. + +Then consume records as usual: + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaShareGroupConsumer.java[indent=0] +---- + +By default, successfully processed records are acknowledged with `ACCEPT`. +If the processing method throws an exception, the record is released (`RELEASED`) and will be attempted for re-delivery. + +=== Controlling Acknowledgement + +Share groups support four acknowledgement types for each record: + +* *`ACCEPT`* Record processed successfully (default on ack). +* *`RELEASE`* Record could not be processed; release it for re-delivery to another consumer (default on nack). +* *`REJECT`* Record is permanently rejected and will not be re-delivered. +* *`RENEW`* Extend the acquisition lock for long-running processing (managed automatically when processing timeout is enabled). + +You can control the acknowledgement type using the `ShareGroupAcknowledgement` metadata, which is injected as a parameter in the consuming method: + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaShareGroupAckExample.java[tags=code,indent=0] +---- + +When the message is acked (or method completes normally), the connector uses the acknowledgement type set on `ShareGroupAcknowledgement`. +If no type is explicitly set, the default is `ACCEPT`. + +When calling `nack()` on the `Message`, you can pass the desired acknowledgement type via metadata: + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaShareGroupNackExample.java[tags=code,indent=0] +---- + +If no metadata is provided on nack, the default acknowledgement type is `RELEASE`. +This can be changed using the `share-group.failure-acknowledgement-type` attribute: + +[source,properties] +---- +mp.messaging.incoming.queue.share-group.enabled=true +mp.messaging.incoming.queue.share-group.failure-acknowledgement-type=reject # <1> +---- + +[arabic] +. Nacked records are permanently rejected instead of released for re-delivery. + +=== Handling Deserialization Failures + +When `fail-on-deserialization-failure` is set to `false`, records that fail deserialization are not forwarded to the consumer method. +They are automatically nacked using the acknowledgement type configured by `share-group.failure-deserialization-acknowledgement-type` (default: `reject`). + +[source,properties] +---- +mp.messaging.incoming.queue.share-group.enabled=true +mp.messaging.incoming.queue.fail-on-deserialization-failure=false # <1> +mp.messaging.incoming.queue.share-group.failure-deserialization-acknowledgement-type=reject # <2> +---- + +[arabic] +. Deserialization failures do not kill the application. +. Records that fail deserialization are permanently rejected (default). + +A custom `DeserializationFailureHandler` can override the acknowledgement type per record by setting the `deserialization-failure-share-ack-type` header +to the set `AcknowledgeType` value (e.g., `RELEASE`, `REJECT`, `ACCEPT`). + +=== Acquisition Lock Renewal and Processing Timeout + +Each record acquired by a share group consumer has a time-limited *acquisition lock* (default 30 seconds, configured on the broker via `group.share.record.lock.duration.ms`). +If the consumer does not acknowledge the record before the lock expires, the broker automatically releases it for re-delivery. + +The connector can periodically send `RENEW` acknowledgements to extend the lock for records still being processed. +This is controlled by the `share-group.unprocessed-record-max-age.ms` attribute: + +* When set to a value greater than `0` (default: `60000`), the connector enables *processing timeout monitoring*: +** Records still in progress are periodically renewed to prevent lock expiry. +** If a record exceeds the configured timeout, the connector reports a failure and marks the channel as unhealthy. +* When set to `0`, processing timeout monitoring is disabled. No `RENEW` acknowledgements are sent. If processing exceeds the broker's lock duration, the record will be automatically released and re-delivered. + +[source,properties] +---- +mp.messaging.incoming.queue.share-group.enabled=true +mp.messaging.incoming.queue.share-group.unprocessed-record-max-age.ms=30000 # <1> +---- + +[arabic] +. Records not processed within 30 seconds trigger a failure report. + +=== Batch Mode + +Share groups support batch consumption. Set `batch=true` to receive all records from a single poll as a batch: + +[source,properties] +---- +mp.messaging.incoming.queue.share-group.enabled=true +mp.messaging.incoming.queue.batch=true +---- + +When a batch is acknowledged, all individual records in the batch are acknowledged. +When a batch is nacked, all records are nacked with the specified acknowledgement type. + +You can control the acknowledgement type per record within a batch using the `ShareGroupAcknowledgement` metadata accessible through `IncomingKafkaRecordBatchMetadata`: + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaShareGroupBatchAckExample.java[tags=code,indent=0] +---- + +Each record in the batch has its own `ShareGroupAcknowledgement` instance. +When the batch is acked, each record is individually acknowledged using its configured type. +This allows accepting some records while rejecting or releasing others within the same batch. + +=== Incompatible Configuration + +The following configuration attributes are not supported with share groups and will be ignored: + +* `pattern` Share groups subscribe to explicit topics only. +* `assign-seek` Share groups do not support manual partition assignment. +* `partitions` Share groups handle record distribution automatically; multiple consumer instances are not needed. +* `commit-strategy` Share groups use their own acknowledgement mechanism. +* `failure-strategy` Share groups use their own failure handling based on `AcknowledgeType`. +* `consumer-rebalance-listener.name` Share groups do not have traditional partition rebalancing. + +If any of these are configured alongside `share-group.enabled=true`, a warning is logged at startup. + +== Configuration Reference + +include::{generated-dir}/smallrye-kafka-incoming.adoc[indent=0] + +You can also pass any property supported by the underlying https://kafka.apache.org/documentation/#consumerconfigs[Kafka +consumer]. + +For example, to configure the `max.poll.records` property, use: + +[source,properties] +---- +mp.messaging.incoming.[channel].max.poll.records=1000 +---- + +Some consumer client properties are configured to sensible default +values: + +If not set, `reconnect.backoff.max.ms` is set to `10000` to avoid high +load on disconnection. + +If not set, `key.deserializer` is set to +`org.apache.kafka.common.serialization.StringDeserializer`. + +The consumer `client.id` is configured according to the number of +clients to create. + +[WARNING] +.Deprecated +==== + +The `partitions` property (`mp.messaging.incoming.++[++channel++]++.partitions`) and the associated `requests` property are *deprecated*. +Use the `concurrency` channel attribute instead, which creates distinct indexed channels with their own consumer. + +==== + +* If a `client.id` is provided, it is used as-is or suffixed with +client index if the deprecated `partitions` property is set. +* If a `client.id` is not provided, it is generated as +`kafka-consumer-++[++channel++][++-index++]++`. diff --git a/content/docs/smallrye-reactive-messaging/latest/kafka/request-reply.adoc b/content/docs/smallrye-reactive-messaging/latest/kafka/request-reply.adoc new file mode 100644 index 000000000..4aedd2cc9 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/kafka/request-reply.adoc @@ -0,0 +1,175 @@ += Kafka Request/Reply +:version-root: ../ +include::{version-root}_attributes.adoc[] + +[WARNING] +.Tech Preview +==== + +Kafka Request Reply Emitter is a tech preview feature. + +==== + +The Kafka https://www.enterpriseintegrationpatterns.com/patterns/messaging/RequestReply.html[Request-Reply] pattern allows you to publish a message to a Kafka topic and then await for a reply message that responds to the initial request. + +The `KafkaRequestReply` emitter implements the requestor (or the client) of the request-reply pattern for Kafka outbound channels: + +[source,java] +---- +include::{examples-dir}/kafka/outbound/KafkaRequestReplyEmitter.java[indent=0] +---- + +The `request` method publishes the request record to the configured target topic of the outgoing channel, +and polls a reply topic (by default, the target topic with `-replies` suffix) for a reply record. +When the reply is received the returned `Uni` is completed with the record value. + +The request send operation generates a correlation id and sets a header (by default `REPLY++_++CORRELATION++_++ID`), +which it expects to be sent back in the reply record. Two additional headers are set on the request record: + +* The topic from which the reply is expected, by default `REPLY++_++TOPIC` header, +which can be configured using the `reply.topic` channel attribute. +* Optionally, the partition from which the reply is expected, by default `REPLY++_++PARTITION` header. +The reply partition header is added only when the Kafka request reply is configured specifically to receive records from a topic-partition +, using the `reply.partition` channel attribute. +The reply partition header integer value is encoded in 4 bytes, +and helper methods `KafkaRequestReply#replyPartitionFromBytes` and `KafkaRequestReply#replyPartitionToBytes` can be used for custom operations. + +The replier (or the server) can be implemented using a Reactive Messaging processor: + +[source,java] +---- +include::{examples-dir}/kafka/outbound/KafkaReplier.java[indent=0] +---- + +Kafka outgoing channels detect default `REPLY++_++CORRELATION++_++ID`, `REPLY++_++TOPIC` and `REPLY++_++PARTITION` headers +and send the reply record to the expected topic-partition by propagating back the correlation id header. + +Default headers can be configured, using `reply.correlation-id.header`, `reply.topic.header` and `reply.partition.header` channel attributes. +If custom headers are used the reply server needs some more manual work. +Given the following request/reply outgoing configuration: + +[source,properties] +---- +mp.messaging.outgoing.reqrep.topic=requests +mp.messaging.outgoing.reqrep.reply.correlation-id.header=MY_CORRELATION +mp.messaging.outgoing.reqrep.reply.topic.header=MY_TOPIC +mp.messaging.outgoing.reqrep.reply.partition.header=MY_PARTITION +mp.messaging.incoming.request.topic=requests +---- + +The reply server can be implemented as the following: + +[source,java] +---- +include::{examples-dir}/kafka/outbound/KafkaCustomHeaderReplier.java[indent=0] +---- + +== Requesting with `Message` types + +Like the core Emitter's `send` methods, `request` method also can receive a `Message` type and return a message: + +[source,java] +---- +include::{examples-dir}/kafka/outbound/KafkaRequestReplyMessageEmitter.java[indent=0] +---- + +[NOTE] +==== + +The ingested reply type of the `KafkaRequestReply` is discovered at runtime, +in order to configure a `MessageConveter` to be applied on the incoming message before returning the `Uni` result. + +==== + +== Requesting multiple replies + +You can use the `requestMulti` method to expect any number of replies represented by the `Multi` return type. + +For example this can be used to aggregate multiple replies to a single request. + +[source,java] +---- +include::{examples-dir}/kafka/outbound/KafkaRequestReplyMultiEmitter.java[indent=0] +---- + +Like the other `request` you can also request `Message` types. + +[NOTE] +==== + +The channel attribute `reply.timeout` will be applied between each message, if reached the returned `Multi` will +fail. + +==== + +== Scaling Request/Reply + +If multiple requestor instances are configured on the same outgoing topic, and the same reply topic, +each requestor consumer will generate a unique consumer group.id and +therefore all requestor instances will receive replies of all instances. If an observed correlation id doesn't match +the id of any pending replies, the reply is simply discarded. +With the additional network traffic this allows scaling requestors, (and repliers) dynamically. + +Alternatively, requestor instances can be configured to consume replies from dedicated topics using `reply.topic` attribute, +or distinct partitions of a single topic, using `reply.partition` attribute. +The later will configure the Kafka consumer to assign statically to the given partition. + +== Pending replies and reply timeout + +By default, the `Uni` returned from the `request` method is configured to fail with timeout exception if no replies is received after 5 seconds. +This timeout is configurable with the channel attribute `reply.timeout`. + +A snapshot of the list of pending replies is available through the `KafkaRequestReply#getPendingReplies` method. + +== Waiting for topic-partition assignment + +The requestor can be found in a position where a request is sent, and it's reply is already published to the reply topic, +before the requestor starts and polls the consumer. +In case the reply consumer is configured with `auto.offset.reset=latest`, which is the default value, this can lead to the requestor missing replies. + +If `auto.offset.reset` is `latest`, at wiring time, before any request can take place, the `KafkaRequestReply` +finds partitions that the consumer needs to subscribe and waits for their assignment to the consumer. +The timeout of the initial subscription can be adjusted with `reply.initial-assignment-timeout` which defaults to the `reply.timeout`. +If this timeout fails, `KafkaRequestReply` will enter an invalid state which will require it to be restarted. +If set to `-1`, the `KafkaRequestReply` will not wait for the initial assignment of the reply consumer to sent requests. + +On other occasions the `KafkaRequestReply#waitForAssignments` method can be used. + +== Correlation Ids + +The Kafka Request/Reply allows configuring the correlation id mechanism completely through a `CorrelationIdHandler` implementation. +The default handler is based on randomly generated UUID strings, written to byte array in Kafka record headers. +The correlation id handler implementation can be configured using the `reply.correlation-id.handler` attribute. +As mentioned the default configuration is `uuid`, +and an alternative `bytes` implementation can be used to generate 12 bytes random correlation ids. + +Custom handlers can be implemented by proposing a CDI-managed bean with `@Identifier` qualifier. + +== Reply Error Handling + +If the reply server produces an error and can or would like to propagate the error back to the requestor, failing the returned `Uni`. + +If configured using the `reply.failure.handler` channel attribute, +the `ReplyFailureHandler` implementations are discovered through CDI, matching the `@Identifier` qualifier. + +A sample reply error handler can lookup header values and return the error to be thrown by the reply: + +[source,java] +---- +include::{examples-dir}/kafka/outbound/MyReplyFailureHandler.java[indent=0] +---- + +`null` return value indicates that no error has been found in the reply record, and it can be delivered to the application. + +== Advanced configuration for the Kafka consumer + +The underlying Kafka consumer can be configured with the `reply` property prefix. +For example the underlying Kafka consuemer can be configured to batch mode using: + +[source,properties] +---- +mp.messaging.outgoing.reqrep.topic=requests +mp.messaging.outgoing.reqrep.reply.topic=quote-results +mp.messaging.outgoing.reqrep.reply.batch=true +mp.messaging.outgoing.reqrep.reply.commit-strategy=latest +---- diff --git a/content/docs/smallrye-reactive-messaging/latest/kafka/test-companion.adoc b/content/docs/smallrye-reactive-messaging/latest/kafka/test-companion.adoc new file mode 100644 index 000000000..d25aa111f --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/kafka/test-companion.adoc @@ -0,0 +1,158 @@ += Kafka Companion +:version-root: ../ +include::{version-root}_attributes.adoc[] + +[WARNING] +.Experimental +==== + +Kafka Companion is experimental and APIs are subject to change in the future. + +==== + +The *Kafka Companion* is a separate Java library for helping to test Kafka applications. +It is not intended to mock Kafka, but to the contrary, connect to a Kafka broker and provide high-level features. + +It is not limited to the SmallRye Reactive Messaging testing, but intends to improve the testability of applications using Kafka. Some of its features: + +* Running In-container Kafka broker for tests via https://github.com/strimzi/test-container[strimzi-test-container]. +* Running the Kafka broker behind a https://github.com/Shopify/toxiproxy[toxiproxy] for simulating network issues. +* Running embedded Kafka Kraft broker for tests. +* Base classes for tests to bootstrap tests. +* Companion classes for easily creating tasks to produce and consume Kafka records. +* Writing assertions for produce and consume tasks, state of consumers, topics, offsets etc. + +== Getting started writing tests + +Easiest way of starting to write Kafka tests is to extend `KafkaCompanionTestBase`. +It starts a single-node Kafka broker for the test suite using testcontainers and creates the `KafkaCompanion` to connect to this broker: + +[source,java] +---- +include::{examples-dir}/kafka/companion/KafkaWithBaseTest.java[tags=code,indent=0] +---- + +`KafkaCompanion` can be used on its own to connect to a broker: + +[source,java] +---- +include::{examples-dir}/kafka/companion/KafkaTest.java[tags=companion,indent=0] +---- + +There are a couple of things to note on how Kafka companion handles *producers*, *consumers* and *tasks*: + +`ProducerBuilder` and `ConsumerBuilder` lazy descriptions of with which configuration to create a producer or a consumer. + +`ProducerTask` and `ConsumerTask` run asynchronously on dedicated threads and are started as soon as they are created. +A task terminates when it is explicitly stopped, when it's predefined duration or number of records has been produced/consumed, or when it encounters an error. +An exterior thread can await on records processed, or simply on termination of the task. +At a given time produced or consumed records are accessible through the task. + +The actual creation of the producer or consumer happens when a task is started. When the task terminates the producer or the consumer is automatically closed. + +For example, in the previous example: + +[arabic] +. We described a producer with a client id `my-producer` and `max.block.ms` of 5 seconds. +. Then we created a task to produce 100 records using the generator function, without waiting for its completion. +. We then described a consumer with group id `my-group` and which commits offset synchronously on every received record. +. Finally, we created a task to consume records for 10 seconds and await its completion. + +== Producing records + +=== Produce from records + +Produce given records: + +[source,java] +---- +include::{examples-dir}/kafka/companion/ProducerTest.java[tags=records,indent=0] +---- + +=== Produce from generator function + +Produce 10 records using the generator function: + +[source,java] +---- +include::{examples-dir}/kafka/companion/ProducerTest.java[tags=generator,indent=0] +---- + +=== Produce from CSV file + +Given a comma-separated file `records.csv` with the following content + +[source,csv] +---- +messages,0,a,asdf +messages,1,b,asdf +messages,3,c,asdf +---- + +Produce records from the file: + +[source,java] +---- +include::{examples-dir}/kafka/companion/ProducerTest.java[tags=csv,indent=0] +---- + +== Consuming records + +=== Consume from topics + +[source,java] +---- +include::{examples-dir}/kafka/companion/ConsumerTest.java[tags=topics,indent=0] +---- + +=== Consume from offsets + +[source,java] +---- +include::{examples-dir}/kafka/companion/ConsumerTest.java[tags=offsets,indent=0] +---- + +=== Consumer assignment and offsets + +During execution of the consumer task, information about the underlying consumer's topic partition assignment, position or committed offsets can be accessed. + +[source,java] +---- +include::{examples-dir}/kafka/companion/ConsumerTest.java[tags=committed,indent=0] +---- + +== Registering Custom Serdes + +KafkaCompanion handles Serializers and Deserializers for default types such as primitives, String, ByteBuffer, UUID. + +Serdes for custom types can be registered to the companion object, and will be used for producer and consumer tasks: + +[source,java] +---- +include::{examples-dir}/kafka/companion/KafkaTest.java[tags=serdes,indent=0] +---- + +== Topics + +Create, list, describe and delete topics: + +[source,java] +---- +include::{examples-dir}/kafka/companion/TopicsTest.java[tags=code,indent=0] +---- + +== Consumer Groups and Offsets + +List topic partition offsets: + +[source,java] +---- +include::{examples-dir}/kafka/companion/OffsetsTest.java[tags=offsets,indent=0] +---- + +List, describe, alter consumer groups and their offsets: + +[source,java] +---- +include::{examples-dir}/kafka/companion/OffsetsTest.java[tags=consumerGroups,indent=0] +---- diff --git a/content/docs/smallrye-reactive-messaging/latest/kafka/transactions.adoc b/content/docs/smallrye-reactive-messaging/latest/kafka/transactions.adoc new file mode 100644 index 000000000..927012f01 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/kafka/transactions.adoc @@ -0,0 +1,165 @@ += Kafka Transactions and Exactly-Once Processing +:version-root: ../ +include::{version-root}_attributes.adoc[] + +https://cwiki.apache.org/confluence/display/KAFKA/KIP-98+-+Exactly+Once+Delivery+and+Transactional+Messaging[Kafka transactions] enable atomic writes to multiple Kafka topics and partitions. +Inside a transaction, a producer writes records to the Kafka topic partitions as it would normally do. +If the transaction completes successfully, all the records previously written to the broker inside that transaction will be _committed_, and will be readable for consumers. +If an error during the transaction causes it to be _aborted_, none will be readable for consumers. + +There are a couple of fundamental things to consider before using transactions: + +* Each transactional producer is configured with a unique identifier called the `transactional.id`. +This is used to identify the same producer instance across application restarts. +By default, it is not configured, and transactions cannot be used. +When it is configured, the producer is limited to idempotent delivery, therefore `enable.idempotence=true` is implied. +* For *only* reading transactional messages, a consumer needs to explicitly configure its `isolation.level` property to `read++_++committed`. +This will make sure that the consumer will deliver only records committed inside a transaction, and filter out messages from aborted transactions. +* It should also be noted that this does not mean all records atomically written inside a transaction will be read atomically by the consumer. +Transactional producers can write to multiple topics and partitions inside a transaction, but consumers do not know where the transaction starts or ends. +Not only multiple consumers inside a consumer group can share those partitions, +all records which were part of a single transaction can be consumed in different batch of records by a consumer. + +Kafka connector provides `KafkaTransactions` custom emitter for writing records inside a transaction. +Before using a transaction we need to make sure that `transactional.id` is configured on the channel properties. + +.... +mp.messaging.outgoing.tx-out-example.transactional.id=example-tx-producer +.... + +It can be used as a regular emitter `@Channel`: + +[source,java] +---- +include::{examples-dir}/kafka/outbound/KafkaTransactionalProducer.java[indent=0] +---- + +[NOTE] +==== + +When `transactional.id` is provided `KafkaProducer#initTransactions` is called when the underlying Kafka producer is created. + +==== + +The function given to the `withTransaction` method receives a `TransactionalEmitter` for producing records, and returns a `Uni` that provides the result of the transaction. +If the processing completes successfully, the producer is flushed and the transaction is committed. +If the processing throws an exception, returns a failing `Uni`, or marks the `TransactionalEmitter` for abort, the transaction is aborted. + +If this method is called on a Vert.x context, the processing function is also called on that context. +Otherwise, it is called on the sending thread of the producer. + +A synchronous variant `withTransactionAndAwait` is also available for blocking contexts such as REST endpoints or `@Blocking` annotated methods. +It blocks the calling thread until the transaction completes and must not be called on a Vert.x event loop thread. +The transaction completion phase (flush, commit/abort) is bounded by the Kafka producer's `transaction.timeout.ms` setting (default 60 seconds). + +[IMPORTANT] +==== + +By default (non-pooled mode), a transaction is considered _in progress_ from the call to the `withTransaction` until the returned `Uni` results in success or failure. +While a transaction is in progress, subsequent calls to the `withTransaction`, including nested ones inside the given function, will throw `IllegalStateException`. +Note that in Reactive Messaging, the execution of processing methods is already serialized, unless `@Blocking(ordered = false)` is used. +If `withTransaction` can be called concurrently, for example from a REST endpoint, it is recommended to limit the concurrency of the execution. +This can be done using the `@Bulkhead` annotation from Microprofile Fault Tolerance, or by using the xref:#pooled-producer-for-concurrent-transactions[pooled producer mode] which allows concurrent transactions. +In pooled producer mode, multiple transactions can run concurrently, each using a different producer from the pool. + +==== + +== Exactly-Once Processing + +Kafka Transactions API also allows managing consumer offsets inside a transaction, together with produced messages. +This in turn enables coupling a consumer with a transactional producer in a consume-transform-produce pattern, +also known as exactly-once processing. +It means that an application consumes messages from a topic-partition, processes them, publishes the results to a topic-partition, +and commits offsets of the consumed messages in a transaction. + +The `KafkaTransactions` emitter also provides a way to apply exactly-once processing to an incoming Kafka message inside a transaction. + +The following example includes a batch of Kafka records inside a transaction. + +.... +mp.messaging.outgoing.tx-out-example.transactional.id=example-tx-producer +mp.messaging.incoming.in-channel.batch=true +mp.messaging.incoming.in-channel.commit-strategy=ignore +mp.messaging.incoming.in-channel.failure-strategy=ignore +.... + +[source,java] +---- +include::{examples-dir}/kafka/outbound/KafkaExactlyOnceProcessor.java[indent=0] +---- + +If the processing completes successfully, before committing the transaction, the topic partition offsets of the given batch message will be committed to the transaction. + +If the processing needs to abort, after aborting the transaction, the consumer's position is reset to the last committed offset, effectively resuming the consumption from that offset. +If no consumer offset has been committed, the consumer's position is reset to the beginning of the topic, even if the offset reset policy is `latest`. + +[IMPORTANT] +==== + +When using exactly-once processing, consumed message offset commits are handled by the transaction and therefore `commit-strategy` needs to be `ignore`. + +==== + +Any strategy can be employed for the `failure-strategy`. +Note that the `Uni` returned from the `#withTransaction` will yield a failure if the transaction fails and is aborted. + +The application can choose to handle the error case, but for the message consumption to continue, `Uni` returned from the `@Incoming` method must not result in failure. +`KafkaTransactions#withTransactionAndAck` method will ack and nack the message but will not stop the reactive stream. +Ignoring the failure simply resets the consumer to the last committed offsets and resumes the processing from there. + +Synchronous variants `withTransactionAndAwait` are also available for exactly-once processing, +accepting `IncomingKafkaRecordMetadata` or `IncomingKafkaRecordBatchMetadata` for offset management within the transaction. + +[NOTE] +==== + +It is recommended to use exactly-once processing along with the batch consumption mode. +While it is possible to use it with a single Kafka message, it'll have a significant performance impact. + +==== + +== Pooled Producer for Concurrent Transactions + +By default, `KafkaTransactions` uses a single transactional producer and a global lock, meaning only one transaction can be in progress at a time. +This limits throughput when consuming from multiple partitions concurrently. + +The *pooled producer* mode uses a pool of Kafka producers, each with its own `transactional.id`. +Each transaction reserves a producer from the pool for the duration of the transaction, enabling concurrent exactly-once processing. + +To enable pooled producer mode, set `pooled-producer.enabled` to `true` on the outgoing channel. +Combined with incoming channel `batch` and `concurrency`, multiple partitions can be processed in parallel, +each with its own independent transaction: + +[source,properties] +---- +mp.messaging.outgoing.tx-out-example.transactional.id=my-tx-producer +mp.messaging.outgoing.tx-out-example.pooled-producer.enabled=true + +mp.messaging.incoming.in-channel.ordered=partition +mp.messaging.incoming.in-channel.commit-strategy=ignore +mp.messaging.incoming.in-channel.failure-strategy=ignore +---- + +The `KafkaTransactions` API is the same as for regular exactly-once processing. +The outgoing record's partition defaults to the incoming record's partition: + +[source,java] +---- +include::{examples-dir}/kafka/outbound/KafkaPooledExactlyOnceProcessor.java[indent=0] +---- + +On commit or abort, the producer is returned to the pool and can be reused for another transaction. +Only the partitions involved in the transaction are reset on abort, so other concurrent transactions are unaffected. + +The pool grows lazily to match actual concurrency, up to `pooled-producer.max-pool-size` (default 10). +Each inner producer gets a unique `transactional.id` derived from the base value, e.g. `my-tx-producer-1`, `my-tx-producer-2`, etc. +Producers are reused across transactions. + +You can also pre-create producers at startup with `pooled-producer.initial-pool-size` (default 0). +Pre-created producers respect the `lazy-client` setting, so with `lazy-client=false` (default), they eagerly initialize the Kafka producer and call `initTransactions()` at startup. + +[source,properties] +---- +mp.messaging.outgoing.tx-out-example.pooled-producer.max-pool-size=20 +mp.messaging.outgoing.tx-out-example.pooled-producer.initial-pool-size=5 +---- diff --git a/content/docs/smallrye-reactive-messaging/latest/kafka/writing-kafka-records.adoc b/content/docs/smallrye-reactive-messaging/latest/kafka/writing-kafka-records.adoc new file mode 100644 index 000000000..6d27eee5b --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/kafka/writing-kafka-records.adoc @@ -0,0 +1,354 @@ += Writing Kafka Records +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The Kafka Connector can write Reactive Messaging `Messages` as Kafka +Records. + +== Example + +Let’s imagine you have a Kafka broker running, and accessible using the +`kafka:9092` address (by default it would use `localhost:9092`). +Configure your application to write the messages from the `prices` +channel into a Kafka _topic_ as follows: + +[source,properties] +---- +kafka.bootstrap.servers=kafka:9092 # <1> + +mp.messaging.outgoing.prices-out.connector=smallrye-kafka # <2> +mp.messaging.outgoing.prices-out.value.serializer=org.apache.kafka.common.serialization.DoubleSerializer # <3> +mp.messaging.outgoing.prices-out.topic=prices # <4> +---- + +[arabic] +. Configure the broker location. You can configure it globally or per +channel +. Configure the connector to manage the `prices` channel +. Sets the (Kafka) serializer to encode the message payload into the +record’s value +. Make sure the topic name is `prices` (and not the default +`prices-out`) + +Then, your application must send `Message++<++Double++>++` to the `prices` +channel. It can use `double` payloads as in the following snippet: + +[source,java] +---- +include::{examples-dir}/kafka/outbound/KafkaPriceProducer.java[indent=0] +---- + +Or, you can send `Message++<++Double++>++`: + +[source,java] +---- +include::{examples-dir}/kafka/outbound/KafkaPriceMessageProducer.java[indent=0] +---- + +== Serialization + +The serialization is handled by the underlying Kafka Client. You need to +configure the: + +* `mp.messaging.outgoing.++[++channel-name++]++.value.serializer` to configure +the value serializer (mandatory) +* `mp.messaging.outgoing.++[++channel-name++]++.key.serializer` to configure +the key serializer (optional, default to `String`) + +If you want to use a custom serializer, add it to your `CLASSPATH` and +configure the associate attribute. + +By default, the written record contains: + +* the `Message` payload as _value_ +* no key, or the key configured using the `key` attribute or the key +passed in the metadata attached to the `Message` +* the timestamp computed for the system clock (`now`) or the timestamp +passed in the metadata attached to the `Message` + +== Sending key/value pairs + +In the Kafka world, it’s often necessary to send _records_, i.e. a +key/value pair. The connector provides the link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-kafka/{javadoc-version}/io/smallrye/reactive/messaging/kafka/Record.html[`io.smallrye.reactive.messaging.kafka.Record`] +class that you can use to send a pair: + +[source,java] +---- +include::{examples-dir}/kafka/outbound/KafkaRecordExample.java[tags=code,indent=0] +---- + +When the connector receives a message with a `Record` payload, it +extracts the key and value from it. The configured serializers for the +key and the value must be compatible with the record’s key and value. +Note that the `key` and the `value` can be `null`. It is also possible +to create a record with a `null` key AND a `null` value. + +If you need more control on the written records, use +`OutgoingKafkaRecordMetadata`. + +== Outbound Metadata + +When sending `Messages`, you can add an instance of +link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-kafka/{javadoc-version}/io/smallrye/reactive/messaging/kafka/api/OutgoingKafkaRecordMetadata.html[`OutgoingKafkaRecordMetadata`] +to influence how the message is going to be written to Kafka. For example, +you can add Kafka headers, configure the record key… + +[source,java] +---- +include::{examples-dir}/kafka/outbound/KafkaOutboundMetadataExample.java[tags=code,indent=0] +---- + +== Propagating Record Key + +When processing messages, you can propagate incoming record key to the +outgoing record. + +Consider the following example method, which consumes messages from the +channel `in`, transforms the payload, and writes the result to the +channel `out`. + +[source,java] +---- +include::{examples-dir}/kafka/outbound/KafkaPriceProcessor.java[tags=code,indent=0] +---- + +Enabled with +`mp.messaging.outgoing.++[++channel-name++]++.propagate-record-key=true` +configuration, record key propagation produces the outgoing record with +the same _key_ as the incoming record. + +If the outgoing record already contains a _key_, it *won’t be +overridden* by the incoming record key. If the incoming record does +have a _null_ key, the `mp.messaging.outgoing.++[++channel-name++]++.key` +property is used. + +== Propagating Record headers + +You can also propagate incoming record headers to the outgoing record, by specifying the list of headers to be considered. + +`mp.messaging.outgoing.++[++channel-name++]++.propagate-headers=Authorization,Proxy-Authorization` + +If the ougoing record already defines a header with the same key, it won't be overriden by the incoming header. + +== Dynamic topic names + +Sometimes it is desirable to select the destination of a message +dynamically. In this case, you should not configure the topic inside +your application configuration file, but instead, use the outbound +metadata to set the name of the topic. + +For example, you can route to a dynamic topic based on the incoming +message: + +[source,java] +---- +include::{examples-dir}/kafka/outbound/KafkaOutboundDynamicTopicExample.java[tags=code,indent=0] +---- + +== Acknowledgement + +Kafka acknowledgement can take times depending on the configuration. +Also, it stores in-memory the records that cannot be written. + +By default, the connector does wait for Kafka to acknowledge the record +to continue the processing (acknowledging the received `Message`). You +can disable this by setting the `waitForWriteCompletion` attribute to +`false`. + +Note that the `acks` attribute has a huge impact on the record +acknowledgement. + +If a record cannot be written, the message is `nacked`. + +== Back-pressure and inflight records + +The Kafka outbound connector handles back-pressure monitoring the number +of in-flight messages waiting to be written to the Kafka broker. The +number of in-flight messages is configured using the +`max-inflight-messages` attribute and defaults to 1024. + +The connector only sends that amount of messages concurrently. No other +messages will be sent until at least one in-flight message gets +acknowledged by the broker. Then, the connector writes a new message to +Kafka when one of the broker’s in-flight messages get acknowledged. Be +sure to configure Kafka’s `batch.size` and `linger.ms` accordingly. + +You can also remove the limit of inflight messages by setting +`max-inflight-messages` to `0`. However, note that the Kafka Producer +may block if the number of requests reaches +`max.in.flight.requests.per.connection`. + +== Handling serialization failures + +For Kafka producer client serialization failures are not recoverable, +thus the message dispatch is not retried. However, using schema +registries, serialization may intermittently fail to contact the +registry. In these cases you may need to apply a failure strategy for +the serializer. To achieve this, create a CDI bean implementing the link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-kafka/{javadoc-version}/io/smallrye/reactive/messaging/kafka/SerializationFailureHandler.html[`SerializationFailureHandler`] +interface: + +[source,java] +---- +@ApplicationScoped +@Identifier("failure-fallback") // Set the name of the failure handler +public class MySerializationFailureHandler + implements SerializationFailureHandler { // Specify the expected type + + @Override + public byte[] decorateSerialization(Uni serialization, String topic, + boolean isKey, String serializer, + Object data, Headers headers) { + return serialization + .onFailure().retry().atMost(3) + .await().atMost(Duration.ofMillis(200)); + } +} +---- + +The bean must be exposed with the `@Identifier` qualifier specifying the +name of the bean. Then, in the connector configuration, specify the +following attribute: + +* `mp.messaging.incoming.$channel.key-serialization-failure-handler`: +name of the bean handling serialization failures happening for the +record’s key +* `mp.messaging.incoming.$channel.value-serialization-failure-handler`: +name of the bean handling serialization failures happening for the +record’s value, + +The handler is called with the serialization action as a `Uni`, the +record’s topic, a boolean indicating whether the failure happened on a +key, the class name of the deserializer that throws the exception, the +corrupted data, the exception, and the records headers. Failure +strategies like retry, providing a fallback value or applying timeout +can be implemented. Note that the method must await on the result and +return the serialized byte array. Alternatively, the handler can +implement `decorateSerialization` method which can return a fallback +value. + +== Sending Cloud Events + +The Kafka connector supports https://cloudevents.io/[Cloud Events]. +The connector sends the outbound record as Cloud Events if: + +* the message metadata contains an +`io.smallrye.reactive.messaging.ce.OutgoingCloudEventMetadata` +instance, +* the channel configuration defines the `cloud-events-type` and +`cloud-events-source` attributes. + +You can create +`io.smallrye.reactive.messaging.ce.OutgoingCloudEventMetadata` instances +using: + +[source,java] +---- +include::{examples-dir}/kafka/outbound/KafkaCloudEventProcessor.java[indent=0] +---- + +If the metadata does not contain an id, the connector generates one +(random UUID). The `type` and `source` can be configured per message or +at the channel level using the `cloud-events-type` and +`cloud-events-source` attributes. Other attributes are also +configurable. + +The metadata can be contributed by multiple methods, however, you must +always retrieve the already existing metadata to avoid overriding the +values: + +[source,java] +---- +include::{examples-dir}/kafka/outbound/KafkaCloudEventMultipleProcessors.java[indent=0] +---- + +By default, the connector sends the Cloud Events using the _binary_ +format. You can write _structured_ Cloud Events by setting the +`cloud-events-mode` to `structured`. Only JSON is supported, so the +created records had its `content-type` header set to +`application/cloudevents{plus}json; charset=UTF-8` When using the +_structured_ mode, the value serializer must be set to +`org.apache.kafka.common.serialization.StringSerializer`, otherwise the +connector reports the error. In addition, in _structured_, the connector +maps the message’s payload to JSON, except for `String` passed directly. + +The record’s key can be set in the channel configuration (`key` +attribute), in the `OutgoingKafkaRecordMetadata` or using the +`partitionkey` Cloud Event attribute. + +[NOTE] +==== + +you can disable the Cloud Event support by setting the `cloud-events` +attribute to `false` + +==== + +== Using `ProducerRecord` + +Kafka built-in type +https://kafka.apache.org/26/javadoc/index.html?org/apache/kafka/clients/producer/ProducerRecord.html[ProducerRecord++<++K,V++>++] +can also be used for producing messages: + +[source,java] +---- +include::{examples-dir}/kafka/outbound/KafkaProducerRecordExample.java[tags=code,indent=0] +---- + +[WARNING] +==== + +This is an advanced feature. The `ProducerRecord` is sent to Kafka as +is. Any possible metadata attached through `Message++<++ProducerRecord++<++K, V++>>++` are ignored and lost. + +==== + +== Producer Interceptors + +Producer interceptors can be configured for Kafka producer clients using the standard `interceptor.classes` property. +Configured classes will be instantiated by the Kafka producer on client creation. + +Alternatively, producer clients can be configured with a CDI managed-bean implementing link:https://javadoc.io/doc/org.apache.kafka/kafka-clients/{javadoc-version}/org/apache/kafka/clients/producer/ProducerInterceptor.html[`org.apache.kafka.clients.producer.ProducerInterceptor`] interface: + +To achieve this, the bean must be exposed with the `@Identifier` qualifier specifying the name of the bean: + +[source,java] +---- +include::{examples-dir}/kafka/outbound/ProducerInterceptorBeanExample.java[indent=0] +---- + +Then, in the channel configuration, specify the following attribute: +`mp.messaging.incoming.$channel.interceptor-bean=my-producer-interceptor`. + +[WARNING] +==== + +The `onSend` method will be called on the producer _sending thread_ and `onAcknowledgement` will be called on the _Kafka producer I/O thread_. +In both cases if implementations are not fast, sending of messages could be delayed. + +==== + +== Configuration Reference + +include::{generated-dir}/smallrye-kafka-outgoing.adoc[indent=0] + +You can also pass any property supported by the underlying https://kafka.apache.org/documentation/#producerconfigs[Kafka +producer]. + +For example, to configure the `batch.size` property, use: + +[source,properties] +---- +mp.messaging.outgoing.[channel].batch.size=32768 +---- + +Some producer client properties are configured to sensible default +values: + +If not set, `reconnect.backoff.max.ms` is set to `10000` to avoid high +load on disconnection. + +If not set, `key.serializer` is set to +`org.apache.kafka.common.serialization.StringSerializer`. + +If not set, producer `client.id` is generated as +`kafka-producer-++[++channel++]++`. diff --git a/content/docs/smallrye-reactive-messaging/latest/mqtt/client-customization.adoc b/content/docs/smallrye-reactive-messaging/latest/mqtt/client-customization.adoc new file mode 100644 index 000000000..768513593 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/mqtt/client-customization.adoc @@ -0,0 +1,21 @@ += Customizing the underlying MQTT client +:version-root: ../ +include::{version-root}_attributes.adoc[] + +You can customize the underlying MQTT Client configuration by +_producing_ an instance of +`io.smallrye.reactive.messaging.mqtt.session.MqttClientSessionOptions`: + +[source,java] +---- +include::{examples-dir}/mqtt/customization/ClientProducers.java[tags=named,indent=0] +---- + +This instance is retrieved and used to configure the client used by the +connector. You need to indicate the name of the client using the +`client-options-name` attribute: + +[source,properties] +---- +mp.messaging.incoming.prices.client-options-name=my-options +---- diff --git a/content/docs/smallrye-reactive-messaging/latest/mqtt/mqtt.adoc b/content/docs/smallrye-reactive-messaging/latest/mqtt/mqtt.adoc new file mode 100644 index 000000000..afa02d06f --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/mqtt/mqtt.adoc @@ -0,0 +1,44 @@ += MQTT Connector +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The MQTT connector adds support for MQTT to Reactive Messaging. + +It lets you receive messages from an MQTT server or broker as well as +send MQTT messages. The MQTT connector is based on the https://vertx.io/docs/vertx-mqtt/java/#_vert_x_mqtt_client[Vert.x MQTT +Client]. + +== Introduction + +http://mqtt.org/[MQTT] is a machine-to-machine (M2M)/"Internet of +Things" connectivity protocol. It was designed as an extremely +lightweight publish/subscribe messaging transport. + +The MQTT Connector allows consuming messages from MQTT as well as +sending MQTT messages. + +== Using the MQTT connector + +To you the MQTT Connector, add the following dependency to your project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.reactive + smallrye-reactive-messaging-mqtt + {project-version} + +---- + +The connector name is: `smallrye-mqtt`. + +So, to indicate that a channel is managed by this connector you need: + +[source,properties] +---- +# Inbound +mp.messaging.incoming.[channel-name].connector=smallrye-mqtt + +# Outbound +mp.messaging.outgoing.[channel-name].connector=smallrye-mqtt +---- diff --git a/content/docs/smallrye-reactive-messaging/latest/mqtt/receiving-mqtt-messages.adoc b/content/docs/smallrye-reactive-messaging/latest/mqtt/receiving-mqtt-messages.adoc new file mode 100644 index 000000000..5a1e176bb --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/mqtt/receiving-mqtt-messages.adoc @@ -0,0 +1,114 @@ += Receiving messages from MQTT +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The MQTT Connector connects to a MQTT broker or router, and forward the +messages to the Reactive Messaging application. It maps each of them +into Reactive Messaging `Messages`. + +== Example + +Let’s imagine you have a MQTT server/broker running, and accessible +using the `mqtt:1883` address (by default it would use +`localhost:1883`). Configure your application to receive MQTT messages +on the `prices` channel as follows: + +[source,properties] +---- +mp.messaging.incoming.prices.connector=smallrye-mqtt # <1> +mp.messaging.incoming.prices.host=mqtt # <2> +mp.messaging.incoming.prices.port=1883 # <3> +---- + +[arabic] +. Sets the connector for the `prices` channel +. Configure the broker/server host name. +. Configure the broker/server port. 1883 is the default. + +[NOTE] +==== + +You don’t need to set the MQTT topic. By default, it uses the channel +name (`prices`). You can configure the `topic` attribute to override it. + +==== + +[NOTE] +==== + +It is generally recommended to set the `client-id`. By default, the connector is generating a unique `client-id`. + +==== + +[IMPORTANT] +==== + +Message coming from MQTT have a `byte++[]++` payload. + +==== + +Then, your application receives `Message++<++byte++[]>++`. You can consume the +payload directly: + +[source,java] +---- +include::{examples-dir}/mqtt/inbound/MqttPriceConsumer.java[indent=0] +---- + +Or, you can retrieve the `Message++<++byte++[]>++`: + +[source,java] +---- +include::{examples-dir}/mqtt/inbound/MqttPriceMessageConsumer.java[indent=0] +---- + +The inbound topic can use the https://mosquitto.org/man/mqtt-7.html[MQTT +wildcards] (`{plus}` and `#`). + +== Deserialization + +The MQTT Connector does not handle the deserialization and creates a +`Message++<++byte++[]>++`. + +== Inbound Metadata + +The MQTT connector does not provide inbound metadata. + +== Failure Management + +If a message produced from a MQTT message is _nacked_, a failure +strategy is applied. The MQTT connector supports 3 strategies: + +* `fail` - fail the application, no more MQTT messages will be +processed. (default) The offset of the record that has not been +processed correctly is not committed. +* `ignore` - the failure is logged, but the processing continue. + +== Configuration Reference + +include::{generated-dir}/smallrye-mqtt-incoming.adoc[indent=0] + +The MQTT connector is based on the https://vertx.io/docs/vertx-mqtt/java/#_vert_x_mqtt_client[Vert.x MQTT +client]. So +you can pass any attribute supported by this client. + +[IMPORTANT] +==== + +A single instance of `MqttClient` and a single connection is used for +each `host` / `port` / `server-name` / `client-id`. This client is +reused for both the inbound and outbound connectors. + +==== + +[IMPORTANT] +==== + +Using `auto-clean-session=false` the MQTT Connector send Subscribe requests +to the broken only if a Persistent Session is not present (like on the first +connection). This means that if a Session is already present (maybe for a +previous run) and you add a new incoming channel, this will not be subscribed. +Beware to check always the subscription present on Broker when use +`auto-clean-session=false`. + +==== diff --git a/content/docs/smallrye-reactive-messaging/latest/mqtt/sending-messages-to-mqtt.adoc b/content/docs/smallrye-reactive-messaging/latest/mqtt/sending-messages-to-mqtt.adoc new file mode 100644 index 000000000..bc8017414 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/mqtt/sending-messages-to-mqtt.adoc @@ -0,0 +1,94 @@ += Sending messages to MQTT +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The MQTT Connector can write Reactive Messaging `Messages` as MQTT +Message. + +== Example + +Let’s imagine you have a MQTT server/broker running, and accessible +using the `mqtt:1883` address (by default it would use +`localhost:1883`). Configure your application to write the messages from +the `prices` channel into a MQTT Messages as follows: + +[source,properties] +---- +mp.messaging.outgoing.prices.type=smallrye-mqtt +mp.messaging.outgoing.prices.host=mqtt +mp.messaging.outgoing.prices.port=1883 +---- + +[arabic] +. Sets the connector for the `prices` channel +. Configure the broker/server host name. +. Configure the broker/server port. 1883 is the default. + +[NOTE] +==== + +You don’t need to set the MQTT topic. By default, it uses the channel +name (`prices`). You can configure the `topic` attribute to override it. +NOTE: It is generally recommended to set the `client-id`. By default, +the connector is generating a unique `client-id`. + +==== + +Then, your application must send `Message++<++Double++>++` to the `prices` +channel. It can use `double` payloads as in the following snippet: + +[source,java] +---- +include::{examples-dir}/mqtt/outbound/MqttPriceProducer.java[indent=0] +---- + +Or, you can send `Message++<++Double++>++`: + +[source,java] +---- +include::{examples-dir}/mqtt/outbound/MqttPriceMessageProducer.java[indent=0] +---- + +== Serialization + +The `Message` sent to MQTT can have various payload types: + +* https://vertx.io/docs/apidocs/io/vertx/core/json/JsonObject.html[`JsonObject`]: +JSON string encoded as `byte++[]++` +* https://vertx.io/docs/apidocs/io/vertx/core/json/JsonArray.html[`JsonArray`]: +JSON string encoded as `byte++[]++` +* `java.lang.String` and Java primitive types: `toString` encoded as +`byte++[]++` +* `byte++[]++` +* complex objects: The objects are encoded to JSON and passed as +`byte++[]++` + +== Outbound Metadata + +The MQTT connector does not provide outbound metadata. + +== Acknowledgement + +MQTT acknowledgement depends on the QoS level. The message is +acknowledged when the broker indicated the successful reception of the +message (or immediately if the level of QoS does not support +acknowledgment). + +If a MQTT message cannot be sent to the broker, the message is `nacked`. + +== Configuration Reference + +include::{generated-dir}/smallrye-mqtt-outgoing.adoc[indent=0] + +The MQTT connector is based on the https://vertx.io/docs/vertx-mqtt/java/#_vert_x_mqtt_client[Vert.x MQTT +client]. So +you can pass any attribute supported by this client. + +[IMPORTANT] +==== + +A single instance of `MqttClient` and a single connection is used for +each `host` / `port` / `server-name` / `client-id`. This client is +reused for both the inbound and outbound connectors. + +==== diff --git a/content/docs/smallrye-reactive-messaging/latest/pulsar/client-configuration.adoc b/content/docs/smallrye-reactive-messaging/latest/pulsar/client-configuration.adoc new file mode 100644 index 000000000..c0e021963 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/pulsar/client-configuration.adoc @@ -0,0 +1,98 @@ += Configuring Pulsar clients, consumers and producers +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Pulsar clients, consumers and producers are very customizable to configure how a Pulsar client application behaves. + +The Pulsar connector creates a Pulsar client and, a consumer or a producer per channel, each with sensible defaults to ease their configuration. +Although the creation is handled, all available configuration options remain configurable through Pulsar channels. + +While idiomatic way of creating `PulsarClient`, `PulsarConsumer` or `PulsarProducer` are through builder APIs, in its essence +those APIs build each time a configuration object, to pass onto the implementation. +Those are link:https://javadoc.io/doc/org.apache.pulsar/pulsar-client-original/{javadoc-version}/org/apache/pulsar/client/impl/conf/ClientConfigurationData.html[`ClientConfigurationData`], +link:https://javadoc.io/doc/org.apache.pulsar/pulsar-client-original/{javadoc-version}/org/apache/pulsar/client/impl/conf/ConsumerConfigurationData.html[`ConsumerConfigurationData`], +link:https://javadoc.io/doc/org.apache.pulsar/pulsar-client-original/{javadoc-version}/org/apache/pulsar/client/impl/conf/ProducerConfigurationData.html[`ProducerConfigurationData`]. + +Pulsar Connector allows receiving properties for those configuration objects directly. +For example, the broker authentication information for `PulsarClient` is received using `authPluginClassName` and `authParams` properties. +In order to configure the authentication for the incoming channel `data` : + +[source,properties] +---- +mp.messaging.incoming.data.connector=smallrye-pulsar +mp.messaging.incoming.data.serviceUrl=pulsar://localhost:6650 +mp.messaging.incoming.data.topic=topic +mp.messaging.incoming.data.subscriptionInitialPosition=Earliest +mp.messaging.incoming.data.schema=INT32 +mp.messaging.incoming.data.authPluginClassName=org.apache.pulsar.client.impl.auth.AuthenticationBasic +mp.messaging.incoming.data.authParams={"userId":"superuser","password":"admin"} +---- + +Note that the Pulsar consumer property `subscriptionInitialPosition` is also configured with the `Earliest` value which represents with enum value `SubscriptionInitialPosition.Earliest`. + +This approach covers most of the configuration cases. +However, non-serializable objects such as `CryptoKeyReader`, `ServiceUrlProvider` etc. cannot be configured this way. +The Pulsar Connector allows taking into account instances of Pulsar configuration data objects – +`ClientConfigurationData`, `ConsumerConfigurationData`, `ProducerConfigurationData`: + +[source,java] +---- +include::{examples-dir}/pulsar/configuration/PulsarClientConfigProducers.java[tags=consumer,indent=0] +---- + +This instance is retrieved and used to configure the client used by the connector. +You need to indicate the name of the client using the `client-configuration`, `consumer-configuration` or `producer-configuration` attributes: + +[source,properties] +---- +mp.messaging.incoming.prices.consumer-configuration=my-consumer-options +---- + +If no `++[++client++|++consumer++|++producer++]++-configuration` is configured, the connector will look for instances identified with the channel name: + +[source,java] +---- +include::{examples-dir}/pulsar/configuration/PulsarClientConfigProducers.java[tags=client,indent=0] +---- + +You also can provide a `Map++<++String, Object++>++` containing configuration values by key: + +[source,java] +---- +include::{examples-dir}/pulsar/configuration/PulsarClientConfigProducers.java[tags=producer,indent=0] +---- + +Different configuration sources are loaded in the following order of precedence, from the least important to the highest: + +[arabic] +. `Map++<++String, Object++>++` config map produced with default config identifier, `default-pulsar-client`, `default-pulsar-consumer`, `default-pulsar-producer`. +. `Map++<++String, Object++>++` config map produced with identifier in the configuration or channel name +. `++[++Client++|++Producer++|++Consuemr++]++ConfigurationData` object produced with identifier in the channel configuration or the channel name +. Channel configuration properties named with `++[++Client++|++Producer++|++Consuemr++]++ConfigurationData` field names. + +Following is the configuration reference for the `PulsarClient`. + +Corresponding sections list the xref:pulsar/receiving-pulsar-messages.adoc#configuration-reference[Consumer Configuration Reference] and +xref:pulsar/sending-messages-to-pulsar.adoc#configuration-reference[Producer Configuration Reference]. +Configuration properties not configurable in configuration files (non-serializable) is noted in the column `Config file`. + +== Shared Resources + +The Pulsar connector shares an event loop group and DNS resolver across all Pulsar clients using +link:https://javadoc.io/doc/org.apache.pulsar/pulsar-client-api/{javadoc-version}/org/apache/pulsar/client/api/PulsarClientSharedResources.html[`PulsarClientSharedResources`]. +This reduces thread usage and avoids creating separate thread pools per client. + +By default, the connector creates and manages a `PulsarClientSharedResources` instance with shared `EventLoopGroup` and `DnsResolver`. +To customize the shared resources, produce a CDI bean of type `PulsarClientSharedResources`: + +[source,java] +---- +include::{examples-dir}/pulsar/configuration/PulsarClientConfigProducers.java[tags=shared-resources,indent=0] +---- + +When a `PulsarClientSharedResources` bean is provided, the connector uses it for all Pulsar clients and does not manage its lifecycle. +The application is responsible for closing the shared resources when they are no longer needed. + +== `PulsarClient` Configuration Reference + +include::{partialsdir}/pulsar/config/smallrye-pulsar-client.adoc[] diff --git a/content/docs/smallrye-reactive-messaging/latest/pulsar/client-service.adoc b/content/docs/smallrye-reactive-messaging/latest/pulsar/client-service.adoc new file mode 100644 index 000000000..a50e7db91 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/pulsar/client-service.adoc @@ -0,0 +1,24 @@ += PulsarClientService +:version-root: ../ +include::{version-root}_attributes.adoc[] + +For advanced use cases, SmallRye Reactive Messaging provides a bean of +type `PulsarClientService` that you can inject: + +[source,java] +---- +@Inject +PulsarClientService pulsarClients; +---- + +From there, you can obtain +`org.apache.pulsar.client.api.Consumer`, +`org.apache.pulsar.client.api.Producer` and `org.apache.pulsar.client.api.PulsarClient` by channel name. + +[IMPORTANT] +==== + +Note that the https://pulsar.apache.org/docs/3.0.x/admin-api-get-started/[`PulsarAdmin`] is a separate library and the Pulsar connector doesn't provide it out-of-the-box. +You'd need to explicitly add the dependency, i.e. `org.apache.pulsar:pulsar-client-admin-original` artifact. + +==== diff --git a/content/docs/smallrye-reactive-messaging/latest/pulsar/health.adoc b/content/docs/smallrye-reactive-messaging/latest/pulsar/health.adoc new file mode 100644 index 000000000..090f18c9c --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/pulsar/health.adoc @@ -0,0 +1,24 @@ += Health reporting +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The Pulsar connector reports the startup, readiness and liveness of each channel managed by the connector: + +* *Startup & Readiness* For both inbound and outbound, probes report _OK_ when the +connection with the broker is established. +* *Liveness* For both inbound and outbound, the liveness check verifies that the +connection is established *AND* that no failures have been caught. + +[NOTE] +==== + +To disable health reporting, set the `health-enabled` attribute for the +channel to `false`. + +==== + +Note that a message processing failures _nacks_ the message which is +then handled by the failure-strategy. It is the responsibility of the +failure-strategy to report the failure and influence the outcome of the +liveness checks. The `fail` failure strategy reports the failure and so +the liveness check will report the failure. diff --git a/content/docs/smallrye-reactive-messaging/latest/pulsar/pulsar.adoc b/content/docs/smallrye-reactive-messaging/latest/pulsar/pulsar.adoc new file mode 100644 index 000000000..29e31c8b8 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/pulsar/pulsar.adoc @@ -0,0 +1,64 @@ += Apache Pulsar Connector +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The Pulsar connector adds support for Apache Pulsar to Reactive Messaging. With +it you can consume Pulsar Messages as well as produce _message_ into Pulsar. + +https://pulsar.apache.org/[Apache Pulsar] an open-source, distributed messaging +and streaming platform built for the cloud. For more details about Pulsar, check the +https://pulsar.apache.org/docs/next/concepts-overview/[documentation]. + +Pulsar implements the publish-subscribe pattern. +Producers publish _messages_ to __topic__s. +Consumers create __subscription__s to those topics to receive and process incoming messages, +and send _acknowledgments_ to the broker when processing is finished. + +When a _subscription_ is created, Pulsar retains all messages, even if the consumer is disconnected. +The retained messages are discarded only when a consumer acknowledges that all these messages are processed successfully. + +A Pulsar _message_ consists of + +* *value*, payload data that message contains, while Pulsar messages contain payloads as unstructured byte array, +a *schema* is applied to write and read with an enforced data structure +* *key* of type string, used for partitioning +* *properties*, optional key/value map +* *topic*, the topic that the message is published to +* *producer name*, the name of the producer who produces the message +* *message ID*, the ID assigned by bookies to a message as soon as the message is persistently stored +* *sequence ID*, the ID assigned by producers, indicating its order in that sequence +* *publish time*, timestamp automatically added by the producer +* *event time*, optional timestamp added by the application + +A Pulsar cluster consists of +- One or more **broker**s, which are stateless components +- A *metadata store* for maintaining topic metadata, schema, coordination and cluster configuration. +By default, a Zookeeper cluster is used, except the standalone mode +- A ensemble of *bookies* used for persistent storage of messages + +== Using the Pulsar Connector + +To use the Pulsar Connector, add the following dependency to your +project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.reactive + smallrye-reactive-messaging-pulsar + {project-version} + +---- + +The connector name is: `smallrye-pulsar`. + +So, to indicate that a channel is managed by this connector you need: + +[source,properties] +---- +# Inbound +mp.messaging.incoming.[channel-name].connector=smallrye-pulsar + +# Outbound +mp.messaging.outgoing.[channel-name].connector=smallrye-pulsar +---- diff --git a/content/docs/smallrye-reactive-messaging/latest/pulsar/receiving-pulsar-messages.adoc b/content/docs/smallrye-reactive-messaging/latest/pulsar/receiving-pulsar-messages.adoc new file mode 100644 index 000000000..bafa43e0b --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/pulsar/receiving-pulsar-messages.adoc @@ -0,0 +1,196 @@ += Receiving messages from Pulsar +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The Pulsar Connector connects to a Pulsar broker using a Pulsar client and creates consumers to +receive messages from Pulsar brokers and it maps each of them into Reactive Messaging `Messages`. + +== Example + +Let’s imagine you have a Pulsar broker running, and accessible +using the `pulsar:6650` address (by default it would use +`localhost:6650`). Configure your application to receive Pulsar messages +on the `prices` channel as follows: + +[source,properties] +---- +mp.messaging.incoming.prices.connector=smallrye-pulsar # <1> +mp.messaging.incoming.prices.serviceUrl=pulsar://pulsar:6650 # <2> +mp.messaging.incoming.prices.schema=DOUBLE # <3> +mp.messaging.incoming.prices.subscriptionInitialPosition=Earliest # <4> +---- + +[arabic] +. Sets the connector for the `prices` channel +. Configure the Pulsar broker service url. +. Configure the schema to consume prices as Double. +. Make sure consumer subscription starts receiving messages from the `Earliest` position. + +[NOTE] +==== + +You don’t need to set the Pulsar topic, nor the consumer name. +By default, the connector uses the channel name (`prices`). +You can configure the `topic` and `consumerName` attributes to override them. + +==== + +[NOTE] +==== + +In Pulsar, consumers need to provide a `subscriptionName` for topic subscriptions. +If not provided the connector is generating a unique *subscription name*. + +==== + +Then, your application can receive the `double` payload directly: + +[source,java] +---- +include::{examples-dir}/pulsar/inbound/PulsarPriceConsumer.java[indent=0] +---- + +Or, you can retrieve the `Message++<++Double++>++`: + +[source,java] +---- +include::{examples-dir}/pulsar/inbound/PulsarPriceMessageConsumer.java[indent=0] +---- + +== Consumer Configuration + +The Pulsar Connector allows flexibly configuring the underlying Pulsar consumer. +One of the ways is to set consumer properties directly on the channel configuration. +The list of available configuration properties are listed in xref:#configuration-reference[Configuration Reference]. + +See the xref:pulsar/client-configuration.adoc[Configuring Pulsar consumers, producers and clients] for more information. + +== Deserialization and Pulsar Schema + +The Pulsar Connector allows configuring Schema configuration for the underlying Pulsar consumer. +See the xref:pulsar/schema-configuration.adoc[Configuring the schema used for Pulsar channels] for more information. + +== Inbound Metadata + +The incoming Pulsar messages include an instance of `PulsarIncomingMessageMetadata` in the metadata. +It provides the key, topic, partitions, headers and so on: + +[source,java] +---- +include::{examples-dir}/pulsar/inbound/PulsarMetadataExample.java[tags=code,indent=0] +---- + +== Acknowledgement + +When a message produced from a Pulsar Message is _acknowledged_, the connector sends an https://pulsar.apache.org/docs/3.0.x/concepts-messaging/#acknowledgment[acknowledgement request] to the Pulsar broker. +All Reactive Messaging messages need to be _acknowledged_, which is handled automatically in most cases. +Acknowledgement requests can be sent to the Pulsar broker using the following two strategies: + +* *Individual acknowledgement* is the default strategy, an acknowledgement request is to the broker for each message. +* *Cumulative acknowledgement*, configured using `ack-strategy=cumulative`, the consumer only acknowledges the last message it received. +All messages in the stream up to (and including) the provided message are not redelivered to that consumer. + +== Failure Management + +If a message produced from a Pulsar message is _nacked_, a failure strategy is applied. +The Pulsar connector supports 4 strategies: + +* `nack` _(default)_ sends https://pulsar.apache.org/docs/3.0.x/concepts-messaging/#negative-acknowledgment[negative acknowledgment] to the broker, triggering the broker to redeliver this message to the consumer. +The negative acknowledgment can be further configured using `negativeAckRedeliveryDelayMicros` and `negativeAck.redeliveryBackoff` properties. +* `fail` fail the application, no more messages will be processed. +* `ignore` the failure is logged, but the acknowledgement strategy will be applied and the processing will continue. +* `continue` the failure is logged, but processing continues without applying acknowledgement or negative acknowledgement. This strategy can be used with xref:#acknowledgement-timeout[acknowledgement timeout] configuration. +* `reconsume-later` sends the message to the https://pulsar.apache.org/docs/3.0.x/concepts-messaging/#retry-letter-topic[retry letter topic] using the `reconsumeLater` API to be reconsumed with a delay. +The delay can be configured using the `reconsumeLater.delay` property and defaults to 3 seconds. +Custom delay or properties per message can be configured by adding an instance of link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/io/smallrye/reactive/messaging/pulsar/PulsarReconsumeLaterMetadata.html[`PulsarReconsumeLaterMetadata`] to the failure metadata. + +For example the following configuration for the incoming channel `data` uses `reconsumer-later` failure strategy with default delays of 60 seconds: + +[source,properties] +---- +mp.messaging.incoming.data.connector=smallrye-pulsar +mp.messaging.incoming.data.serviceUrl=pulsar://localhost:6650 +mp.messaging.incoming.data.topic=data +mp.messaging.incoming.data.schema=INT32 +mp.messaging.incoming.data.failure-strategy=reconsume-later +mp.messaging.incoming.data.retryEnable=true +mp.messaging.incoming.data.reconsumeLater.delay=60 // in seconds +mp.messaging.incoming.data.deadLetterPolicy.retryLetterTopic=data-retry +mp.messaging.incoming.data.deadLetterPolicy.maxRedeliverCount=2 +---- + +=== Acknowledgement timeout + +Similar to the negative acknowledgement, with the https://pulsar.apache.org/docs/3.0.x/concepts-messaging/#acknowledgment-timeout[acknowledgment timeout] mechanism, the Pulsar client tracks the unacknowledged messages, +for the given _ackTimeout_ period and sends _redeliver unacknowledged messages request_ to the broker, thus the broker resends the unacknowledged messages to the consumer. + +To configure the timeout and redelivery backoff mechanism you can set `ackTimeoutMillis` and `ackTimeout.redeliveryBackoff` properties. +The `ackTimeout.redeliveryBackoff` value accepts comma separated values of min delay in milliseconds, max delay in milliseconds and multiplier respectively: + +[source,properties] +---- +mp.messaging.incoming.data.connector=smallrye-pulsar +mp.messaging.incoming.data.failure-strategy=continue +mp.messaging.incoming.data.ackTimeoutMillis=10000 +mp.messaging.incoming.data.ackTimeout.redeliveryBackoff=1000,60000,2 +---- + +=== Dead-letter topic + +The https://pulsar.apache.org/docs/3.0.x/concepts-messaging/#dead-letter-topic[dead letter topic] pushes messages that are not consumed successfully to a dead letter topic an continue message consumption. +Note that dead letter topic can be used in different message redelivery methods, such as acknowledgment timeout, negative acknowledgment or retry letter topic. + +[source,properties] +---- +mp.messaging.incoming.data.connector=smallrye-pulsar +mp.messaging.incoming.data.failure-strategy=nack +mp.messaging.incoming.data.deadLetterPolicy.maxRedeliverCount=2 +mp.messaging.incoming.data.deadLetterPolicy.deadLetterTopic=my-dead-letter-topic +mp.messaging.incoming.data.deadLetterPolicy.initialSubscriptionName=my-dlq-subscription +mp.messaging.incoming.data.subscriptionType=Shared +---- + +[IMPORTANT] +==== + +_Negative acknowledgment_ or _acknowledgment timeout_ methods for redelivery will redeliver the whole batch of messages containing at least an unprocessed message. +See xref:pulsar/sending-messages-to-pulsar.adoc#producer-batching[producer batching] for more information. + +==== + +== Receiving Pulsar Messages in Batches + +By default, incoming methods receive each Pulsar message individually. +You can enable batch mode using `batchReceive=true` property, or setting a `batchReceivePolicy` in consumer configuration. + +[source,java] +---- +include::{examples-dir}/pulsar/inbound/PulsarMessageBatchExample.java[tags=code,indent=0] +---- + +Or you can directly receive the list of payloads to the consume method: + +[source,java] +---- +include::{examples-dir}/pulsar/inbound/PulsarMessageBatchPayloadExample.java[tags=code,indent=0] +---- + +=== Accessing metadata of batch records + +When receiving records in batch mode, the metadata of each record is accessible through the `PulsarIncomingBatchMessageMetadata` : + +[source,java] +---- +include::{examples-dir}/pulsar/inbound/PulsarMessageBatchExample.java[tags=batch,indent=0] +---- + +Like in this example, this can be useful to propagate the tracing information of each record. + +== Configuration Reference + +include::{generated-dir}/smallrye-pulsar-incoming.adoc[indent=0] + +In addition to the configuration properties provided by the connector, +following Pulsar consumer properties can also be set on the channel: + +include::{partialsdir}/pulsar/config/smallrye-pulsar-consumer.adoc[] diff --git a/content/docs/smallrye-reactive-messaging/latest/pulsar/schema-configuration.adoc b/content/docs/smallrye-reactive-messaging/latest/pulsar/schema-configuration.adoc new file mode 100644 index 000000000..67aaf89d5 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/pulsar/schema-configuration.adoc @@ -0,0 +1,57 @@ += Configuring the schema used for Pulsar channels +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Pulsar messages are stored with payloads as unstructured byte array. +A Pulsar *schema* defines how to serialize structured data to the raw message bytes +The *schema* is applied in producers and consumers to write and read with an enforced data structure. +It serializes data into raw bytes before they are published to a topic and deserializes the raw bytes before they are delivered to consumers. + +Pulsar uses a schema registry as a central repository to store the registered schema information, +which enables producers/consumers to coordinate the schema of a topic's messages through brokers. +By default the Apache BookKeeper is used to store schemas. + +Pulsar API provides built-in schema information for a number of +https://pulsar.apache.org/docs/3.0.x/schema-understand#primitive-type[primitive types] +and https://pulsar.apache.org/docs/3.0.x/schema-understand#complex-type[complex types] such as Key/Value, Avro and Protobuf. + +The Pulsar Connector allows specifying the schema as a primitive type using the `schema` property: + +[source,properties] +---- +mp.messaging.incoming.prices.connector=smallrye-pulsar +mp.messaging.incoming.prices.schema=INT32 + +mp.messaging.outgoing.prices-out.connector=smallrye-pulsar +mp.messaging.outgoing.prices-out.schema=DOUBLE +---- + +If the value for the `schema` property matches a https://javadoc.io/doc/org.apache.pulsar/pulsar-client-api/latest/org/apache/pulsar/common/schema/SchemaType.html[Schema Type] +a simple schema will be created with that type and will be used for that channel. + +The Pulsar Connector allows configuring complex schema types by providing `Schema` beans through CDI, identified with the `@Identifier` qualifier. + +For example the following bean provides an Avro schema and a Key/Value schema: + +[source,java] +---- +include::{examples-dir}/pulsar/configuration/PulsarSchemaProvider.java[indent=0] +---- + +To configure the incoming channel `users` with defined schema, you need to set the `schema` property to the identifier of the schema `user-schema`: + +[source,properties] +---- +mp.messaging.incoming.users.connector=smallrye-pulsar +mp.messaging.incoming.users.schema=user-schema +---- + +If no `schema` property is found, the connector looks for `Schema` beans identified with the channel name. +For example, the outgoing channel `a-channel` will use the key/value schema. + +[source,properties] +---- +mp.messaging.outgoing.a-channel.connector=smallrye-pulsar +---- + +If no schema information is provided incoming channels will use `Schema.AUTO++_++CONSUME()`, whereas outgoing channels will use `Schema.AUTO++_++PRODUCE++_++BYTES()` schemas. diff --git a/content/docs/smallrye-reactive-messaging/latest/pulsar/sending-messages-to-pulsar.adoc b/content/docs/smallrye-reactive-messaging/latest/pulsar/sending-messages-to-pulsar.adoc new file mode 100644 index 000000000..f3561a08a --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/pulsar/sending-messages-to-pulsar.adoc @@ -0,0 +1,160 @@ += Sending messages to Pulsar +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The Pulsar Connector can write Reactive Messaging `Message`s as Pulsar Message. + +== Example + +Let’s imagine you have a Pulsar broker running, and accessible +using the `pulsar:6650` address (by default it would use +`localhost:1883`). Configure your application to write the messages from +the `prices` channel into a Pulsar Messages as follows: + +[source,properties] +---- +mp.messaging.outgoing.prices.connector=smallrye-pulsar # <1> +mp.messaging.outgoing.prices.serviceUrl=pulsar://pulsar:6650 # <2> +mp.messaging.outgoing.prices.schema=DOUBLE # <3> +---- + +[arabic] +. Sets the connector for the `prices` channel +. Configure the Pulsar broker service url. +. Configure the schema to consume prices as Double. + +[NOTE] +==== + +You don’t need to set the Pulsar topic, nor the producer name. +By default, the connector uses the channel name (`prices`). +You can configure the `topic` and `producerName` attributes to override them. + +==== + +Then, your application must send `Message++<++Double++>++` to the `prices` +channel. It can use `double` payloads as in the following snippet: + +[source,java] +---- +include::{examples-dir}/pulsar/outbound/PulsarPriceProducer.java[indent=0] +---- + +Or, you can send `Message++<++Double++>++`: + +[source,java] +---- +include::{examples-dir}/pulsar/outbound/PulsarPriceMessageProducer.java[indent=0] +---- + +== Producer Configuration + +The Pulsar Connector allows flexibly configuring the underlying Pulsar producer. +One of the ways is to set producer properties directly on the channel configuration. +The list of available configuration properties are listed in xref:#configuration-reference[Configuration Reference]. + +See the xref:pulsar/client-configuration.adoc[Configuring Pulsar consumers, producers and clients] for more information. + +== Serialization and Pulsar Schema + +The Pulsar Connector allows configuring Schema configuration for the underlying Pulsar producer. +See the xref:pulsar/schema-configuration.adoc[Configuring the schema used for Pulsar channels] for more information. + +== Sending key/value pairs + +In order to send Kev/Value pairs to Pulsar, you can configure the Pulsar producer Schema with a +link:https://javadoc.io/doc/org.apache.pulsar/pulsar-client-api/{javadoc-version}/org/apache/pulsar/common/schema/KeyValue.html[`org.apache.pulsar.common.schema.KeyValue`] type: + +[source,java] +---- +include::{examples-dir}/pulsar/outbound/PulsarKeyValueExample.java[tags=code,indent=0] +---- + +If you need more control on the written records, use +`PulsarOutgoingMessageMetadata`. + +== Outbound Metadata + +When sending `Message`s, you can add an instance of +link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-pulsar/{javadoc-version}/io/smallrye/reactive/messaging/pulsar/PulsarOutgoingMessageMetadata.html[`PulsarOutgoingMessageMetadata`] +to influence how the message is going to be written to Pulsar. +For example, configure the record key, and set message properties: + +[source,java] +---- +include::{examples-dir}/pulsar/outbound/PulsarOutboundMetadataExample.java[tags=code,indent=0] +---- + +== OutgoingMessage + +Using link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-pulsar/{javadoc-version}/io/smallrye/reactive/messaging/pulsar/OutgoingMessage.html[`OutgoingMessage`], +is an easy way of customizing the Pulsar message to be published when dealing with payloads and not `Message`s. + +You can create an `OutgoingMessage` with key and value, or from an incoming Pulsar Message: + +[source,java] +---- +include::{examples-dir}/pulsar/outbound/PulsarOutgoingMessageExample.java[tags=code,indent=0] +---- + +== Acknowledgement + +Upon receiving a message from a Producer, a Pulsar broker assigns a `MessageId` to the message and sends it back to the producer, +confirming that the message is published. + +By default, the connector does wait for Pulsar to acknowledge the record +to continue the processing (acknowledging the received `Message`). +You can disable this by setting the `waitForWriteCompletion` attribute to `false`. + +If a record cannot be written, the message is `nacked`. + +[IMPORTANT] +==== + +The Pulsar client automatically retries sending messages in case of failure, until the _send timeout_ is reached. +The _send timeout_ is configurable with `sendTimeoutMs` attribute and by default is is 30 seconds. + +==== + +== Back-pressure and inflight records + +The Pulsar outbound connector handles back-pressure monitoring the number +of pending messages waiting to be written to the Pulsar broker. +The number of pending messages is configured using the +`max-inflight-messages` attribute and defaults to 1000. + +The connector only sends that amount of messages concurrently. No other +messages will be sent until at least one pending message gets +acknowledged by the broker. Then, the connector writes a new message to +Pulsar when one of the broker’s pending messages get acknowledged. + +The Pulsar producer also has a limit on the number of pending messages controlled by +`maxPendingMessages` and for partitioned topics `maxPendingMessagesAcrossPartitions` producer configuration properties. +The default value for the both is `0` which means the limit is enforced by the connector and not the underlying producer. +If a `maxPendingMessagesAcrossPartitions` is set and subscribed topic is partitioned, the connector will set the `max-inflight-messages` use the producer's limit. + +[WARNING] +.Deprecation +==== + +Previously a single connector attribute `maxPendingMessages` controlled both the connector back-pressure and the Pulsar producer configuration. +The `max-inflight-messages` attribute is not the same as the Pulsar producer `maxPendingMessages` configuration property. + +==== + +== Producer Batching + +By default, the Pulsar producer batches individual messages together to be published to the broker. +You can configure batching parameters using `batchingMaxPublishDelayMicros`, `batchingPartitionSwitchFrequencyByPublishDelay`, +`batchingMaxMessages`, `batchingMaxBytes` configuration properties, or disable it completely with `batchingEnabled=false`. + +When using `Key++_++Shared` consumer subscriptions, the `batcherBuilder` can be configured to `BatcherBuilder.KEY++_++BASED`. + +== Configuration Reference + +include::{generated-dir}/smallrye-pulsar-outgoing.adoc[indent=0] + +In addition to the configuration properties provided by the connector, +following Pulsar producer properties can also be set on the channel: + +include::{partialsdir}/pulsar/config/smallrye-pulsar-producer.adoc[] diff --git a/content/docs/smallrye-reactive-messaging/latest/pulsar/transactions.adoc b/content/docs/smallrye-reactive-messaging/latest/pulsar/transactions.adoc new file mode 100644 index 000000000..d46f4e6ba --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/pulsar/transactions.adoc @@ -0,0 +1,89 @@ += Pulsar Transactions and Exactly-Once Processing +:version-root: ../ +include::{version-root}_attributes.adoc[] + +https://pulsar.apache.org/docs/3.0.x/txn-why/[Pulsar transactions] enable event streaming applications to consume, process, and produce messages in one atomic operation. + +Transactions allow one or multiple producers to send batch of messages to multiple topics where all messages in the batch are eventually visible to any consumer, or none is ever visible to consumers. + +In order to be used, transaction support needs to be activated on the broker configuration: + +[source,properties] +---- +# used to enable transaction coordinator +transactionCoordinatorEnabled=true + +# used to create systemTopic used for transaction buffer snapshot +systemTopicEnabled=true +---- + +On the client side, the transaction support also needs to be enabled on `PulsarClient` configuration: + +[source,properties] +---- +mp.messaging.outgoing.tx-producer.enableTransaction=true +---- + +Pulsar connector provides `PulsarTransactions` custom emitter for writing records inside a transaction. + +It can be used as a regular emitter `@Channel`: + +[source,java] +---- +include::{examples-dir}/pulsar/outbound/PulsarTransactionalProducer.java[indent=0] +---- + +The function given to the `withTransaction` method receives a `TransactionalEmitter` for producing records, and returns a `Uni` that provides the result of the transaction. +If the processing completes successfully, the producer is flushed and the transaction is committed. +If the processing throws an exception, returns a failing `Uni`, or marks the `TransactionalEmitter` for abort, the transaction is aborted. + +[NOTE] +==== + +Multiple transactional producers can participate in a single transaction. +This ensures all messages are sent using the started transaction and before the transaction is committed, all participating producers are flushed. + +==== + +If this method is called on a Vert.x context, the processing function is also called on that context. +Otherwise, it is called on the sending thread of the producer. + +== Exactly-Once Processing + +Pulsar Transactions API also allows managing consumer offsets inside a transaction, together with produced messages. +This in turn enables coupling a consumer with a transactional producer in a consume-transform-produce pattern, +also known as exactly-once processing. +It means that an application consumes messages, processes them, publishes the results to a topic, and commits offsets of the consumed messages in a transaction. + +The `PulsarTransactions` emitter also provides a way to apply exactly-once processing to an incoming Pulsar message inside a transaction. + +The following example includes a batch of Pulsar messages inside a transaction. + +[source,properties] +---- + mp.messaging.outgoing.tx-out-example.enableTransaction=true + # ... + mp.messaging.incoming.in-channel.enableTransaction=true + mp.messaging.incoming.in-channel.batchReceive=true +---- + +[source,java] +---- +include::{examples-dir}/pulsar/outbound/PulsarExactlyOnceProcessor.java[indent=0] +---- + +If the processing completes successfully, the message is acknowledged inside the transaction and the transaction is committed. + +[IMPORTANT] +==== + +When using exactly-once processing, messages can only be acked individually rather than cumulatively. + +==== + +If the processing needs to abort, the message is nack'ed. One of the failure strategies can be employed in order to retry the processing or simply fail-stop. +Note that the `Uni` returned from the `#withTransaction` will yield a failure if the transaction fails and is aborted. + +The application can choose to handle the error case, but for the message consumption to continue, `Uni` returned from the `@Incoming` method must not result in failure. +`PulsarTransactions#withTransactionAndAck` method will ack and nack the message but will not stop the reactive stream. +Ignoring the failure simply resets the consumer to the last committed offsets and resumes the processing from there. diff --git a/content/docs/smallrye-reactive-messaging/latest/rabbitmq/rabbitmq-og-client-customization.adoc b/content/docs/smallrye-reactive-messaging/latest/rabbitmq/rabbitmq-og-client-customization.adoc new file mode 100644 index 000000000..2eeebf8fb --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/rabbitmq/rabbitmq-og-client-customization.adoc @@ -0,0 +1,57 @@ += Customizing the underlying RabbitMQ client +:version-root: ../ +include::{version-root}_attributes.adoc[] + +You can customize the underlying RabbitMQ Client configuration by +_producing_ an instance of +https://rabbitmq.github.io/rabbitmq-java-client/api/current/com/rabbitmq/client/ConnectionFactory.html[`ConnectionFactory`]: + +[source,java] +---- +include::{examples-dir}/rabbitmq/og/customization/RabbitMQProducers.java[tags=named,indent=0,opts=optional] +---- + +This instance is retrieved and used to configure the client used by the +connector. You need to indicate the name of the client using the +`client-options-name` attribute: + +.... +mp.messaging.incoming.prices.client-options-name=my-named-options +.... + +== Credentials Provider + +The OG connector supports RabbitMQ's +https://rabbitmq.github.io/rabbitmq-java-client/api/current/com/rabbitmq/client/impl/CredentialsProvider.html[`CredentialsProvider`] +interface for dynamic credential management. This is useful when +credentials are rotated or fetched from an external secrets manager. + +To use a credentials provider, expose a CDI bean implementing +`com.rabbitmq.client.impl.CredentialsProvider` with an `@Identifier` +qualifier, and reference it via the `credentials-provider-name` attribute: + +[source,properties] +---- +mp.messaging.incoming.prices.credentials-provider-name=my-credentials-provider +---- + +== Cluster mode + +To connect to a RabbitMQ cluster, use the `addresses` attribute to +specify multiple broker addresses. When set, this overrides the `host` +and `port` attributes: + +[source,properties] +---- +rabbitmq-addresses=host1:5672,host2:5672,host3:5672 +---- + +== NIO Sockets + +The connector supports using NIO sockets for the RabbitMQ connection. +Enable NIO mode with: + +[source,properties] +---- +rabbitmq-use-nio=true +---- diff --git a/content/docs/smallrye-reactive-messaging/latest/rabbitmq/rabbitmq-og-cloud.adoc b/content/docs/smallrye-reactive-messaging/latest/rabbitmq/rabbitmq-og-cloud.adoc new file mode 100644 index 000000000..b1df1aed5 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/rabbitmq/rabbitmq-og-cloud.adoc @@ -0,0 +1,52 @@ += Connecting to managed instances +:version-root: ../ +include::{version-root}_attributes.adoc[] + +This section describes the connector configuration to use managed +RabbitMQ instances (hosted on the Cloud). + +== Cloud AMQP + +To connect to an instance of RabbitMQ hosted on https://www.cloudamqp.com/[Cloud +AMQP], use the following configuration: + +[source,properties] +---- +rabbitmq-host=host-name +rabbitmq-port=5671 +rabbitmq-username=user-name +rabbitmq-password=password +rabbitmq-virtual-host=user-name +rabbitmq-ssl=true +---- + +You can extract the values from the `AMQPS` url displayed on the +administration portal: + +.... +amqps://user-name:password@host/user-name +.... + +== Amazon MQ + +https://aws.amazon.com/amazon-mq/[Amazon MQ] can host RabbitMQ brokers +(as well as AMQP 1.0 brokers). To connect to a RabbitMQ instance hosted +on Amazon MQ, use the following configuration: + +[source,properties] +---- +rabbitmq-host=host-name +rabbitmq-port=5671 +rabbitmq-username=user-name +rabbitmq-password=password +rabbitmq-ssl=true +---- + +You can extract the host value from the `AMQPS` url displayed on the +administration console: + +.... +amqps://foobarbaz.mq.us-east-2.amazonaws.com:5671 +.... + +The username and password are configured during the broker creation. diff --git a/content/docs/smallrye-reactive-messaging/latest/rabbitmq/rabbitmq-og-health.adoc b/content/docs/smallrye-reactive-messaging/latest/rabbitmq/rabbitmq-og-health.adoc new file mode 100644 index 000000000..ff7bd4d47 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/rabbitmq/rabbitmq-og-health.adoc @@ -0,0 +1,27 @@ += Health reporting +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The RabbitMQ OG connector reports the readiness and liveness of each +channel managed by the connector. + +On the inbound side (receiving messages from RabbitMQ), the check +verifies that the receiver is connected to the broker. + +On the outbound side (sending records to RabbitMQ), the check verifies +that the sender is not disconnected from the broker; the sender _may_ +still be in an initialized state (connection not yet attempted), but +this is regarded as live/ready. + +You can disable health reporting by setting the `health-enabled` attribute of the channel to `false`. +It disables both liveness and readiness. +You can disable readiness reporting by setting the `health-readiness-enabled` attribute of the channel to `false`. + +== @Channel and lazy subscription + +When you inject a channel using `@Channel` annotation, you are responsible for subscribing to the channel. +Until the subscription happens, the channel is not connected to the broker and thus cannot receive messages. +The default health check will fail in this case. + +To handle this use case, you need to configure the `health-lazy-subscription` attribute of the channel to `true`. +It configures the health check to not fail if there are no subscription yet. diff --git a/content/docs/smallrye-reactive-messaging/latest/rabbitmq/rabbitmq-og-tracing.adoc b/content/docs/smallrye-reactive-messaging/latest/rabbitmq/rabbitmq-og-tracing.adoc new file mode 100644 index 000000000..46b1ecce4 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/rabbitmq/rabbitmq-og-tracing.adoc @@ -0,0 +1,41 @@ += OpenTelemetry Tracing +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The RabbitMQ OG connector supports https://opentelemetry.io/[OpenTelemetry] +tracing for both incoming and outgoing channels. + +== Enabling tracing + +Tracing is enabled by default. You can disable it per channel with: + +[source,properties] +---- +mp.messaging.incoming.prices.tracing.enabled=false +mp.messaging.outgoing.prices.tracing.enabled=false +---- + +== How it works + +When tracing is enabled: + +* *Outgoing messages*: The connector creates a `PUBLISH` span and +injects the trace context into the message headers before sending. +* *Incoming messages*: The connector extracts the trace context from +the message headers and creates a `RECEIVE` span linked to the +producer's trace context. + +The span attributes include the exchange name and routing key. + +== Including headers as span attributes + +You can configure specific message headers to be recorded as span +attributes using the `tracing.attribute-headers` property: + +[source,properties] +---- +mp.messaging.incoming.prices.tracing.attribute-headers=my-header,another-header +---- + +This is a comma-separated list of header names whose values will be +added as attributes to the tracing span. diff --git a/content/docs/smallrye-reactive-messaging/latest/rabbitmq/rabbitmq-og.adoc b/content/docs/smallrye-reactive-messaging/latest/rabbitmq/rabbitmq-og.adoc new file mode 100644 index 000000000..77a76a528 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/rabbitmq/rabbitmq-og.adoc @@ -0,0 +1,68 @@ += RabbitMQ OG Connector +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The RabbitMQ OG Connector adds support for RabbitMQ to Reactive Messaging, +based on the AMQP 0-9-1 Protocol Specification. + +Advanced Message Queuing Protocol 0-9-1 (https://www.rabbitmq.com/resources/specs/amqp0-9-1.pdf[AMQP +0-9-1]) is an +open standard for passing business messages between applications or +organizations. + +With this connector, your application can: + +* receive messages from a RabbitMQ queue +* send messages to a RabbitMQ exchange + +The RabbitMQ OG connector is based on the https://www.rabbitmq.com/client-libraries/java-client[RabbitMQ Java Client], +the official RabbitMQ client library. + +[NOTE] +==== + +This connector is an alternative to the existing RabbitMQ connector +(`smallrye-rabbitmq`), which is based on the Vert.x RabbitMQ client. +The OG connector uses the original RabbitMQ Java client directly, +providing improved reconnection handling and direct access to all +RabbitMQ client features. + +==== + +[IMPORTANT] +==== + +The *AMQP connector* supports the AMQP 1.0 protocol, which is very +different from AMQP 0-9-1. You _can_ use the AMQP connector with +RabbitMQ provided that the latter has the https://github.com/rabbitmq/rabbitmq-amqp1.0/blob/v3.7.x/README.md[AMQP 1.0 +Plugin] +installed, albeit with reduced functionality. + +==== + +== Using the RabbitMQ OG connector + +To use the RabbitMQ OG Connector, add the following dependency to your +project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.reactive + smallrye-reactive-messaging-rabbitmq-og + {project-version} + +---- + +The connector name is: `smallrye-rabbitmq-og`. + +So, to indicate that a channel is managed by this connector you need: + +[source,properties] +---- +# Inbound +mp.messaging.incoming.[channel-name].connector=smallrye-rabbitmq-og + +# Outbound +mp.messaging.outgoing.[channel-name].connector=smallrye-rabbitmq-og +---- diff --git a/content/docs/smallrye-reactive-messaging/latest/rabbitmq/receiving-messages-from-rabbitmq.adoc b/content/docs/smallrye-reactive-messaging/latest/rabbitmq/receiving-messages-from-rabbitmq.adoc new file mode 100644 index 000000000..43ee6049d --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/rabbitmq/receiving-messages-from-rabbitmq.adoc @@ -0,0 +1,308 @@ += Receiving messages from RabbitMQ +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The RabbitMQ OG connector lets you retrieve messages from a https://www.rabbitmq.com/[RabbitMQ +broker]. The RabbitMQ connector retrieves +_RabbitMQ Messages_ and maps each of them into Reactive Messaging +`Messages`. + +[NOTE] +==== + +In this context, the reactive messaging concept of a _Channel_ is +realised as a https://www.rabbitmq.com/queues.html[RabbitMQ Queue]. + +==== + +== Example + +Let's imagine you have a RabbitMQ broker running, and accessible using +the `rabbitmq:5672` address (by default it would use `localhost:5672`). +Configure your application to receive RabbitMQ Messages on the `prices` +channel as follows: + +[source,properties] +---- +rabbitmq-host=rabbitmq # <1> +rabbitmq-port=5672 # <2> +rabbitmq-username=my-username # <3> +rabbitmq-password=my-password # <4> + +mp.messaging.incoming.prices.connector=smallrye-rabbitmq-og # <5> +mp.messaging.incoming.prices.queue.name=my-queue # <6> +mp.messaging.incoming.prices.routing-keys=urgent # <7> +---- + +[arabic] +. Configures the broker/router host name. You can do it per channel +(using the `host` attribute) or globally using `rabbitmq-host`. +. Configures the broker/router port. You can do it per channel (using +the `port` attribute) or globally using `rabbitmq-port`. The default +is 5672. +. Configures the broker/router username if required. You can do it per +channel (using the `username` attribute) or globally using +`rabbitmq-username`. +. Configures the broker/router password if required. You can do it per +channel (using the `password` attribute) or globally using +`rabbitmq-password`. +. Instructs the `prices` channel to be managed by the RabbitMQ OG +connector. +. Configures the RabbitMQ queue to read messages from. +. Configures the binding between the RabbitMQ exchange and the +RabbitMQ queue using a routing key. The default is `#` (all messages +will be forwarded from the exchange to the queue) but in general +this can be a comma-separated list of one or more keys. + +Then, your application receives `Message++<++String++>++`. You can consume the +payload directly: + +[source,java] +---- +include::{examples-dir}/rabbitmq/og/inbound/RabbitMQPriceConsumer.java[indent=0,opts=optional] +---- + +Or, you can retrieve the `Message++<++String++>++`: + +[source,java] +---- +include::{examples-dir}/rabbitmq/og/inbound/RabbitMQPriceMessageConsumer.java[indent=0,opts=optional] +---- + +[NOTE] +==== + +Whether you need to explicitly acknowledge the message depends on the +`auto-acknowledgement` channel setting; if that is set to `true` then +your message will be automatically acknowledged on receipt. + +==== + +== Deserialization + +The connector converts incoming RabbitMQ Messages into Reactive +Messaging `Message++<++T++>++` instances. Incoming messages are received as +`byte++[]++` and can be automatically converted to `String` using the +built-in message converter. + +[cols=",",options="header",] +|=== +|content++_++type |Type +|`text/plain` |`String` +|_Anything else or unset_ |`byte++[]++` +|=== + +[NOTE] +==== + +Unlike the Vert.x-based RabbitMQ connector, the OG connector does not +provide automatic conversion to Vert.x `JsonObject` or `JsonArray` +types. For JSON payloads, receive the message as `String` or `byte++[]++` +and use your preferred JSON library (such as Jackson) for +deserialization. + +==== + +== Inbound Metadata + +Messages coming from RabbitMQ contain an instance of link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-rabbitmq-og/{javadoc-version}/io/smallrye/reactive/messaging/rabbitmq/og/IncomingRabbitMQMetadata.html[`IncomingRabbitMQMetadata`] +in the metadata. + +RabbitMQ message headers can be accessed from the metadata either by +calling `getHeader(String header)` to retrieve a single +header value as a `String`, or `getHeader(String header, Class++<++T++>++ type)` to retrieve +a typed header value, or `getHeaders()` to get a map of all header values. + +[source,java] +---- +include::{examples-dir}/rabbitmq/og/inbound/RabbitMQMetadataExample.java[tags=code,indent=0,opts=optional] +---- + +The type `++<++T++>++` of the header value depends on the RabbitMQ type used for +the header: + +[cols=",",options="header",] +|=== +|RabbitMQ Header Type |T +|String |`String` +|Boolean |`Boolean` +|Number |`Number` +|List |`java.util.List` +|=== + +[NOTE] +==== + +The `IncomingRabbitMQMetadata` in the OG connector returns direct types +(e.g. `String`, `Integer`, `Date`) for most property accessors rather +than `Optional` wrappers. Values will be `null` when not set by the +producer. The `getHeader` methods still return `Optional`. + +==== + +== Acknowledgement + +When a Reactive Messaging Message associated with a RabbitMQ Message is +acknowledged, it informs the broker that the message has been +_accepted_. + +Whether you need to explicitly acknowledge the message depends on the +`auto-acknowledgement` setting for the channel; if that is set to `true` +then your message will be automatically acknowledged on receipt. + +== Failure Management + +If a message produced from a RabbitMQ message is _nacked_, a failure +strategy is applied. The RabbitMQ OG connector supports four strategies, +controlled by the `failure-strategy` channel setting: + +* `fail` - fail the application; no more RabbitMQ messages will be +processed. The RabbitMQ message is marked as rejected. +* `accept` - this strategy marks the RabbitMQ message as accepted. The +processing continues ignoring the failure. +* `reject` - this strategy marks the RabbitMQ message as rejected +(default). The processing continues with the next message. +* `requeue` - this strategy marks the RabbitMQ message as rejected +with requeue flag to true. The processing continues with the next message, +but the requeued message will be redelivered to the consumer. + +The RabbitMQ reject `requeue` flag can be controlled on different failure strategies +using the link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/io/smallrye/reactive/messaging/rabbitmq/og/RabbitMQRejectMetadata.html[`RabbitMQRejectMetadata`]. +To do that, use the `Message.nack(Throwable, Metadata)` method by including the +`RabbitMQRejectMetadata` metadata with `requeue` to `true`. + +[source,java] +---- +include::{examples-dir}/rabbitmq/og/inbound/RabbitMQRejectMetadataExample.java[tags=code,indent=0,opts=optional] +---- + +[WARNING] +.Experimental +==== + +`RabbitMQFailureHandler` is experimental and APIs are subject to change in the future + +==== + +In addition, you can also provide your own failure strategy. +To provide a failure strategy implement a bean exposing the interface +link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/io/smallrye/reactive/messaging/rabbitmq/og/fault/RabbitMQFailureHandler.html[`RabbitMQFailureHandler`], +qualified with a `@Identifier`. +Set the name of the bean as the `failure-strategy` channel setting. + +== Configuration Reference + +NOTE: This table is generated from the project's build and is not available for this version. + +To use an existing _queue_, you need to configure the `queue.name` +attribute. + +For example, if you have a RabbitMQ broker already configured with a +queue called `peopleQueue` that you wish to read messages from, you need +the following configuration: + +[source,properties] +---- +mp.messaging.incoming.people.connector=smallrye-rabbitmq-og +mp.messaging.incoming.people.queue.name=peopleQueue +---- + +If you want RabbitMQ to create the queue for you but bind it to an +existing topic exchange `people`, you need the following configuration: + +[source,properties] +---- +mp.messaging.incoming.people.connector=smallrye-rabbitmq-og +mp.messaging.incoming.people.queue.name=peopleQueue +mp.messaging.incoming.people.queue.declare=true +---- + +[NOTE] +==== + +In the above the channel name `people` is implicitly assumed to be the +name of the exchange; if this is not the case you would need to name the +exchange explicitly using the `exchange.name` property. + +==== + +[NOTE] +==== + +The connector supports RabbitMQ's "Server-named Queues" feature to create +an exclusive, auto-deleting, non-durable and randomly named queue. To +enable this feature you set the queue name to exactly `(server.auto)`. +Using this name not only enables the server named queue feature but also +automatically makes ths queue exclusive, auto-deleting, and non-durable; +therefore ignoring any values provided for the `exclusive`, `auto-delete` +and `durable` options. + +==== + +If you want RabbitMQ to create the `people` exchange, queue and binding, +you need the following configuration: + +[source,properties] +---- +mp.messaging.incoming.people.connector=smallrye-rabbitmq-og +mp.messaging.incoming.people.exchange.declare=true +mp.messaging.incoming.people.queue.name=peopleQueue +mp.messaging.incoming.people.queue.declare=true +mp.messaging.incoming.people.queue.routing-keys=tall,short +---- + +In the above we have used an explicit list of routing keys rather than +the default (`#`). Each component of the list creates a separate binding +between the queue and the exchange, so in the case above we would have +two bindings; one based on a routing key of `tall`, the other based on +one of `short`. + +[NOTE] +==== + +The default value of `routing-keys` is `#` (indicating a match against +all possible routing keys) which is only appropriate for _topic_ +Exchanges. If you are using other types of exchange and/or need to +declare queue bindings, you'll need to supply a valid value for the +exchange in question. + +==== + +== Custom arguments for Queue declaration + +When queue declaration is made by the Reactive Messaging channel, using the `queue.declare=true` configuration, +custom queue arguments can be specified using the `queue.arguments` attribute. +`queue.arguments` accepts the identifier (using the `@Identifier` qualifier) of a `Map++<++String, Object++>++` exposed as a CDI bean. +If no arguments has been configured, the default *rabbitmq-queue-arguments* identifier is looked for. + +The following CDI bean produces such a configuration identified with *my-arguments*: + +[source,java] +---- +include::{examples-dir}/rabbitmq/og/customization/ArgumentProducers.java[indent=0,opts=optional] +---- + +Then the channel can be configured to use those arguments in queue declaration: + +[source,properties] +---- +mp.messaging.incoming.data.queue.arguments=my-arguments +---- + +Similarly, the `dead-letter-queue.arguments` allows configuring custom arguments for dead letter queue when one is declared (`auto-bind-dlq=true`). + +== Consumer configuration + +The OG connector provides additional consumer options: + +* `consumer-tag` - a custom consumer tag; if not provided, the broker +generates one automatically. +* `consumer-exclusive` - whether the consumer has exclusive access to +the queue. +* `consumer-arguments` - a comma-separated list of arguments +(`key1:value1,key2:value2,...`) for the consumer. +* `content-type-override` - overrides the `content++_++type` attribute of +the incoming message; should be a valid MIME type. +* `max-outstanding-messages` - the maximum number of unacknowledged +messages being processed concurrently. This controls backpressure +through RabbitMQ's QoS (prefetch count) mechanism. diff --git a/content/docs/smallrye-reactive-messaging/latest/rabbitmq/request-reply.adoc b/content/docs/smallrye-reactive-messaging/latest/rabbitmq/request-reply.adoc new file mode 100644 index 000000000..094e929e0 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/rabbitmq/request-reply.adoc @@ -0,0 +1,139 @@ += RabbitMQ Request/Reply +:version-root: ../ +include::{version-root}_attributes.adoc[] + +[WARNING] +.Experimental +==== + +RabbitMQ Request Reply Emitter is an experimental feature. + +==== + +The RabbitMQ https://www.enterpriseintegrationpatterns.com/patterns/messaging/RequestReply.html[Request-Reply] pattern allows you to publish a message to a RabbitMQ address and then await for a reply message that responds to the initial request, which can be used to implement https://www.rabbitmq.com/tutorials/tutorial-six-java[RPC over RabbitMQ]. + +Given the following request/reply outgoing configuration example: + +[source,properties] +---- +mp.messaging.outgoing.my-request.connector=smallrye-rabbitmq +mp.messaging.outgoing.my-request.exchange.name=rpc +mp.messaging.outgoing.my-request.exchange.type=direct +mp.messaging.outgoing.my-request.default-routing-key=rpc-request +---- + +The `RabbitMQRequestReply` emitter implements the requestor (or the client) of the request-reply pattern for RabbitMQ outbound channels: + +[source,java] +---- +include::{examples-dir}/rabbitmq/outbound/RabbitMQRequestReplyEmitter.java[indent=0] +---- + +[NOTE] +==== + +While in this example we use a direct exchange, it is also possible to use a topic or fanout exchange as well as https://www.rabbitmq.com/docs/local-random-exchange[Local Random Exchanges] for request/reply. + +==== + +The `request` method publishes the request message to the configured target address of the outgoing channel leveraging RabbitMQ https://www.rabbitmq.com/docs/direct-reply-to[Direct Reply-To], +and waits for a reply. +When the reply is received the returned `Uni` is completed with the message payload. + +The request send operation generates a correlation id and sets the `correlationId` property, +which it expects to be sent back in the reply message. + +The replier (or the server) can be implemented using a Reactive Messaging processor: + +[source,properties] +---- +mp.messaging.incoming.request.connector=smallrye-rabbitmq +mp.messaging.incoming.request.exchange.name=rpc +mp.messaging.incoming.request.exchange.type=direct +mp.messaging.incoming.request.routing-keys=rpc-request + +mp.messaging.outgoing.reply.connector=smallrye-rabbitmq +mp.messaging.outgoing.reply.exchange.name="" +---- + +[source,java] +---- +include::{examples-dir}/rabbitmq/outbound/RabbitMQReplier.java[indent=0] +---- + +== Requesting with `Message` types + +Like the core Emitter's `send` methods, `request` method also can receive a `Message` type and return a message: + +[source,java] +---- +include::{examples-dir}/rabbitmq/outbound/RabbitMQRequestReplyMessageEmitter.java[indent=0] +---- + +[NOTE] +==== + +The ingested reply type of the `RabbitMQRequestReply` is discovered at runtime, +in order to configure a `MessageConverter` to be applied on the incoming message before returning the `Uni` result. + +==== + +== Requesting multiple replies + +You can use the `requestMulti` method to expect any number of replies represented by the `Multi` return type. + +For example this can be used to aggregate multiple replies to a single request. + +[source,java] +---- +include::{examples-dir}/rabbitmq/outbound/RabbitMQRequestReplyMultiEmitter.java[indent=0] +---- + +Like the other `request` you can also request `Message` types. + +[NOTE] +==== + +The channel attribute `reply.timeout` will be applied between each message, if reached the returned `Multi` will +fail. + +==== + +== Scaling Request/Reply + +If multiple requestor instances are configured on the same outgoing address, and the same reply address, +each requestor instance will receive replies of all instances. If an observed correlation id doesn't match +the id of any pending replies, the reply is simply discarded. +With the additional network traffic this allows scaling requestors, (and repliers) dynamically. + +== Pending replies and reply timeout + +By default, the `Uni` returned from the `request` method is configured to fail with timeout exception if no replies is received after 5 seconds. +This timeout is configurable with the channel attribute `reply.timeout`. + +A snapshot of the list of pending replies is available through the `RabbitMQRequestReply#getPendingReplies` method. + +== Correlation Ids + +The RabbitMQ Request/Reply allows configuring the correlation id mechanism completely through a `CorrelationIdHandler` implementation. +The default handler is based on randomly generated UUID strings. +The correlation id handler implementation can be configured using the `reply.correlation-id.handler` attribute. +As mentioned the default configuration is `uuid`. + +Custom handlers can be implemented by proposing a CDI-managed bean with `@Identifier` qualifier. + +== Reply Error Handling + +If the reply server produces an error and can or would like to propagate the error back to the requestor, failing the returned `Uni`. + +If configured using the `reply.failure.handler` channel attribute, +the `ReplyFailureHandler` implementations are discovered through CDI, matching the `@Identifier` qualifier. + +A sample reply error handler can lookup header values and return the error to be thrown by the reply: + +[source,java] +---- +include::{examples-dir}/rabbitmq/outbound/MyRabbitMQReplyFailureHandler.java[indent=0] +---- + +`null` return value indicates that no error has been found in the reply, and it can be delivered to the application. diff --git a/content/docs/smallrye-reactive-messaging/latest/rabbitmq/sending-messages-to-rabbitmq.adoc b/content/docs/smallrye-reactive-messaging/latest/rabbitmq/sending-messages-to-rabbitmq.adoc new file mode 100644 index 000000000..0dbb3830c --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/rabbitmq/sending-messages-to-rabbitmq.adoc @@ -0,0 +1,241 @@ += Sending messages to RabbitMQ +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The RabbitMQ OG connector can write Reactive Messaging `Messages` as +RabbitMQ Messages. + +[NOTE] +==== + +In this context, the reactive messaging concept of a _Channel_ is +realised as a https://www.rabbitmq.com/tutorials/amqp-concepts.html#exchanges[RabbitMQ +Exchange]. + +==== + +== Example + +Let's imagine you have a RabbitMQ broker running, and accessible using +the `rabbitmq:5672` address (by default it would use `localhost:5672`). +Configure your application to send the messages from the `prices` +channel as a RabbitMQ Message as follows: + +.... +rabbitmq-host=rabbitmq # <1> +rabbitmq-port=5672 # <2> +rabbitmq-username=my-username # <3> +rabbitmq-password=my-password # <4> + +mp.messaging.outgoing.prices.connector=smallrye-rabbitmq-og # <5> +mp.messaging.outgoing.prices.default-routing-key=normal # <6> +.... + +[arabic] +. Configures the broker/router host name. You can do it per channel +(using the `host` attribute) or globally using `rabbitmq-host` +. Configures the broker/router port. You can do it per channel (using +the `port` attribute) or globally using `rabbitmq-port`. The default +is `5672`. +. Configures the broker/router username if required. You can do it per +channel (using the `username` attribute) or globally using +`rabbitmq-username`. +. Configures the broker/router password if required. You can do it per +channel (using the `password` attribute) or globally using +`rabbitmq-password`. +. Instructs the `prices` channel to be managed by the RabbitMQ OG +connector +. Supplies the default routing key to be included in outbound +messages; this will be used if the "raw payload" form of message +sending is used (see below). + +[NOTE] +==== + +You don't need to set the RabbitMQ exchange name. By default, it uses +the channel name (`prices`) as the name of the exchange to send messages +to. You can configure the `exchange.name` attribute to override it. + +==== + +Then, your application can send `Message++<++Double++>++` to the prices channel. +It can use `double` payloads as in the following snippet: + +[source,java] +---- +include::{examples-dir}/rabbitmq/og/outbound/RabbitMQPriceProducer.java[indent=0,opts=optional] +---- + +Or, you can send `Message++<++Double++>++`, which affords the opportunity to +explicitly specify metadata on the outgoing message: + +[source,java] +---- +include::{examples-dir}/rabbitmq/og/outbound/RabbitMQPriceMessageProducer.java[indent=0,opts=optional] +---- + +== Serialization + +When sending a `Message++<++T++>++`, the connector converts the message into a +RabbitMQ Message. The payload is converted to the RabbitMQ Message body. + +[width="100%",cols="32%,68%",options="header",] +|=== +|T |RabbitMQ Message Body +|primitive types or `UUID`/`String` |String value with `content++_++type` set to `text/plain` +|`byte++[]++` |Binary content, with `content++_++type` set to `application/octet-stream` +|Any other class |The payload is converted via `toString()` with `content++_++type` set to `application/json` +|=== + +[NOTE] +==== + +Unlike the Vert.x-based RabbitMQ connector, the OG connector does not +handle Vert.x `JsonObject`, `JsonArray`, or `Buffer` types directly. +For JSON payloads, serialize to `String` or `byte++[]++` before sending. + +==== + +If the message payload cannot be serialized, the message is _nacked_. + +== Outbound Metadata + +When sending `Messages`, you can add an instance of link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-rabbitmq-og/{javadoc-version}/io/smallrye/reactive/messaging/rabbitmq/og/OutgoingRabbitMQMetadata.html[`OutgoingRabbitMQMetadata`] +to influence how the message is handled by RabbitMQ. For example, you +can configure the routing key, timestamp and headers: + +[source,java] +---- +include::{examples-dir}/rabbitmq/og/outbound/RabbitMQOutboundMetadataExample.java[tags=code,indent=0,opts=optional] +---- + +[NOTE] +==== + +The `OutgoingRabbitMQMetadata` in the OG connector uses +`OutgoingRabbitMQMetadata.builder()` as the entry point for building +metadata (instead of `new OutgoingRabbitMQMetadata.Builder()`). It +also uses `java.util.Date` for timestamps instead of `ZonedDateTime`. + +==== + +== Publisher Confirms + +The OG connector supports https://www.rabbitmq.com/docs/confirms#publisher-confirms[publisher confirms] +for reliable message publishing. When enabled, the connector waits for +the broker to acknowledge each published message before considering the +send operation complete. + +[source,properties] +---- +mp.messaging.outgoing.prices.publish-confirms=true +---- + +When publisher confirms are enabled, message acknowledgement in +Reactive Messaging is tied to the broker's confirmation. If the broker +does not confirm the message, the Reactive Messaging message is _nacked_. + +== Acknowledgement + +By default, the Reactive Messaging `Message` is acknowledged when the +broker acknowledges the message. + +== Configuration Reference + +NOTE: This table is generated from the project's build and is not available for this version. + +== Using existing destinations + +To use an existing _exchange_, you may need to configure the +`exchange.name` attribute. + +For example, if you have a RabbitMQ broker already configured with an +exchange called `people` that you wish to send messages to, you need the +following configuration: + +[source,properties] +---- +mp.messaging.outgoing.people.connector=smallrye-rabbitmq-og +---- + +You would need to configure the `exchange.name` attribute, if the +exchange name were not the channel name: + +[source,properties] +---- +mp.messaging.outgoing.people-out.connector=smallrye-rabbitmq-og +mp.messaging.outgoing.people-out.exchange.name=people +---- + +If you want RabbitMQ to create the `people` exchange, you need the +following configuration: + +[source,properties] +---- +mp.messaging.outgoing.people-out.connector=smallrye-rabbitmq-og +mp.messaging.outgoing.people-out.exchange.name=people +mp.messaging.outgoing.people-out.exchange.declare=true +---- + +[NOTE] +==== + +The above example will create a `topic` exchange and use an empty +default `routing-key` (unless overridden programatically using outgoing +metadata for the message). If you want to create a different type of +exchange or have a different default routing key, then the +`exchange.type` and `default-routing-key` properties need to be +explicitly specified. + +==== + +== Sending to specific queues via the default exchange + +To send a message to a specific queue (usually a reply queue), +you have to configure the default exchange as an outgoing channel and set the name of the queue as routing key in the message metadata. +The name of the exchange needs to be set to `""`. + +[source,properties] +---- +mp.messaging.outgoing.channel-name-for-default-exchange.connector=smallrye-rabbitmq-og +mp.messaging.outgoing.channel-name-for-default-exchange.exchange.name="" +---- + +== Custom arguments for Exchange declaration + +When exchange declaration is made by the Reactive Messaging channel, using the `exchange.declare=true` configuration, +custom exchange arguments can be specified using the `exchange.arguments` attribute. +`exchange.arguments` accepts the identifier (using the `@Identifier` qualifier) of a `Map++<++String, Object++>++` exposed as a CDI bean. +If no arguments has been configured, the default *rabbitmq-exchange-arguments* identifier is looked for. + +The following CDI bean produces such a configuration identified with *my-arguments*: + +[source,java] +---- +include::{examples-dir}/rabbitmq/og/customization/ArgumentProducers.java[indent=0,opts=optional] +---- + +Then the channel can be configured to use those arguments in exchange declaration: + +[source,properties] +---- +mp.messaging.outgoing.data.exchange.arguments=my-arguments +---- + +Similarly, the `dead-letter-exchange.arguments` allows configuring custom arguments for dead letter exchange when one is declared (`dlx.declare=true`). + +== Retry on failure + +The OG connector supports automatic retry when message publishing fails. +This is controlled by two configuration attributes: + +* `retry-on-fail-attempts` (default: 6) - the number of retry attempts +before giving up. +* `retry-on-fail-interval` (default: 5 seconds) - the interval between +retry attempts. + +[source,properties] +---- +mp.messaging.outgoing.prices.retry-on-fail-attempts=10 +mp.messaging.outgoing.prices.retry-on-fail-interval=3 +---- diff --git a/content/docs/smallrye-reactive-messaging/latest/sns/sending-aws-sns-messages.adoc b/content/docs/smallrye-reactive-messaging/latest/sns/sending-aws-sns-messages.adoc new file mode 100644 index 000000000..69282c824 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/sns/sending-aws-sns-messages.adoc @@ -0,0 +1,72 @@ += Sending AWS SNS Messages +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The AWS SNS connector allows you to send messages to an AWS SNS topic. + +== Sending messages + +Before you start, you need to have an AWS account and an SNS topic created. +To send messages to an SNS topic, you need to create a method that produces messages to the topic. + +Then, your application can send `Message++<++String++>++` to the prices channel. +It can use `String` payloads as in the following snippet: + +[source,java] +---- +include::{examples-dir}/sns/outbound/SnsProducer.java[indent=0] +---- + +Or, you can send `Message++<++Double++>++`, which affords the opportunity to +explicitly specify metadata on the outgoing message: + +[source,java] +---- +include::{examples-dir}/sns/outbound/SnsMessageProducer.java[indent=0] +---- + +== Sending messages in batch + +You can configure the outbound channel to send messages in batch of maximum 10 messages (AWS SNS limitation). + +You can customize the size of batches, `10` being the default batch size, and the delay to wait for new messages to be added to the batch, 3000ms being the default delay: + +[source,java] +---- +mp.messaging.outgoing.prices.connector=smallrye-sns +mp.messaging.outgoing.prices.topic=prices +mp.messaging.outgoing.prices.batch=true +mp.messaging.outgoing.prices.batch-size=5 +mp.messaging.outgoing.prices.batch-delay=3000 +---- + +== Serialization + +When sending a `Message++<++T++>++`, the connector converts the message into a AWS SNS Message. +How the message is converted depends on the payload type: + +* If the payload is of type `PublishRequest` it is sent as is. +* If the payload is of type `PublishRequest.Builder`, the topic url is set and sent. +* If the payload is of primitive types the payload is converted to String and the message attribute `++_++classname` is set to the class name of the payload. +* If the payload is of any other object type, the payload is serialized (using the `JsonMapping` implementation discovered) and the message attribute `++_++classname` is set to the class name of the payload. + +If the message payload cannot be serialized to JSON, the message is _nacked_. + +== Outbound Metadata + +When sending `Messages`, you can add an instance of link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-aws-sns/{javadoc-version}/io/smallrye/reactive/messaging/aws/sns/SnsOutboundMetadata.html[`SnsOutboundMetadata`] +to influence how the message is handled by AWS SNS. For example, you can configure the routing key, timestamp and headers: + +[source,java] +---- +include::{examples-dir}/sns/outbound/SnsOutboundMetadataExample.java[tags=code,indent=0] +---- + +== Acknowledgement + +By default, the Reactive Messaging `Message` is acknowledged when the +send message request is successful. If the message is not sent successfully, the message is _nacked_. + +== Configuration Reference + +include::{generated-dir}/smallrye-sns-outgoing.adoc[indent=0] diff --git a/content/docs/smallrye-reactive-messaging/latest/sns/sns.adoc b/content/docs/smallrye-reactive-messaging/latest/sns/sns.adoc new file mode 100644 index 000000000..b24bf08f7 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/sns/sns.adoc @@ -0,0 +1,59 @@ += AWS SNS Connector +:version-root: ../ +include::{version-root}_attributes.adoc[] + +[WARNING] +.Preview +==== + +The AWS SNS Connector is currently in *preview*. + +==== + +The AWS SNS Connector adds support for AWS SNS to Reactive Messaging. + +With this connector, your application can send messages to a AWS SNS topic. + +The AWS SNS connector is based on the https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/home.html[AWS SDK for Java V2]. + +== Using the AWS SNS connector + +To use the AWS SNS Connector, add the following dependency to your project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.reactive + smallrye-reactive-messaging-aws-sns + {project-version} + +---- + +The connector name is: `smallrye-sns`. + +So, to indicate that a channel is managed by this connector you need: + +[source,properties] +---- +# Inbound +mp.messaging.incoming.[channel-name].connector=smallrye-sns + +# Outbound +mp.messaging.outgoing.[channel-name].connector=smallrye-sns +---- + +== Configuration + +When available the AWS SNS connector discovers the SNS client as a CDI bean. +This allows using the connector with the https://docs.quarkiverse.io/quarkus-amazon-services/dev/amazon-sns.html[Quarkiverse AWS SNS extension]. +If the SNS client is not available as a CDI bean, the connector creates a new client using the provided configuration. + +* `topic` - The name of the SNS queue, defaults to channel name if not provided. +* `region` - The name of the SNS region. +* `endpoint-override` - The endpoint url override. +* `credentials-provider` - The fully qualified class name of the credential provider to be used in the client, if not provided the default provider chain is used. + +== Additional Resources + +* https://docs.aws.amazon.com/sns/index.html[AWS SNS Documentation] +* https://docs.quarkiverse.io/quarkus-amazon-services/dev/amazon-sns.html[Quarkiverse AWS SNS Extension] diff --git a/content/docs/smallrye-reactive-messaging/latest/sqs/receiving-aws-sqs-messages.adoc b/content/docs/smallrye-reactive-messaging/latest/sqs/receiving-aws-sqs-messages.adoc new file mode 100644 index 000000000..2b03c4832 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/sqs/receiving-aws-sqs-messages.adoc @@ -0,0 +1,140 @@ += Receiving AWS SQS Messages +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The AWS SQS connector allows you to receive messages from an AWS SQS queue. + +== Receiving messages + +Before you start, you need to have an AWS account and a SQS queue created. +To receive messages from an SQS queue, you need to create a method that consumes messages from the queue. + +[source,properties] +---- +mp.messaging.incoming.data.connector=smallrye-sqs +mp.messaging.incoming.data.queue=my-queue +---- + +Then, your application receives `Message++<++String++>++`. +You can consume the payload directly: + +[source,java] +---- +include::{examples-dir}/sqs/inbound/SqsStringConsumer.java[indent=0] +---- + +Or, you can retrieve the `Message++<++String++>++`: + +[source,java] +---- +include::{examples-dir}/sqs/inbound/SqsMessageStringConsumer.java[indent=0] +---- + +You also can directly consume the `software.amazon.awssdk.services.sqs.model.Message`: + +[source,java] +---- +include::{examples-dir}/sqs/inbound/SqsSdkMessageConsumer.java[indent=0] +---- + +=== Receive message request customizer + +The receive message requests sent to AWS SQS can be customized by providing a CDI bean implementation of +link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-aws-sqs/{javadoc-version}/io/smallrye/reactive/messaging/aws/sqs/SqsReceiveMessageRequestCustomizer.html[`SqsReceiveMessageRequestCustomizer`] +and configuring it's identifier using the `receive.request.customizer` connector attribute. + +[source,java] +---- +include::{examples-dir}/sqs/inbound/SqsReceiveMessageRequestCustomizerExample.java[indent=0] +---- + +[source,properties] +---- +mp.messaging.incoming.data.connector=smallrye-sqs +mp.messaging.incoming.data.queue=my-queue +mp.messaging.incoming.data.receive.request.customizer=my-customizer +---- + +Receive requests failed with retryable exceptions are retried automatically, by setting the failed request id. + +=== Receive message request pause and resume + +The AWS SQS connector fetches messages by continuously sending receive message requests. +If messages are not processed in a timely manner, the connector pauses fetching messages until queued messages are processed. + +The pause resume can be disabled using the `receive.request.pause.resume` connector attribute. + +[source,properties] +---- +mp.messaging.incoming.data.receive.request.pause.resume=false +---- + +== Deserialization + +The connector converts incoming SQS Messages into Reactive Messaging `Message++<++T++>++` instances. + +The payload type `T` is determined based on the value of the SQS message attribute `++_++classname`. + +If you send messages with the AWS SQS connector (outbound connector), +the `++_++classname` attribute is automatically added to the message. +The primitive types are transformed from the string representation to the corresponding Java type. +For objects, if one of the `JsonMapping` modules is present on the classpath, +the connector used that JSON module to deserialize the message body to objects. + +If the `++_++classname` attribute is not present, the payload is deserialized as a `String`. + +[source,java] +---- +include::{examples-dir}/sqs/json/SqsJsonMapping.java[tags=code,indent=0] +---- + +== Inbound Metadata + +Messages coming from SQS contain an instance of link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-aws-sqs/{javadoc-version}/io/smallrye/reactive/messaging/aws/sqs/SqsIncomingMetadata.html[`SqsIncomingMetadata`] +in the metadata. + +SQS message attributes can be accessed from the metadata either by name or by the `MessageAttributeValue` object. + +[source,java] +---- +include::{examples-dir}/sqs/inbound/SqsMetadataExample.java[indent=0] +---- + +== Acknowledgement Strategies + +The default strategy for acknowledging AWS SQS Message is to _delete_ the message from the queue. +You can set the `ack-strategy` attribute to `ignore` if you want to ignore the message. + +++[++NOTE++]++ Deprecated +`ack.delete` attribute is deprecated and will be removed in a future release. + +You can implement a custom strategy by implementing the link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-aws-sqs/{javadoc-version}/io/smallrye/reactive/messaging/aws/sqs/SqsAckHandler.html[`SqsAckHandler`], +interface with a `Factory` class and registering it as a CDI bean with an `@Identifier`. + +[source,java] +---- +include::{examples-dir}/sqs/inbound/SqsCustomAckStrategy.java[indent=0] +---- + +== Failure Strategies + +The default strategy for handling message processing failures is `ignore`. +It lets the visibility timeout of the message consumer to expire and reconsume the message. + +Other possible strategies are: + +* `fail`: the failure is logged and the channel fail-stops. +* `delete`: the message is removed from the queue. +* `visibility`: the message visibility timeout is reset to 0. + +You can implement a custom strategy by implementing the link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-aws-sqs/{javadoc-version}/io/smallrye/reactive/messaging/aws/sqs/SqsFailureHandler.html[`SqsFailureHandler`], +interface with a `Factory` class and registering it as a CDI bean with an `@Identifier`. + +[source,java] +---- +include::{examples-dir}/sqs/inbound/SqsCustomNackStrategy.java[indent=0] +---- + +== Configuration Reference + +include::{generated-dir}/smallrye-sqs-incoming.adoc[indent=0] diff --git a/content/docs/smallrye-reactive-messaging/latest/sqs/sending-aws-sqs-messages.adoc b/content/docs/smallrye-reactive-messaging/latest/sqs/sending-aws-sqs-messages.adoc new file mode 100644 index 000000000..94e92d1ce --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/sqs/sending-aws-sqs-messages.adoc @@ -0,0 +1,74 @@ += Sending AWS SQS Messages +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The AWS SQS connector allows you to send messages to an AWS SQS queue. + +== Sending messages + +Before you start, you need to have an AWS account and an SQS queue created. +To send messages to an SQS queue, you need to create a method that produces messages to the queue. + +Then, your application can send `Message++<++String++>++` to the prices channel. +It can use `String` payloads as in the following snippet: + +[source,java] +---- +include::{examples-dir}/sqs/outbound/SqsStringProducer.java[indent=0] +---- + +Or, you can send `Message++<++Double++>++`, which affords the opportunity to +explicitly specify metadata on the outgoing message: + +[source,java] +---- +include::{examples-dir}/sqs/outbound/SqsMessageStringProducer.java[indent=0] +---- + +== Sending messages in batch + +You can configure the outbound channel to send messages in batch of maximum 10 messages (AWS SQS limitation). + +You can customize the size of batches, `10` being the default batch size, and the delay to wait for new messages to be added to the batch, 3000ms being the default delay: + +[source,java] +---- +mp.messaging.outgoing.prices.connector=smallrye-sqs +mp.messaging.outgoing.prices.queue=prices +mp.messaging.outgoing.prices.batch=true +mp.messaging.outgoing.prices.batch-size=5 +mp.messaging.outgoing.prices.batch-delay=3000 +---- + +== Serialization + +When sending a `Message++<++T++>++`, the connector converts the message into a AWS SQS Message. +How the message is converted depends on the payload type: + +* If the payload is of type `SendMessageRequest` it is sent as is. +* If the payload is of type `SendMessageRequest.Builder`, the queue url is set and sent. +* If the payload is of type `software.amazon.awssdk.services.sqs.model.Message` it is usd to set the message body and attributes. +* If the payload is of primitive types the paylaod is converted to String and the message attribute `++_++classname` is set to the class name of the payload. +* If the payload is of any other object type, the payload is serialized (using the `JsonMapping` implementation discovered) and the message attribute `++_++classname` is set to the class name of the payload. + +If the message payload cannot be serialized to JSON, the message is _nacked_. + +== Outbound Metadata + +When sending `Messages`, you can add an instance of link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-aws-sqs/{javadoc-version}/io/smallrye/reactive/messaging/aws/sqs/SqsOutboundMetadata.html[`SqsOutboundMetadata`] +to influence how the message is handled by AWS SQS. For example, you +can configure the routing key, timestamp and headers: + +[source,java] +---- +include::{examples-dir}/sqs/outbound/SqsOutboundMetadataExample.java[tags=code,indent=0] +---- + +== Acknowledgement + +By default, the Reactive Messaging `Message` is acknowledged when the +send message request is successful. If the message is not sent successfully, the message is _nacked_. + +== Configuration Reference + +include::{generated-dir}/smallrye-sqs-outgoing.adoc[indent=0] diff --git a/content/docs/smallrye-reactive-messaging/latest/sqs/sqs.adoc b/content/docs/smallrye-reactive-messaging/latest/sqs/sqs.adoc new file mode 100644 index 000000000..83b664f6d --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/latest/sqs/sqs.adoc @@ -0,0 +1,62 @@ += AWS SQS Connector +:version-root: ../ +include::{version-root}_attributes.adoc[] + +[WARNING] +.Preview +==== + +The AWS SQS Connector is currently in *preview*. + +==== + +The AWS SQS Connector adds support for AWS SQS to Reactive Messaging. + +With this connector, your application can: + +* receive messages from a RabbitMQ queue +* send messages to a RabbitMQ exchange + +The AWS SQS connector is based on the https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/home.html[AWS SDK for Java V2]. + +== Using the AWS SQS connector + +To use the AWS SQS Connector, add the following dependency to your project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.reactive + smallrye-reactive-messaging-aws-sqs + {project-version} + +---- + +The connector name is: `smallrye-sqs`. + +So, to indicate that a channel is managed by this connector you need: + +[source,properties] +---- +# Inbound +mp.messaging.incoming.[channel-name].connector=smallrye-sqs + +# Outbound +mp.messaging.outgoing.[channel-name].connector=smallrye-sqs +---- + +== Configuration + +When available the AWS SQS connector discovers the SQS client as a CDI bean. +This allows using the connector with the https://docs.quarkiverse.io/quarkus-amazon-services/dev/amazon-sqs.html[Quarkiverse AWS SQS extension]. +If the SQS client is not available as a CDI bean, the connector creates a new client using the provided configuration. + +* `queue` - The name of the SQS queue, defaults to channel name if not provided. +* `region` - The name of the SQS region. +* `endpoint-override` - The endpoint url override. +* `credentials-provider` - The fully qualified class name of the credential provider to be used in the client, if not provided the default provider chain is used. + +== Additional Resources + +* https://docs.aws.amazon.com/sqs/index.html[AWS SQS Documentation] +* https://docs.quarkiverse.io/quarkus-amazon-services/dev/amazon-sqs.html[Quarkiverse AWS SQS Extension] diff --git a/content/docs/smallrye-reactive-messaging/main/_attributes.adoc b/content/docs/smallrye-reactive-messaging/main/_attributes.adoc new file mode 100644 index 000000000..dacabad96 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_attributes.adoc @@ -0,0 +1,26 @@ +:project-version: 5.0.0-RC1 +// Attributes shared by every page. smallrye.io overrides the ones it needs when it publishes. +// Pages in subdirectories set :version-root: (../ per level) before including this file. +ifndef::version-root[:version-root:] +:idprefix: +:idseparator: - +:sectanchors: +:examples-dir: {version-root}_examples +:partialsdir: {version-root}_partials +:project-name: SmallRye Reactive Messaging +:javadoc-version: latest +:version: {project-version} +:generated-dir: {version-root}_generated + +// Added by smallrye.io (tools/docs/Docs.java sync) +:javadoc-version: 5.0.0-RC1 +:relfileprefix: /docs/smallrye-reactive-messaging/main/ +:imagesdir: /docs/smallrye-reactive-messaging/main +:docs-project: smallrye-reactive-messaging +:docs-version: main +:docs-ref: main +:weld-version: 6.0.4.Final +:smallrye-config-version: 4.0.0 +:mutiny-version: 3.1.0 +:camel-version: 4.22.1 +:spec-version: 3.0.1 diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/ack/AutoNack.java b/content/docs/smallrye-reactive-messaging/main/_examples/ack/AutoNack.java new file mode 100644 index 000000000..bdb5a371a --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/ack/AutoNack.java @@ -0,0 +1,55 @@ +package ack; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Uni; + +public class AutoNack { + + // tag::auto-nack[] + @Incoming("data") + @Outgoing("out") + public String process(String s) { + if (s.equalsIgnoreCase("b")) { + // Throwing an exception triggers a nack + throw new IllegalArgumentException("b"); + } + + if (s.equalsIgnoreCase("e")) { + // Returning null would skip the message (it will be acked) + return null; + } + + return s.toUpperCase(); + } + + @Incoming("data") + @Outgoing("out") + public Uni processAsync(String s) { + if (s.equalsIgnoreCase("a")) { + // Returning a failing Uni triggers a nack + return Uni.createFrom().failure(new Exception("a")); + } + + if (s.equalsIgnoreCase("b")) { + // Throwing an exception triggers a nack + throw new IllegalArgumentException("b"); + } + + if (s.equalsIgnoreCase("e")) { + // Returning null would skip the message (it will be acked not nacked) + return Uni.createFrom().nullItem(); + } + + if (s.equalsIgnoreCase("f")) { + // returning `null` is invalid for method returning Unis, the message is nacked + return null; + } + + return Uni.createFrom().item(s.toUpperCase()); + } + + // end::auto-nack[] + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/ack/MessageAckExamples.java b/content/docs/smallrye-reactive-messaging/main/_examples/ack/MessageAckExamples.java new file mode 100644 index 000000000..5b4ddf459 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/ack/MessageAckExamples.java @@ -0,0 +1,52 @@ +package ack; + +import java.util.concurrent.CompletableFuture; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +public class MessageAckExamples { + + // tag::with-payload[] + @Incoming("in") + @Outgoing("out") + public Message process(Message in) { + // The acknowledgement is forwarded, when the consumer + // acknowledges the message, `in` will be acknowledged + return in.withPayload(in.getPayload() + 1); + } + // end::with-payload[] + + public void creation() { + // tag::message-creation[] + Message message = Message.of("hello", () -> { + // called when the consumer acknowledges the message + + // return a CompletionStage completed when the + // acknowledgment of the created message is + // completed. + // For immediate ack use: + return CompletableFuture.completedFuture(null); + + }); + // end::message-creation[] + } + + // tag::process[] + @Incoming("in") + @Outgoing("out") + public Message processAndProduceNewMessage(Message in) { + // The acknowledgement is forwarded, when the consumer + // acknowledges the message, `in` will be acknowledged + return Message.of(in.getPayload() + 1, + () -> { + // Called when the consumer acknowledges the message + // ... + // Don't forget to acknowledge the incoming message: + return in.ack(); + }); + } + // end::process[] + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/ack/NackOnEmitter.java b/content/docs/smallrye-reactive-messaging/main/_examples/ack/NackOnEmitter.java new file mode 100644 index 000000000..ffdbc70bc --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/ack/NackOnEmitter.java @@ -0,0 +1,27 @@ +package ack; + +import java.util.concurrent.CompletionStage; + +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Emitter; + +public class NackOnEmitter { + + // tag::emitter[] + @Inject + @Channel("data") + Emitter emitter; + + public void emitPayload() { + CompletionStage completionStage = emitter.send("hello"); + completionStage.whenComplete((acked, nacked) -> { + if (nacked != null) { + // the processing has failed + } + }); + } + // end::emitter[] + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/ack/PreAckExamples.java b/content/docs/smallrye-reactive-messaging/main/_examples/ack/PreAckExamples.java new file mode 100644 index 000000000..04b336bde --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/ack/PreAckExamples.java @@ -0,0 +1,20 @@ +package ack; + +import org.eclipse.microprofile.reactive.messaging.Acknowledgment; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +public class PreAckExamples { + + // tag::pre[] + @Incoming("in") + @Outgoing("out") + @Acknowledgment(Acknowledgment.Strategy.PRE_PROCESSING) + public String process(String input) { + // The message wrapping the payload is already acknowledged + // The default would have waited the produced message to be + // acknowledged + return input.toUpperCase(); + } + // end::pre[] +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/ack/StreamAckExamples.java b/content/docs/smallrye-reactive-messaging/main/_examples/ack/StreamAckExamples.java new file mode 100644 index 000000000..ea47f34b4 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/ack/StreamAckExamples.java @@ -0,0 +1,42 @@ +package ack; + +import java.util.concurrent.Flow.Publisher; + +import org.eclipse.microprofile.reactive.messaging.Acknowledgment; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +public class StreamAckExamples { + + // tag::message[] + @Incoming("in") + @Outgoing("out") + public Publisher> transform(Multi> stream) { + return stream + .map(message -> message.withPayload(message.getPayload().toUpperCase())); + } + // end::message[] + + // tag::payload[] + @Incoming("in") + @Outgoing("out") + public Publisher transformPayload(Multi stream) { + return stream + // The incoming messages are already acknowledged + .map(String::toUpperCase); + } + // end::payload[] + + // tag::payload-to-multi[] + @Incoming("in") + @Outgoing("out") + // Defaults to pre-processing, but post-processing is also supported + @Acknowledgment(Acknowledgment.Strategy.POST_PROCESSING) + public Multi transformPayload(String one) { + return Multi.createFrom().items(one, one); + } + // end::payload-to-multi[] +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/amqp/customization/ClientProducers.java b/content/docs/smallrye-reactive-messaging/main/_examples/amqp/customization/ClientProducers.java new file mode 100644 index 000000000..4dfa6c829 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/amqp/customization/ClientProducers.java @@ -0,0 +1,34 @@ +package amqp.customization; + +import jakarta.enterprise.inject.Produces; + +import io.smallrye.common.annotation.Identifier; +import io.vertx.amqp.AmqpClientOptions; +import io.vertx.core.net.PemKeyCertOptions; +import io.vertx.core.net.PemTrustOptions; + +public class ClientProducers { + + // tag::named[] + @Produces + @Identifier("my-options") + public AmqpClientOptions getNamedOptions() { + // You can use the produced options to configure the TLS connection + PemKeyCertOptions keycert = new PemKeyCertOptions() + .addCertPath("./tls/tls.crt") + .addKeyPath("./tls/tls.key"); + PemTrustOptions trust = new PemTrustOptions().addCertPath("./tlc/ca.crt"); + + return new AmqpClientOptions() + .setSsl(true) + .setKeyCertOptions(keycert) + .setTrustOptions(trust) + .addEnabledSaslMechanism("EXTERNAL") + .setHostnameVerificationAlgorithm("") // Disable hostname verification + .setConnectTimeout(30000) + .setReconnectInterval(5000) + .setContainerId("my-container"); + } + // end::named[] + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/amqp/customization/MyCbsTokenProvider.java b/content/docs/smallrye-reactive-messaging/main/_examples/amqp/customization/MyCbsTokenProvider.java new file mode 100644 index 000000000..45346773f --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/amqp/customization/MyCbsTokenProvider.java @@ -0,0 +1,32 @@ +package amqp.customization; + +import java.time.OffsetDateTime; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.amqp.AmqpConnectorCommonConfiguration; +import io.smallrye.reactive.messaging.amqp.cbs.CbsToken; +import io.smallrye.reactive.messaging.amqp.cbs.CbsTokenProvider; +import io.smallrye.reactive.messaging.amqp.cbs.DefaultCbsToken; + +// tag::provider[] +@ApplicationScoped +public class MyCbsTokenProvider implements CbsTokenProvider { + + @Override + public Uni getToken(AmqpConnectorCommonConfiguration config) { + String token = fetchTokenFromAuthService(); + return Uni.createFrom().item(new DefaultCbsToken( + token, + "jwt", + OffsetDateTime.now().plusHours(1), + OffsetDateTime.now().plusMinutes(45))); + } + + private String fetchTokenFromAuthService() { + // Retrieve a token from your identity provider + return "my-jwt-token"; + } +} +// end::provider[] diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/amqp/customization/MySasTokenProvider.java b/content/docs/smallrye-reactive-messaging/main/_examples/amqp/customization/MySasTokenProvider.java new file mode 100644 index 000000000..0440f43b5 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/amqp/customization/MySasTokenProvider.java @@ -0,0 +1,29 @@ +package amqp.customization; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.amqp.AmqpConnectorCommonConfiguration; +import io.smallrye.reactive.messaging.amqp.cbs.CbsToken; +import io.smallrye.reactive.messaging.amqp.cbs.CbsTokenProvider; +import io.smallrye.reactive.messaging.amqp.cbs.DefaultCbsToken; + +// tag::sas-provider[] +@ApplicationScoped +public class MySasTokenProvider implements CbsTokenProvider { + + @Override + public Uni getToken(AmqpConnectorCommonConfiguration config) { + String sasToken = generateSasToken(config.getHost()); + return Uni.createFrom().item(new DefaultCbsToken( + sasToken, + "servicebus.windows.net:sastoken", + 3600)); // valid for 1 hour + } + + private String generateSasToken(String host) { + // Build a SAS token for Azure Service Bus + return "SharedAccessSignature sr=..."; + } +} +// end::sas-provider[] diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/amqp/inbound/AmqpMetadataExample.java b/content/docs/smallrye-reactive-messaging/main/_examples/amqp/inbound/AmqpMetadataExample.java new file mode 100644 index 000000000..8683ccbb7 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/amqp/inbound/AmqpMetadataExample.java @@ -0,0 +1,27 @@ +package amqp.inbound; + +import java.util.Optional; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.amqp.IncomingAmqpMetadata; +import io.vertx.core.json.JsonObject; + +public class AmqpMetadataExample { + + public void metadata() { + Message incoming = Message.of(12.0); + // tag::code[] + Optional metadata = incoming.getMetadata(IncomingAmqpMetadata.class); + metadata.ifPresent(meta -> { + String address = meta.getAddress(); + String subject = meta.getSubject(); + boolean durable = meta.isDurable(); + // Use io.vertx.core.json.JsonObject + JsonObject properties = meta.getProperties(); + // ... + }); + // end::code[] + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/amqp/inbound/AmqpPriceConsumer.java b/content/docs/smallrye-reactive-messaging/main/_examples/amqp/inbound/AmqpPriceConsumer.java new file mode 100644 index 000000000..ac877fc2b --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/amqp/inbound/AmqpPriceConsumer.java @@ -0,0 +1,15 @@ +package amqp.inbound; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; + +@ApplicationScoped +public class AmqpPriceConsumer { + + @Incoming("prices") + public void consume(double price) { + // process your price. + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/amqp/inbound/AmqpPriceMessageConsumer.java b/content/docs/smallrye-reactive-messaging/main/_examples/amqp/inbound/AmqpPriceMessageConsumer.java new file mode 100644 index 000000000..1611ebf7d --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/amqp/inbound/AmqpPriceMessageConsumer.java @@ -0,0 +1,21 @@ +package amqp.inbound; + +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; + +@ApplicationScoped +public class AmqpPriceMessageConsumer { + + @Incoming("prices") + public CompletionStage consume(Message price) { + // process your price. + + // Acknowledge the incoming message, marking the AMQP message as `accepted`. + return price.ack(); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/amqp/outbound/AmqpCloudEventMultipleProcessors.java b/content/docs/smallrye-reactive-messaging/main/_examples/amqp/outbound/AmqpCloudEventMultipleProcessors.java new file mode 100644 index 000000000..f87f08a15 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/amqp/outbound/AmqpCloudEventMultipleProcessors.java @@ -0,0 +1,39 @@ +package amqp.outbound; + +import java.net.URI; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.reactive.messaging.ce.OutgoingCloudEventMetadata; + +@ApplicationScoped +public class AmqpCloudEventMultipleProcessors { + + @Incoming("source") + @Outgoing("processed") + public Message process(Message in) { + return in.addMetadata(OutgoingCloudEventMetadata.builder() + .withId("id-" + in.getPayload()) + .withType("greeting") + .build()); + } + + @SuppressWarnings("unchecked") + @Incoming("processed") + @Outgoing("cloud-events") + public Message process2(Message in) { + OutgoingCloudEventMetadata metadata = in + .getMetadata(OutgoingCloudEventMetadata.class) + .orElseGet(() -> OutgoingCloudEventMetadata.builder().build()); + + return in.addMetadata(OutgoingCloudEventMetadata.from(metadata) + .withSource(URI.create("source://me")) + .withSubject("test") + .build()); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/amqp/outbound/AmqpCloudEventProcessor.java b/content/docs/smallrye-reactive-messaging/main/_examples/amqp/outbound/AmqpCloudEventProcessor.java new file mode 100644 index 000000000..3b7a981f8 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/amqp/outbound/AmqpCloudEventProcessor.java @@ -0,0 +1,25 @@ +package amqp.outbound; + +import java.net.URI; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.reactive.messaging.ce.OutgoingCloudEventMetadata; + +@ApplicationScoped +public class AmqpCloudEventProcessor { + + @Outgoing("cloud-events") + public Message toCloudEvents(Message in) { + return in.addMetadata(OutgoingCloudEventMetadata.builder() + .withId("id-" + in.getPayload()) + .withType("greetings") + .withSource(URI.create("http://example.com")) + .withSubject("greeting-message") + .build()); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/amqp/outbound/AmqpOutboundDynamicAddressExample.java b/content/docs/smallrye-reactive-messaging/main/_examples/amqp/outbound/AmqpOutboundDynamicAddressExample.java new file mode 100644 index 000000000..35f848796 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/amqp/outbound/AmqpOutboundDynamicAddressExample.java @@ -0,0 +1,26 @@ +package amqp.outbound; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.amqp.OutgoingAmqpMetadata; + +public class AmqpOutboundDynamicAddressExample { + public Message metadata(Message incoming) { + + // tag::code[] + String addressName = selectAddressFromIncommingMessage(incoming); + OutgoingAmqpMetadata metadata = OutgoingAmqpMetadata.builder() + .withAddress(addressName) + .withDurable(true) + .build(); + + // Create a new message from the `incoming` message + // Add `metadata` to the metadata from the `incoming` message. + return incoming.addMetadata(metadata); + // end::code[] + } + + private String selectAddressFromIncommingMessage(Message incoming) { + return "fake"; + } +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/amqp/outbound/AmqpOutboundMetadataExample.java b/content/docs/smallrye-reactive-messaging/main/_examples/amqp/outbound/AmqpOutboundMetadataExample.java new file mode 100644 index 000000000..3eef8b590 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/amqp/outbound/AmqpOutboundMetadataExample.java @@ -0,0 +1,23 @@ +package amqp.outbound; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.amqp.OutgoingAmqpMetadata; + +public class AmqpOutboundMetadataExample { + + public Message metadata(Message incoming) { + + // tag::code[] + OutgoingAmqpMetadata metadata = OutgoingAmqpMetadata.builder() + .withDurable(true) + .withSubject("my-subject") + .build(); + + // Create a new message from the `incoming` message + // Add `metadata` to the metadata from the `incoming` message. + return incoming.addMetadata(metadata); + // end::code[] + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/amqp/outbound/AmqpPriceMessageProducer.java b/content/docs/smallrye-reactive-messaging/main/_examples/amqp/outbound/AmqpPriceMessageProducer.java new file mode 100644 index 000000000..418a901b3 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/amqp/outbound/AmqpPriceMessageProducer.java @@ -0,0 +1,26 @@ +package amqp.outbound; + +import java.time.Duration; +import java.util.Random; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +@ApplicationScoped +public class AmqpPriceMessageProducer { + + private Random random = new Random(); + + @Outgoing("prices") + public Multi> generate() { + // Build an infinite stream of random prices + // It emits a price every second + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .map(x -> Message.of(random.nextDouble())); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/amqp/outbound/AmqpPriceProducer.java b/content/docs/smallrye-reactive-messaging/main/_examples/amqp/outbound/AmqpPriceProducer.java new file mode 100644 index 000000000..0e760b71f --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/amqp/outbound/AmqpPriceProducer.java @@ -0,0 +1,25 @@ +package amqp.outbound; + +import java.time.Duration; +import java.util.Random; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +@ApplicationScoped +public class AmqpPriceProducer { + + private Random random = new Random(); + + @Outgoing("prices") + public Multi generate() { + // Build an infinite stream of random prices + // It emits a price every second + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .map(x -> random.nextDouble()); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/amqp/outbound/AmqpReplier.java b/content/docs/smallrye-reactive-messaging/main/_examples/amqp/outbound/AmqpReplier.java new file mode 100644 index 000000000..7f5236aba --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/amqp/outbound/AmqpReplier.java @@ -0,0 +1,20 @@ +package amqp.outbound; + +import java.util.Random; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +@ApplicationScoped +public class AmqpReplier { + + Random rand = new Random(); + + @Incoming("request") + @Outgoing("reply") + String handleRequest(String request) { + return request + "-" + rand.nextInt(100); + } +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/amqp/outbound/AmqpReplierWithMetadata.java b/content/docs/smallrye-reactive-messaging/main/_examples/amqp/outbound/AmqpReplierWithMetadata.java new file mode 100644 index 000000000..c798cd136 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/amqp/outbound/AmqpReplierWithMetadata.java @@ -0,0 +1,31 @@ +package amqp.outbound; + +import java.util.Random; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.reactive.messaging.amqp.IncomingAmqpMetadata; +import io.smallrye.reactive.messaging.amqp.OutgoingAmqpMetadata; + +@ApplicationScoped +public class AmqpReplierWithMetadata { + + Random rand = new Random(); + + @Incoming("request") + @Outgoing("reply") + Message handleRequest(Message request) { + IncomingAmqpMetadata incoming = request.getMetadata(IncomingAmqpMetadata.class) + .orElseThrow(); + OutgoingAmqpMetadata outMeta = OutgoingAmqpMetadata.builder() + .withAddress(incoming.getReplyTo()) + .withCorrelationId(incoming.getId()) + .build(); + String reply = request.getPayload() + "-" + rand.nextInt(100); + return request.withPayload(reply).addMetadata(outMeta); + } +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/amqp/outbound/AmqpRequestReplyEmitter.java b/content/docs/smallrye-reactive-messaging/main/_examples/amqp/outbound/AmqpRequestReplyEmitter.java new file mode 100644 index 000000000..f1d5285c8 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/amqp/outbound/AmqpRequestReplyEmitter.java @@ -0,0 +1,21 @@ +package amqp.outbound; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; + +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.amqp.reply.AmqpRequestReply; + +@ApplicationScoped +public class AmqpRequestReplyEmitter { + + @Inject + @Channel("my-request") + AmqpRequestReply quoteRequest; + + public Uni requestQuote(String request) { + return quoteRequest.request(request); + } +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/amqp/outbound/AmqpRequestReplyMessageEmitter.java b/content/docs/smallrye-reactive-messaging/main/_examples/amqp/outbound/AmqpRequestReplyMessageEmitter.java new file mode 100644 index 000000000..de5f1bbaf --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/amqp/outbound/AmqpRequestReplyMessageEmitter.java @@ -0,0 +1,22 @@ +package amqp.outbound; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.amqp.reply.AmqpRequestReply; + +@ApplicationScoped +public class AmqpRequestReplyMessageEmitter { + + @Inject + @Channel("my-request") + AmqpRequestReply quoteRequest; + + public Uni> requestMessage(String request) { + return quoteRequest.request(Message.of(request)); + } +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/amqp/outbound/AmqpRequestReplyMultiEmitter.java b/content/docs/smallrye-reactive-messaging/main/_examples/amqp/outbound/AmqpRequestReplyMultiEmitter.java new file mode 100644 index 000000000..25d776606 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/amqp/outbound/AmqpRequestReplyMultiEmitter.java @@ -0,0 +1,21 @@ +package amqp.outbound; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; + +import io.smallrye.mutiny.Multi; +import io.smallrye.reactive.messaging.amqp.reply.AmqpRequestReply; + +@ApplicationScoped +public class AmqpRequestReplyMultiEmitter { + + @Inject + @Channel("my-request") + AmqpRequestReply quoteRequest; + + public Multi requestQuote(String request) { + return quoteRequest.requestMulti(request).select().first(5); + } +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/amqp/outbound/MyAmqpReplyFailureHandler.java b/content/docs/smallrye-reactive-messaging/main/_examples/amqp/outbound/MyAmqpReplyFailureHandler.java new file mode 100644 index 000000000..77b1d2d14 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/amqp/outbound/MyAmqpReplyFailureHandler.java @@ -0,0 +1,24 @@ +package amqp.outbound; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.smallrye.common.annotation.Identifier; +import io.smallrye.reactive.messaging.amqp.AmqpMessage; +import io.smallrye.reactive.messaging.amqp.reply.ReplyFailureHandler; + +@ApplicationScoped +@Identifier("my-reply-error") +public class MyAmqpReplyFailureHandler implements ReplyFailureHandler { + + @Override + public Throwable handleReply(AmqpMessage replyMessage) { + var properties = replyMessage.getApplicationProperties(); + if (properties != null) { + String error = properties.getString("REPLY_ERROR"); + if (error != null) { + return new IllegalArgumentException(error); + } + } + return null; + } +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/beans/Chain.java b/content/docs/smallrye-reactive-messaging/main/_examples/beans/Chain.java new file mode 100644 index 000000000..8a44b6d55 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/beans/Chain.java @@ -0,0 +1,27 @@ +package beans; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +public class Chain { + + // tag::chain[] + @Outgoing("source") + public Multi generate() { + return Multi.createFrom().items("Hello", "from", "reactive", "messaging"); + } + + @Incoming("source") + @Outgoing("sink") + public String process(String in) { + return in.toUpperCase(); + } + + @Incoming("sink") + public void consume(String processed) { + System.out.println(processed); + } + // end::chain[] +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/beans/MessageProcessingBean.java b/content/docs/smallrye-reactive-messaging/main/_examples/beans/MessageProcessingBean.java new file mode 100644 index 000000000..edfbf0df4 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/beans/MessageProcessingBean.java @@ -0,0 +1,20 @@ +package beans; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +@ApplicationScoped +public class MessageProcessingBean { + + @Incoming("consumed-channel") + @Outgoing("populated-channel") + public Message process(Message in) { + // Process the payload + String payload = in.getPayload().toUpperCase(); + // Create a new message from `in` and just update the payload + return in.withPayload(payload); + } +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/beans/PayloadProcessingBean.java b/content/docs/smallrye-reactive-messaging/main/_examples/beans/PayloadProcessingBean.java new file mode 100644 index 000000000..b7fa6b0ae --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/beans/PayloadProcessingBean.java @@ -0,0 +1,16 @@ +package beans; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +@ApplicationScoped +public class PayloadProcessingBean { + + @Incoming("consumed-channel") + @Outgoing("populated-channel") + public String process(String in) { + return in.toUpperCase(); + } +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/broadcast/BroadcastExamples.java b/content/docs/smallrye-reactive-messaging/main/_examples/broadcast/BroadcastExamples.java new file mode 100644 index 000000000..53a8bb160 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/broadcast/BroadcastExamples.java @@ -0,0 +1,29 @@ +package broadcast; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.reactive.messaging.annotations.Broadcast; + +public class BroadcastExamples { + + // tag::chain[] + @Incoming("in") + @Outgoing("out") + @Broadcast + public int increment(int i) { + return i + 1; + } + + @Incoming("out") + public void consume1(int i) { + //... + } + + @Incoming("out") + public void consume2(int i) { + //... + } + // end::chain[] + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/broadcast/BroadcastWithCountExamples.java b/content/docs/smallrye-reactive-messaging/main/_examples/broadcast/BroadcastWithCountExamples.java new file mode 100644 index 000000000..e227d7747 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/broadcast/BroadcastWithCountExamples.java @@ -0,0 +1,29 @@ +package broadcast; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.reactive.messaging.annotations.Broadcast; + +public class BroadcastWithCountExamples { + + // tag::chain[] + @Incoming("in") + @Outgoing("out") + @Broadcast(2) + public int increment(int i) { + return i + 1; + } + + @Incoming("out") + public void consume1(int i) { + //... + } + + @Incoming("out") + public void consume2(int i) { + //... + } + // end::chain[] + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/camel/api/CamelApi.java b/content/docs/smallrye-reactive-messaging/main/_examples/camel/api/CamelApi.java new file mode 100644 index 000000000..f96087599 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/camel/api/CamelApi.java @@ -0,0 +1,94 @@ +package camel.api; + +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +import org.apache.camel.BindToRegistry; +import org.apache.camel.CamelContext; +import org.apache.camel.Exchange; +import org.apache.camel.builder.LambdaRouteBuilder; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.component.reactive.streams.api.CamelReactiveStreamsService; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; + +public class CamelApi { + + // tag::reactive[] + @Inject + CamelReactiveStreamsService reactiveStreamsService; + // end::reactive[] + + // tag::source[] + @Outgoing("camel") + public Publisher retrieveDataFromCamelRoute() { + return reactiveStreamsService.from("seda:camel"); + } + // end::source[] + + // tag::source-route-builder[] + @Singleton + static class MyRouteBuilder extends RouteBuilder { + @Inject + CamelReactiveStreamsService reactiveStreamsService; + + @Outgoing("sink") + public Publisher getDataFromCamelRoute() { + return reactiveStreamsService.fromStream("my-stream", String.class); + } + + @Override + public void configure() { + from("seda:camel").process( + exchange -> exchange.getMessage().setBody(exchange.getIn().getBody(String.class).toUpperCase())) + .to("reactive-streams:my-stream"); + } + } + // end::source-route-builder[] + + // tag::source-lambda-route-builder[] + @ApplicationScoped + static class MyLambdaRouteBuilder { + @Inject + CamelReactiveStreamsService reactiveStreamsService; + + @Outgoing("sink") + public Publisher getDataFromCamelRoute() { + return reactiveStreamsService.fromStream("my-stream", String.class); + } + + @Produces + @BindToRegistry + public LambdaRouteBuilder route() { + return rb -> rb.from("seda:camel").process( + exchange -> exchange.getMessage().setBody(exchange.getIn().getBody(String.class).toUpperCase())) + .to("reactive-streams:my-stream"); + } + } + // end::source-lambda-route-builder[] + + // tag::sink[] + @Incoming("to-camel") + public Subscriber sendDataToCamelRoute() { + return reactiveStreamsService.subscriber("file:./target?fileName=values.txt&fileExist=append", + String.class); + } + // end::sink[] + + // tag::producer[] + @Inject + CamelContext camel; + + @Incoming("to-camel") + public CompletionStage sink(String value) { + return camel.createProducerTemplate() + .asyncSendBody("file:./target?fileName=values.txt&fileExist=append", value).thenApply(x -> null); + } + // end::producer[] +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/camel/inbound/CamelFileConsumer.java b/content/docs/smallrye-reactive-messaging/main/_examples/camel/inbound/CamelFileConsumer.java new file mode 100644 index 000000000..21c206129 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/camel/inbound/CamelFileConsumer.java @@ -0,0 +1,20 @@ +package camel.inbound; + +import java.io.File; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.apache.camel.component.file.GenericFile; +import org.eclipse.microprofile.reactive.messaging.Incoming; + +@ApplicationScoped +public class CamelFileConsumer { + + @Incoming("files") + public void consume(GenericFile gf) { + File file = gf.getFile(); + // process the file + + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/camel/inbound/CamelFileMessageConsumer.java b/content/docs/smallrye-reactive-messaging/main/_examples/camel/inbound/CamelFileMessageConsumer.java new file mode 100644 index 000000000..56cbc8559 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/camel/inbound/CamelFileMessageConsumer.java @@ -0,0 +1,23 @@ +package camel.inbound; + +import java.io.File; +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.apache.camel.component.file.GenericFile; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; + +@ApplicationScoped +public class CamelFileMessageConsumer { + + @Incoming("files") + public CompletionStage consume(Message> msg) { + File file = msg.getPayload().getFile(); + // process the file + + return msg.ack(); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/camel/inbound/IncomingCamelMetadataExample.java b/content/docs/smallrye-reactive-messaging/main/_examples/camel/inbound/IncomingCamelMetadataExample.java new file mode 100644 index 000000000..63f0a4f3d --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/camel/inbound/IncomingCamelMetadataExample.java @@ -0,0 +1,29 @@ +package camel.inbound; + +import java.io.File; +import java.util.Optional; +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.apache.camel.Exchange; +import org.apache.camel.component.file.GenericFile; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.camel.IncomingExchangeMetadata; + +@ApplicationScoped +public class IncomingCamelMetadataExample { + + @Incoming("files") + public CompletionStage consume(Message> msg) { + Optional metadata = msg.getMetadata(IncomingExchangeMetadata.class); + if (metadata.isPresent()) { + // Retrieve the camel exchange: + Exchange exchange = metadata.get().getExchange(); + } + return msg.ack(); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/camel/outbound/CamelOutboundMetadataExample.java b/content/docs/smallrye-reactive-messaging/main/_examples/camel/outbound/CamelOutboundMetadataExample.java new file mode 100644 index 000000000..3261e7371 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/camel/outbound/CamelOutboundMetadataExample.java @@ -0,0 +1,31 @@ +package camel.outbound; + +import java.time.Duration; +import java.util.Random; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; +import io.smallrye.reactive.messaging.camel.OutgoingExchangeMetadata; + +@ApplicationScoped +public class CamelOutboundMetadataExample { + + private Random random = new Random(); + + @Outgoing("prices") + public Multi> generate() { + // tag::code[] + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .map(x -> random.nextDouble()) + .map(p -> Double.toString(p)) + .map(s -> Message.of(s) + .addMetadata(new OutgoingExchangeMetadata() + .putProperty("my-property", "my-value"))); + // end::code[] + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/camel/outbound/CamelPriceMessageProducer.java b/content/docs/smallrye-reactive-messaging/main/_examples/camel/outbound/CamelPriceMessageProducer.java new file mode 100644 index 000000000..5a0202608 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/camel/outbound/CamelPriceMessageProducer.java @@ -0,0 +1,27 @@ +package camel.outbound; + +import java.time.Duration; +import java.util.Random; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +@ApplicationScoped +public class CamelPriceMessageProducer { + + private Random random = new Random(); + + @Outgoing("prices") + public Multi> generate() { + // Build an infinite stream of random prices + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .map(x -> random.nextDouble()) + .map(p -> Double.toString(p)) + .map(Message::of); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/camel/outbound/CamelPriceProducer.java b/content/docs/smallrye-reactive-messaging/main/_examples/camel/outbound/CamelPriceProducer.java new file mode 100644 index 000000000..30392e08e --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/camel/outbound/CamelPriceProducer.java @@ -0,0 +1,26 @@ +package camel.outbound; + +import java.time.Duration; +import java.util.Random; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +@ApplicationScoped +public class CamelPriceProducer { + + private Random random = new Random(); + + @Outgoing("prices") + public Multi generate() { + // Build an infinite stream of random prices + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .onOverflow().drop() + .map(x -> random.nextDouble()) + .map(p -> Double.toString(p)); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/camel/processor/CamelProcessor.java b/content/docs/smallrye-reactive-messaging/main/_examples/camel/processor/CamelProcessor.java new file mode 100644 index 000000000..434b12911 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/camel/processor/CamelProcessor.java @@ -0,0 +1,18 @@ +package camel.processor; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +@ApplicationScoped +public class CamelProcessor { + + @Incoming("mynatssubject") + @Outgoing("mykafkatopic") + public byte[] process(byte[] message) { + // do some logic + return message; + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/connectors/MyAckHandler.java b/content/docs/smallrye-reactive-messaging/main/_examples/connectors/MyAckHandler.java new file mode 100644 index 000000000..dc1d1d146 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/connectors/MyAckHandler.java @@ -0,0 +1,25 @@ +package connectors; + +import java.util.concurrent.CompletionStage; + +import connectors.api.BrokerClient; +import io.smallrye.mutiny.Uni; + +public class MyAckHandler { + + private final BrokerClient client; + + static MyAckHandler create(BrokerClient client) { + return new MyAckHandler(client); + } + + public MyAckHandler(BrokerClient client) { + this.client = client; + } + + public CompletionStage handle(MyMessage msg) { + return Uni.createFrom().completionStage(client.ack(msg.getConsumedMessage())) + .emitOn(msg::runOnMessageContext) + .subscribeAsCompletionStage(); + } +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/connectors/MyConnector.java b/content/docs/smallrye-reactive-messaging/main/_examples/connectors/MyConnector.java new file mode 100644 index 000000000..9dd75beb8 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/connectors/MyConnector.java @@ -0,0 +1,75 @@ +package connectors; + +import static io.smallrye.reactive.messaging.annotations.ConnectorAttribute.Direction.INCOMING; +import static io.smallrye.reactive.messaging.annotations.ConnectorAttribute.Direction.INCOMING_AND_OUTGOING; +import static io.smallrye.reactive.messaging.annotations.ConnectorAttribute.Direction.OUTGOING; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Flow; + +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.spi.Connector; + +import connectors.api.BrokerClient; +import io.smallrye.reactive.messaging.annotations.ConnectorAttribute; +import io.smallrye.reactive.messaging.connector.InboundConnector; +import io.smallrye.reactive.messaging.connector.OutboundConnector; +import io.smallrye.reactive.messaging.providers.connectors.ExecutionHolder; +import io.vertx.mutiny.core.Vertx; + +@ApplicationScoped +@Connector(MyConnector.CONNECTOR_NAME) +@ConnectorAttribute(name = "client-id", type = "string", direction = INCOMING_AND_OUTGOING, description = "The client id ", mandatory = true) +@ConnectorAttribute(name = "buffer-size", type = "int", direction = INCOMING, description = "The size buffer of incoming messages waiting to be processed", defaultValue = "128") +@ConnectorAttribute(name = "topic", type = "string", direction = OUTGOING, description = "The default topic to send the messages, defaults to channel name if not set") +@ConnectorAttribute(name = "maxPendingMessages", type = "int", direction = OUTGOING, description = "The maximum size of a queue holding pending messages", defaultValue = "1000") +@ConnectorAttribute(name = "waitForWriteCompletion", type = "boolean", direction = OUTGOING, description = "Whether the outgoing channel waits for the write completion", defaultValue = "true") +public class MyConnector implements InboundConnector, OutboundConnector { + + public static final String CONNECTOR_NAME = "smallrye-my-connector"; + + @Inject + ExecutionHolder executionHolder; + + Vertx vertx; + + List incomingChannels = new CopyOnWriteArrayList<>(); + List outgoingChannels = new CopyOnWriteArrayList<>(); + + @PostConstruct + void init() { + this.vertx = executionHolder.vertx(); + } + + @Override + public Flow.Publisher> getPublisher(Config config) { + MyConnectorIncomingConfiguration ic = new MyConnectorIncomingConfiguration(config); + String channelName = ic.getChannel(); + String clientId = ic.getClientId(); + int bufferSize = ic.getBufferSize(); + // ... + BrokerClient client = BrokerClient.create(clientId); + MyIncomingChannel channel = new MyIncomingChannel(vertx, ic, client); + incomingChannels.add(channel); + return channel.getStream(); + } + + @Override + public Flow.Subscriber> getSubscriber(Config config) { + MyConnectorOutgoingConfiguration oc = new MyConnectorOutgoingConfiguration(config); + String channelName = oc.getChannel(); + String clientId = oc.getClientId(); + int pendingMessages = oc.getMaxPendingMessages(); + // ... + BrokerClient client = BrokerClient.create(clientId); + MyOutgoingChannel channel = new MyOutgoingChannel(vertx, oc, client); + outgoingChannels.add(channel); + return channel.getSubscriber(); + } +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/connectors/MyConnectorWithPartials.java b/content/docs/smallrye-reactive-messaging/main/_examples/connectors/MyConnectorWithPartials.java new file mode 100644 index 000000000..55d0cc2fa --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/connectors/MyConnectorWithPartials.java @@ -0,0 +1,106 @@ +package connectors; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Flow; + +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.spi.Connector; + +import connectors.api.BrokerClient; +import io.smallrye.reactive.messaging.connector.InboundConnector; +import io.smallrye.reactive.messaging.connector.OutboundConnector; +import io.smallrye.reactive.messaging.health.HealthReport; +import io.smallrye.reactive.messaging.health.HealthReporter; +import io.smallrye.reactive.messaging.providers.connectors.ExecutionHolder; +import io.vertx.mutiny.core.Vertx; + +// tag::health-report[] +@ApplicationScoped +@Connector(MyConnectorWithPartials.CONNECTOR_NAME) +public class MyConnectorWithPartials implements InboundConnector, OutboundConnector, HealthReporter { + + public static final String CONNECTOR_NAME = "smallrye-my-connector"; + + List incomingChannels = new CopyOnWriteArrayList<>(); + List outgoingChannels = new CopyOnWriteArrayList<>(); + + @Override + public HealthReport getReadiness() { + HealthReport.HealthReportBuilder builder = HealthReport.builder(); + for (MyIncomingChannel channel : incomingChannels) { + builder.add(channel.getChannel(), true); + } + for (MyOutgoingChannel channel : outgoingChannels) { + builder.add(channel.getChannel(), true); + } + return builder.build(); + } + + @Override + public HealthReport getLiveness() { + HealthReport.HealthReportBuilder builder = HealthReport.builder(); + for (MyIncomingChannel channel : incomingChannels) { + builder.add(channel.getChannel(), true); + } + for (MyOutgoingChannel channel : outgoingChannels) { + builder.add(channel.getChannel(), true); + } + return builder.build(); + } + + @Override + public HealthReport getStartup() { + HealthReport.HealthReportBuilder builder = HealthReport.builder(); + for (MyIncomingChannel channel : incomingChannels) { + builder.add(channel.getChannel(), true); + } + for (MyOutgoingChannel channel : outgoingChannels) { + builder.add(channel.getChannel(), true); + } + return builder.build(); + } + + // end::health-report[] + + @Inject + ExecutionHolder executionHolder; + + Vertx vertx; + + @PostConstruct + void init() { + this.vertx = executionHolder.vertx(); + } + + @Override + public Flow.Publisher> getPublisher(Config config) { + MyConnectorIncomingConfiguration ic = new MyConnectorIncomingConfiguration(config); + String channelName = ic.getChannel(); + String clientId = ic.getClientId(); + int bufferSize = ic.getBufferSize(); + // ... + BrokerClient client = BrokerClient.create(clientId); + MyIncomingChannel channel = new MyIncomingChannel(vertx, ic, client); + incomingChannels.add(channel); + return channel.getStream(); + } + + @Override + public Flow.Subscriber> getSubscriber(Config config) { + MyConnectorOutgoingConfiguration oc = new MyConnectorOutgoingConfiguration(config); + String channelName = oc.getChannel(); + String clientId = oc.getClientId(); + int pendingMessages = oc.getMaxPendingMessages(); + // ... + BrokerClient client = BrokerClient.create(clientId); + MyOutgoingChannel channel = new MyOutgoingChannel(vertx, oc, client); + outgoingChannels.add(channel); + return channel.getSubscriber(); + } +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/connectors/MyFailureHandler.java b/content/docs/smallrye-reactive-messaging/main/_examples/connectors/MyFailureHandler.java new file mode 100644 index 000000000..f41b78055 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/connectors/MyFailureHandler.java @@ -0,0 +1,27 @@ +package connectors; + +import java.util.concurrent.CompletionStage; + +import org.eclipse.microprofile.reactive.messaging.Metadata; + +import connectors.api.BrokerClient; +import io.smallrye.mutiny.Uni; + +public class MyFailureHandler { + + private final BrokerClient client; + + static MyFailureHandler create(BrokerClient client) { + return new MyFailureHandler(client); + } + + public MyFailureHandler(BrokerClient client) { + this.client = client; + } + + public CompletionStage handle(MyMessage msg, Throwable reason, Metadata metadata) { + return Uni.createFrom().completionStage(() -> client.reject(msg.getConsumedMessage(), reason.getMessage())) + .emitOn(msg::runOnMessageContext) + .subscribeAsCompletionStage(); + } +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/connectors/MyIncomingChannel.java b/content/docs/smallrye-reactive-messaging/main/_examples/connectors/MyIncomingChannel.java new file mode 100644 index 000000000..a7db74ccc --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/connectors/MyIncomingChannel.java @@ -0,0 +1,57 @@ +package connectors; + +import java.util.concurrent.Flow; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import connectors.api.BrokerClient; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.health.HealthReport; +import io.vertx.core.internal.VertxInternal; +import io.vertx.mutiny.core.Context; +import io.vertx.mutiny.core.Vertx; + +public class MyIncomingChannel { + + private final String channel; + private final BrokerClient client; + private final Context context; + private final MyAckHandler ackHandler; + private final MyFailureHandler failureHandler; + private final AtomicBoolean closed = new AtomicBoolean(false); + private Flow.Publisher> stream; + + public MyIncomingChannel(Vertx vertx, MyConnectorIncomingConfiguration cfg, BrokerClient client) { + // create and configure the client with MyConnectorIncomingConfiguration + this.channel = cfg.getChannel(); + this.client = client; + this.context = Context.newInstance(((VertxInternal) vertx.getDelegate()).createEventLoopContext()); + this.ackHandler = MyAckHandler.create(this.client); + this.failureHandler = MyFailureHandler.create(this.client); + this.stream = Multi.createBy().repeating() + .uni(() -> Uni.createFrom().completionStage(this.client.poll())) + .until(__ -> closed.get()) + .emitOn(context::runOnContext) + .map(consumed -> new MyMessage<>(consumed, ackHandler, failureHandler)); + } + + public String getChannel() { + return channel; + } + + public Flow.Publisher> getStream() { + return this.stream; + } + + public void close() { + closed.compareAndSet(false, true); + client.close(); + } + + void isReady(HealthReport.HealthReportBuilder healthReportBuilder) { + + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/connectors/MyIncomingChannelWithPartials.java b/content/docs/smallrye-reactive-messaging/main/_examples/connectors/MyIncomingChannelWithPartials.java new file mode 100644 index 000000000..fb8417101 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/connectors/MyIncomingChannelWithPartials.java @@ -0,0 +1,79 @@ +package connectors; + +import java.util.concurrent.Flow; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import connectors.api.BrokerClient; +import connectors.api.ConsumedMessage; +import connectors.tracing.MyOpenTelemetryInstrumenter; +import connectors.tracing.MyTrace; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.health.HealthReport; +import io.smallrye.reactive.messaging.tracing.TracingUtils; +import io.vertx.core.internal.VertxInternal; +import io.vertx.mutiny.core.Context; +import io.vertx.mutiny.core.Vertx; + +public class MyIncomingChannelWithPartials { + + private final String channel; + private final BrokerClient client; + private final Context context; + private final MyAckHandler ackHandler; + private final MyFailureHandler failureHandler; + private final AtomicBoolean closed = new AtomicBoolean(false); + private final boolean tracingEnabled; + private Flow.Publisher> stream; + + public MyIncomingChannelWithPartials(Vertx vertx, MyConnectorIncomingConfiguration cfg, BrokerClient client) { + // create and configure the client with MyConnectorIncomingConfiguration + this.channel = cfg.getChannel(); + this.client = client; + this.context = Context.newInstance(((VertxInternal) vertx.getDelegate()).createEventLoopContext()); + this.ackHandler = MyAckHandler.create(this.client); + this.failureHandler = MyFailureHandler.create(this.client); + this.tracingEnabled = true; + // tag::incoming-tracing[] + Multi> receiveMulti = Multi.createBy().repeating() + .uni(() -> Uni.createFrom().completionStage(this.client.poll())) + .until(__ -> closed.get()) + .emitOn(context::runOnContext) + .map(consumed -> new MyMessage<>(consumed, ackHandler, failureHandler)); + + Instrumenter instrumenter = MyOpenTelemetryInstrumenter.createInstrumenter(true); + if (tracingEnabled) { + receiveMulti = receiveMulti.map(message -> { + ConsumedMessage consumedMessage = message.getMetadata(MyIncomingMetadata.class).get().getCustomMessage(); + return TracingUtils.traceIncoming(instrumenter, message, new MyTrace.Builder() + .withClientId(consumedMessage.clientId()) + .withTopic(consumedMessage.topic()) + .withProperties(consumedMessage.properties()) + .build()); + }); + } + // end::incoming-tracing[] + this.stream = receiveMulti; + } + + public String getChannel() { + return channel; + } + + public Flow.Publisher> getStream() { + return this.stream; + } + + public void close() { + closed.compareAndSet(false, true); + client.close(); + } + + void isReady(HealthReport.HealthReportBuilder healthReportBuilder) { + + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/connectors/MyIncomingMetadata.java b/content/docs/smallrye-reactive-messaging/main/_examples/connectors/MyIncomingMetadata.java new file mode 100644 index 000000000..c288085be --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/connectors/MyIncomingMetadata.java @@ -0,0 +1,34 @@ +package connectors; + +import java.util.Map; + +import connectors.api.ConsumedMessage; + +public class MyIncomingMetadata { + + private final ConsumedMessage msg; + + public MyIncomingMetadata(ConsumedMessage msg) { + this.msg = msg; + } + + public ConsumedMessage getCustomMessage() { + return msg; + } + + public T getBody() { + return msg.body(); + } + + public String getKey() { + return msg.key(); + } + + public long getTimestamp() { + return msg.timestamp(); + } + + public Map getProperties() { + return msg.properties(); + } +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/connectors/MyMessage.java b/content/docs/smallrye-reactive-messaging/main/_examples/connectors/MyMessage.java new file mode 100644 index 000000000..cd432f0f3 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/connectors/MyMessage.java @@ -0,0 +1,51 @@ +package connectors; + +import java.util.concurrent.CompletionStage; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Metadata; + +import connectors.api.ConsumedMessage; +import io.smallrye.reactive.messaging.providers.locals.ContextAwareMessage; + +public class MyMessage implements Message, ContextAwareMessage { + + private final T payload; + private final Metadata metadata; + private final MyAckHandler ackHandler; + + private final MyFailureHandler nackHandler; + private ConsumedMessage consumed; + + public MyMessage(ConsumedMessage message, MyAckHandler ackHandler, MyFailureHandler nackHandler) { + this.consumed = message; + this.payload = message.body(); + this.ackHandler = ackHandler; + this.nackHandler = nackHandler; + this.metadata = ContextAwareMessage.captureContextMetadata(new MyIncomingMetadata<>(message)); + } + + public ConsumedMessage getConsumedMessage() { + return consumed; + } + + @Override + public T getPayload() { + return payload; + } + + @Override + public Metadata getMetadata() { + return metadata; + } + + @Override + public CompletionStage ack() { + return ackHandler.handle(this); + } + + @Override + public CompletionStage nack(Throwable reason, Metadata nackMetadata) { + return nackHandler.handle(this, reason, nackMetadata); + } +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/connectors/MyMessageConverter.java b/content/docs/smallrye-reactive-messaging/main/_examples/connectors/MyMessageConverter.java new file mode 100644 index 000000000..6f9f180e9 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/connectors/MyMessageConverter.java @@ -0,0 +1,27 @@ +package connectors; + +import java.lang.reflect.Type; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import connectors.api.ConsumedMessage; +import io.smallrye.reactive.messaging.MessageConverter; +import io.smallrye.reactive.messaging.providers.helpers.TypeUtils; + +@ApplicationScoped +public class MyMessageConverter implements MessageConverter { + @Override + public boolean canConvert(Message in, Type target) { + return TypeUtils.isAssignable(target, ConsumedMessage.class) + && in.getMetadata(MyIncomingMetadata.class).isPresent(); + } + + @Override + public Message convert(Message in, Type target) { + return in.withPayload(in.getMetadata(MyIncomingMetadata.class) + .map(MyIncomingMetadata::getCustomMessage) + .orElse(null)); + } +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/connectors/MyOutgoingChannel.java b/content/docs/smallrye-reactive-messaging/main/_examples/connectors/MyOutgoingChannel.java new file mode 100644 index 000000000..013defda1 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/connectors/MyOutgoingChannel.java @@ -0,0 +1,49 @@ +package connectors; + +import java.util.concurrent.Flow; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import connectors.api.BrokerClient; +import connectors.api.SendMessage; +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.providers.helpers.MultiUtils; +import io.vertx.mutiny.core.Vertx; + +public class MyOutgoingChannel { + private final String channel; + private Flow.Subscriber> subscriber; + private final BrokerClient client; + private final String topic; + + public MyOutgoingChannel(Vertx vertx, MyConnectorOutgoingConfiguration oc, BrokerClient client) { + this.channel = oc.getChannel(); + this.client = client; + this.topic = oc.getTopic().orElse(oc.getChannel()); + this.subscriber = MultiUtils.via(multi -> multi.call(m -> publishMessage(this.client, m))); + } + + private Uni publishMessage(BrokerClient client, Message m) { + // construct the outgoing message + SendMessage sendMessage = new SendMessage(); + Object payload = m.getPayload(); + sendMessage.setPayload(payload); + sendMessage.setTopic(topic); + m.getMetadata(MyOutgoingMetadata.class).ifPresent(out -> { + sendMessage.setTopic(out.getTopic()); + sendMessage.setKey(out.getKey()); + //... + }); + return Uni.createFrom().completionStage(() -> client.send(sendMessage)) + .onItem().transformToUni(receipt -> Uni.createFrom().completionStage(m.ack())) + .onFailure().recoverWithUni(t -> Uni.createFrom().completionStage(m.nack(t))); + } + + public Flow.Subscriber> getSubscriber() { + return this.subscriber; + } + + public String getChannel() { + return this.channel; + } +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/connectors/MyOutgoingChannelWithPartials.java b/content/docs/smallrye-reactive-messaging/main/_examples/connectors/MyOutgoingChannelWithPartials.java new file mode 100644 index 000000000..2ba3d9e59 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/connectors/MyOutgoingChannelWithPartials.java @@ -0,0 +1,105 @@ +package connectors; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.Flow; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import connectors.api.BrokerClient; +import connectors.api.SendMessage; +import connectors.tracing.MyOpenTelemetryInstrumenter; +import connectors.tracing.MyTrace; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.providers.helpers.MultiUtils; +import io.smallrye.reactive.messaging.providers.helpers.SenderProcessor; +import io.smallrye.reactive.messaging.tracing.TracingUtils; +import io.vertx.mutiny.core.Vertx; + +public class MyOutgoingChannelWithPartials { + private SenderProcessor processor; + private Flow.Subscriber> subscriber; + private final BrokerClient client; + private final String topic; + private final boolean tracingEnabled; + + private Instrumenter instrumenter; + + public MyOutgoingChannelWithPartials(Vertx vertx, MyConnectorOutgoingConfiguration oc, BrokerClient client) { + this.client = client; + this.topic = oc.getTopic().orElse(oc.getChannel()); + this.tracingEnabled = true; + // tag::sender-processor[] + long requests = oc.getMaxPendingMessages(); + boolean waitForWriteCompletion = oc.getWaitForWriteCompletion(); + if (requests <= 0) { + requests = Long.MAX_VALUE; + } + this.processor = new SenderProcessor(requests, waitForWriteCompletion, m -> publishMessage(client, m)); + this.subscriber = MultiUtils.via(processor, m -> m.onFailure().invoke(f -> { + // log the failure + })); + // end::sender-processor[] + instrumenter = MyOpenTelemetryInstrumenter.createInstrumenter(false); + + } + + // tag::send-message[] + private Uni publishMessage(BrokerClient client, Message message) { + // construct the outgoing message + SendMessage sendMessage; + Object payload = message.getPayload(); + if (payload instanceof SendMessage) { + sendMessage = (SendMessage) message.getPayload(); + } else { + sendMessage = new SendMessage(); + sendMessage.setPayload(payload); + sendMessage.setTopic(topic); + message.getMetadata(MyOutgoingMetadata.class).ifPresent(out -> { + sendMessage.setTopic(out.getTopic()); + sendMessage.setKey(out.getKey()); + //... + }); + } + return Uni.createFrom().completionStage(() -> client.send(sendMessage)) + .onItem().transformToUni(receipt -> Uni.createFrom().completionStage(message.ack())) + .onFailure().recoverWithUni(t -> Uni.createFrom().completionStage(message.nack(t))); + } + // end::send-message[] + + // tag::outgoing-tracing[] + private Uni publishMessageWithTracing(BrokerClient client, Message message) { + // construct the outgoing message + SendMessage sendMessage; + Object payload = message.getPayload(); + if (payload instanceof SendMessage) { + sendMessage = (SendMessage) message.getPayload(); + } else { + sendMessage = new SendMessage(); + sendMessage.setPayload(payload); + sendMessage.setTopic(topic); + message.getMetadata(MyOutgoingMetadata.class).ifPresent(out -> { + sendMessage.setTopic(out.getTopic()); + sendMessage.setKey(out.getKey()); + //... + }); + } + if (tracingEnabled) { + Map properties = new HashMap<>(); + TracingUtils.traceOutgoing(instrumenter, message, new MyTrace.Builder() + .withProperties(properties) + .withTopic(sendMessage.getTopic()) + .build()); + sendMessage.setProperties(properties); + } + return Uni.createFrom().completionStage(() -> client.send(sendMessage)) + .onItem().transformToUni(receipt -> Uni.createFrom().completionStage(message.ack())) + .onFailure().recoverWithUni(t -> Uni.createFrom().completionStage(message.nack(t))); + } + // end::outgoing-tracing[] + + public Flow.Subscriber> getSubscriber() { + return this.subscriber; + } +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/connectors/MyOutgoingMessage.java b/content/docs/smallrye-reactive-messaging/main/_examples/connectors/MyOutgoingMessage.java new file mode 100644 index 000000000..86c8a3636 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/connectors/MyOutgoingMessage.java @@ -0,0 +1,70 @@ +package connectors; + +import java.util.concurrent.CompletionStage; +import java.util.function.Function; +import java.util.function.Supplier; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Metadata; + +import io.smallrye.reactive.messaging.providers.locals.ContextAwareMessage; + +public class MyOutgoingMessage implements Message, ContextAwareMessage { + + private final T payload; + private final Metadata metadata; + + private final Supplier> ack; + private final Function> nack; + + public static MyOutgoingMessage from(Message message) { + return new MyOutgoingMessage<>(message.getPayload(), message.getMetadata(), message.getAck(), message.getNack()); + } + + public MyOutgoingMessage(T payload, Metadata metadata, + Supplier> ack, + Function> nack) { + this.payload = payload; + this.metadata = metadata; + this.ack = ack; + this.nack = nack; + } + + public MyOutgoingMessage(T payload, String key, String topic, + Supplier> ack, + Function> nack) { + this(payload, Metadata.of(new MyOutgoingMetadata(topic, key)), ack, nack); + } + + @Override + public T getPayload() { + return payload; + } + + @Override + public Metadata getMetadata() { + return metadata; + } + + @Override + public Supplier> getAck() { + return this.ack; + } + + @Override + public Function> getNack() { + return this.nack; + } + + public MyOutgoingMessage withKey(String key) { + this.metadata.with(this.metadata.get(MyOutgoingMetadata.class) + .map(m -> MyOutgoingMetadata.builder(m).withKey(key).build())); + return this; + } + + public MyOutgoingMessage withTopic(String topic) { + this.metadata.with(this.metadata.get(MyOutgoingMetadata.class) + .map(m -> MyOutgoingMetadata.builder(m).withTopic(topic).build())); + return this; + } +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/connectors/MyOutgoingMetadata.java b/content/docs/smallrye-reactive-messaging/main/_examples/connectors/MyOutgoingMetadata.java new file mode 100644 index 000000000..15c47cc1b --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/connectors/MyOutgoingMetadata.java @@ -0,0 +1,56 @@ +package connectors; + +public class MyOutgoingMetadata { + private String topic; + private String key; + + public static MyOutgoingMetadataBuilder builder() { + return new MyOutgoingMetadataBuilder(); + } + + public static MyOutgoingMetadataBuilder builder(MyOutgoingMetadata metadata) { + return new MyOutgoingMetadataBuilder(metadata); + } + + public MyOutgoingMetadata(String topic, String key) { + this.topic = topic; + this.key = key; + } + + public String getTopic() { + return topic; + } + + public String getKey() { + return key; + } + + public static class MyOutgoingMetadataBuilder { + private String topic; + private String key; + + public MyOutgoingMetadataBuilder() { + + } + + public MyOutgoingMetadataBuilder(MyOutgoingMetadata metadata) { + this.key = metadata.getKey(); + this.topic = metadata.getTopic(); + } + + public MyOutgoingMetadataBuilder withTopic(String topic) { + this.topic = topic; + return this; + } + + public MyOutgoingMetadataBuilder withKey(String key) { + this.key = key; + return this; + } + + public MyOutgoingMetadata build() { + return new MyOutgoingMetadata(topic, key); + } + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/connectors/test/MyConnectorTest.java b/content/docs/smallrye-reactive-messaging/main/_examples/connectors/test/MyConnectorTest.java new file mode 100644 index 000000000..718aac41f --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/connectors/test/MyConnectorTest.java @@ -0,0 +1,52 @@ +package connectors.test; + +import static org.awaitility.Awaitility.await; + +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CopyOnWriteArrayList; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.junit.jupiter.api.Test; + +import connectors.MyConnector; +import io.smallrye.reactive.messaging.test.common.config.MapBasedConfig; + +public class MyConnectorTest extends WeldTestBase { + + @Test + void incomingChannel() { + String host = ""; + int port = 0; + String myTopic = UUID.randomUUID().toString(); + MapBasedConfig config = new MapBasedConfig() + .with("mp.messaging.incoming.data.topic", myTopic) + .with("mp.messaging.incoming.data.host", host) + .with("mp.messaging.incoming.data.port", port) + .with("mp.messaging.incoming.data.connector", MyConnector.CONNECTOR_NAME); + MyApp app = runApplication(config, MyApp.class); + + int expected = 10; + // produce expected number of messages to myTopic + + // wait until app received + await().until(() -> app.received().size() == expected); + } + + @ApplicationScoped + public static class MyApp { + + List received = new CopyOnWriteArrayList<>(); + + @Incoming("data") + void consume(String msg) { + received.add(msg); + } + + public List received() { + return received; + } + } +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/connectors/test/WeldTestBase.java b/content/docs/smallrye-reactive-messaging/main/_examples/connectors/test/WeldTestBase.java new file mode 100644 index 000000000..35cf9555b --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/connectors/test/WeldTestBase.java @@ -0,0 +1,140 @@ +package connectors.test; + +import jakarta.enterprise.inject.spi.BeanManager; + +import org.eclipse.microprofile.reactive.messaging.spi.ConnectorLiteral; +import org.jboss.weld.environment.se.Weld; +import org.jboss.weld.environment.se.WeldContainer; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; + +import connectors.MyConnector; +import connectors.MyMessageConverter; +import io.smallrye.config.inject.ConfigExtension; +import io.smallrye.reactive.messaging.providers.MediatorFactory; +import io.smallrye.reactive.messaging.providers.connectors.ExecutionHolder; +import io.smallrye.reactive.messaging.providers.connectors.WorkerPoolRegistry; +import io.smallrye.reactive.messaging.providers.extension.ChannelProducer; +import io.smallrye.reactive.messaging.providers.extension.EmitterFactoryImpl; +import io.smallrye.reactive.messaging.providers.extension.HealthCenter; +import io.smallrye.reactive.messaging.providers.extension.LegacyEmitterFactoryImpl; +import io.smallrye.reactive.messaging.providers.extension.MediatorManager; +import io.smallrye.reactive.messaging.providers.extension.MutinyEmitterFactoryImpl; +import io.smallrye.reactive.messaging.providers.extension.ReactiveMessagingExtension; +import io.smallrye.reactive.messaging.providers.impl.ConfiguredChannelFactory; +import io.smallrye.reactive.messaging.providers.impl.ConnectorFactories; +import io.smallrye.reactive.messaging.providers.impl.InternalChannelRegistry; +import io.smallrye.reactive.messaging.providers.metrics.MetricDecorator; +import io.smallrye.reactive.messaging.providers.metrics.MicrometerDecorator; +import io.smallrye.reactive.messaging.providers.wiring.Wiring; +import io.smallrye.reactive.messaging.test.common.config.MapBasedConfig; +import io.smallrye.reactive.messaging.test.common.config.SmallRyeConfigTestUtil; + +public class WeldTestBase { + + protected Weld weld; + protected WeldContainer container; + + @BeforeEach + public void initWeld() { + weld = new Weld(); + + // SmallRye config + ConfigExtension extension = new ConfigExtension(); + weld.addExtension(extension); + + weld.addBeanClass(MediatorFactory.class); + weld.addBeanClass(MediatorManager.class); + weld.addBeanClass(InternalChannelRegistry.class); + weld.addBeanClass(ConnectorFactories.class); + weld.addBeanClass(ConfiguredChannelFactory.class); + weld.addBeanClass(ChannelProducer.class); + weld.addBeanClass(ExecutionHolder.class); + weld.addBeanClass(WorkerPoolRegistry.class); + weld.addBeanClass(HealthCenter.class); + weld.addBeanClass(Wiring.class); + weld.addExtension(new ReactiveMessagingExtension()); + + weld.addBeanClass(EmitterFactoryImpl.class); + weld.addBeanClass(MutinyEmitterFactoryImpl.class); + weld.addBeanClass(LegacyEmitterFactoryImpl.class); + + weld.addBeanClass(MyConnector.class); + weld.addBeanClass(MyMessageConverter.class); + weld.addBeanClass(MetricDecorator.class); + weld.addBeanClass(MicrometerDecorator.class); + weld.disableDiscovery(); + } + + @AfterEach + public void stopContainer() { + if (container != null) { + // TODO Explicitly close the connector + getBeanManager().createInstance() + .select(MyConnector.class, ConnectorLiteral.of(MyConnector.CONNECTOR_NAME)).get(); + container.close(); + } + // Release the config objects + SmallRyeConfigTestUtil.releaseConfig(); + } + + public BeanManager getBeanManager() { + if (container == null) { + runApplication(new MapBasedConfig()); + } + return container.getBeanManager(); + } + + public void addBeans(Class... clazzes) { + weld.addBeanClasses(clazzes); + } + + public T get(Class clazz) { + return getBeanManager().createInstance().select(clazz).get(); + } + + public T runApplication(MapBasedConfig config, Class clazz) { + weld.addBeanClass(clazz); + runApplication(config); + return get(clazz); + } + + public void runApplication(MapBasedConfig config) { + if (config != null) { + config.write(); + } else { + MapBasedConfig.cleanup(); + } + + SmallRyeConfigTestUtil.installConfig(); + container = weld.initialize(); + } + + public static void addConfig(MapBasedConfig config) { + if (config != null) { + config.write(); + } else { + MapBasedConfig.cleanup(); + } + } + + public HealthCenter getHealth() { + if (container == null) { + throw new IllegalStateException("Application not started"); + } + return container.getBeanManager().createInstance().select(HealthCenter.class).get(); + } + + public boolean isStarted() { + return getHealth().getStartup().isOk(); + } + + public boolean isReady() { + return getHealth().getReadiness().isOk(); + } + + public boolean isAlive() { + return getHealth().getLiveness().isOk(); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/connectors/tracing/MyOpenTelemetryInstrumenter.java b/content/docs/smallrye-reactive-messaging/main/_examples/connectors/tracing/MyOpenTelemetryInstrumenter.java new file mode 100644 index 000000000..9454e254f --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/connectors/tracing/MyOpenTelemetryInstrumenter.java @@ -0,0 +1,64 @@ +package connectors.tracing; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessageOperation; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingSpanNameExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; +import io.smallrye.reactive.messaging.tracing.TracingUtils; + +/** + * Encapsulates the OpenTelemetry instrumentation API so that those classes are only needed if + * users explicitly enable tracing. + */ +public class MyOpenTelemetryInstrumenter { + + private final Instrumenter instrumenter; + + private MyOpenTelemetryInstrumenter(Instrumenter instrumenter) { + this.instrumenter = instrumenter; + } + + public static MyOpenTelemetryInstrumenter createIncoming() { + return new MyOpenTelemetryInstrumenter(createInstrumenter(true)); + } + + public static MyOpenTelemetryInstrumenter createOutgoing() { + return new MyOpenTelemetryInstrumenter(createInstrumenter(false)); + } + + // tag::create-instrumenter[] + public static Instrumenter createInstrumenter(boolean incoming) { + MessageOperation messageOperation = incoming ? MessageOperation.RECEIVE : MessageOperation.PUBLISH; + + MyAttributesExtractor myExtractor = new MyAttributesExtractor(); + MessagingAttributesGetter attributesGetter = myExtractor.getMessagingAttributesGetter(); + var spanNameExtractor = MessagingSpanNameExtractor.create(attributesGetter, messageOperation); + InstrumenterBuilder builder = Instrumenter.builder(GlobalOpenTelemetry.get(), + "io.smallrye.reactive.messaging", spanNameExtractor); + var attributesExtractor = MessagingAttributesExtractor.create(attributesGetter, messageOperation); + + builder + .addAttributesExtractor(attributesExtractor) + .addAttributesExtractor(myExtractor); + + if (incoming) { + return builder.buildConsumerInstrumenter(MyTraceTextMapGetter.INSTANCE); + } else { + return builder.buildProducerInstrumenter(MyTraceTextMapSetter.INSTANCE); + } + } + // + + public Message traceIncoming(Message message, MyTrace myTrace, boolean makeCurrent) { + return TracingUtils.traceIncoming(instrumenter, message, myTrace, makeCurrent); + } + + public void traceOutgoing(Message message, MyTrace myTrace) { + TracingUtils.traceOutgoing(instrumenter, message, myTrace); + } +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/consumption/ConsumptionExamples.java b/content/docs/smallrye-reactive-messaging/main/_examples/consumption/ConsumptionExamples.java new file mode 100644 index 000000000..2094888b6 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/consumption/ConsumptionExamples.java @@ -0,0 +1,70 @@ +package consumption; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.mutiny.Uni; + +public class ConsumptionExamples { + + //tag::message-cs[] + @Incoming("my-channel") + public CompletionStage consumeMessage(Message message) { + handle(message.getPayload()); + return message.ack(); + } + //end::message-cs[] + + //tag::payload-sync[] + @Incoming("my-channel") + public void consumePayload(Price payload) { + // do something + } + //end::payload-sync[] + + //tag::payload-cs[] + @Incoming("my-channel") + public CompletionStage consumePayloadCS(Price payload) { + CompletionStage cs = handleAsync(payload); + return cs; + } + //end::payload-cs[] + + //tag::payload-uni[] + @Incoming("my-channel") + public Uni consumePayloadUni(Price payload) { + return Uni.createFrom().item(payload) + .onItem().invoke(this::handle) + .onItem().ignore().andContinueWithNull(); + } + //end::payload-uni[] + + //tag::message-uni[] + @Incoming("my-channel") + public Uni consumeMessageUni(Message message) { + return Uni.createFrom().item(message) + .onItem().invoke(m -> handle(m.getPayload())) + .onItem().transformToUni(x -> Uni.createFrom().completionStage(message.ack())); + } + //end::message-uni[] + + private CompletionStage handleAsync(Price price) { + return CompletableFuture.completedFuture(null); + } + + @SuppressWarnings("UnusedReturnValue") + private Price handle(Price price) { + return price; + } + + public static class Price { + final double price; + + public Price(double p) { + this.price = p; + } + } +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/converters/ConverterExample.java b/content/docs/smallrye-reactive-messaging/main/_examples/converters/ConverterExample.java new file mode 100644 index 000000000..a9d3b661e --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/converters/ConverterExample.java @@ -0,0 +1,24 @@ +package converters; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +public class ConverterExample { + + // tag::code[] + @Outgoing("persons") + public Multi source() { + return Multi.createFrom().items("Neo", "Morpheus", "Trinity"); + } + + // The messages need to be converted as they are emitted as Message + // and consumed as Message + @Incoming("persons") + public void consume(Person p) { + // ... + } + // end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/converters/MyConverter.java b/content/docs/smallrye-reactive-messaging/main/_examples/converters/MyConverter.java new file mode 100644 index 000000000..3f517ece0 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/converters/MyConverter.java @@ -0,0 +1,31 @@ +package converters; + +import java.lang.reflect.Type; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.MessageConverter; + +// tag::code[] +@ApplicationScoped +public class MyConverter implements MessageConverter { + @Override + public boolean canConvert(Message in, Type target) { + // Checks whether this converter can be used to convert + // the incoming message into a message containing a payload + // of the type `target`. + return in.getPayload().getClass().equals(String.class) + && target.equals(Person.class); + } + + @Override + public Message convert(Message in, Type target) { + // Convert the incoming message into the new message. + // It's important to build the new message **from** + // the received one. + return in.withPayload(new Person((String) in.getPayload())); + } +} +// end::code[] diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/customizers/MyClientCustomizer.java b/content/docs/smallrye-reactive-messaging/main/_examples/customizers/MyClientCustomizer.java new file mode 100644 index 000000000..9e07288be --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/customizers/MyClientCustomizer.java @@ -0,0 +1,17 @@ +package customizers; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.config.Config; + +import io.smallrye.reactive.messaging.ClientCustomizer; + +@ApplicationScoped +public class MyClientCustomizer implements ClientCustomizer { + @Override + public ClientConfig customize(String channel, Config channelConfig, ClientConfig config) { + // customize the client configuration + return config; + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/decorators/ConsumedMessageDecorator.java b/content/docs/smallrye-reactive-messaging/main/_examples/decorators/ConsumedMessageDecorator.java new file mode 100644 index 000000000..d1b403fde --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/decorators/ConsumedMessageDecorator.java @@ -0,0 +1,41 @@ +package decorators; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.mutiny.Multi; +import io.smallrye.reactive.messaging.PublisherDecorator; + +// tag::code[] +@ApplicationScoped +public class ConsumedMessageDecorator implements PublisherDecorator { + + private final Map counters = new HashMap<>(); + + @Override + public Multi> decorate(Multi> publisher, String channelName, + boolean isConnector) { + if (isConnector) { + AtomicLong counter = new AtomicLong(); + counters.put(channelName, counter); + return publisher.onItem().invoke(counter::incrementAndGet); + } else { + return publisher; + } + } + + @Override + public int getPriority() { + return 10; + } + + public long getMessageCount(String channel) { + return counters.get(channel).get(); + } +} +// end::code[] diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/emitter/ChannelExamples.java b/content/docs/smallrye-reactive-messaging/main/_examples/emitter/ChannelExamples.java new file mode 100644 index 000000000..1b2f021ba --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/emitter/ChannelExamples.java @@ -0,0 +1,31 @@ +package emitter; + +import java.util.concurrent.Flow.Publisher; + +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.mutiny.Multi; + +public class ChannelExamples { + + // tag::channel[] + @Inject + @Channel("my-channel") + Multi streamOfPayloads; + + @Inject + @Channel("my-channel") + Multi> streamOfMessages; + + @Inject + @Channel("my-channel") + Publisher publisherOfPayloads; + + @Inject + @Channel("my-channel") + Publisher> publisherOfMessages; + // end::channel[] +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/emitter/EmitterExamples.java b/content/docs/smallrye-reactive-messaging/main/_examples/emitter/EmitterExamples.java new file mode 100644 index 000000000..4c5c4ed99 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/emitter/EmitterExamples.java @@ -0,0 +1,119 @@ +package emitter; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.*; + +import io.smallrye.reactive.messaging.annotations.Broadcast; +import messages.MyMetadata; + +public class EmitterExamples { + + // tag::payload[] + @Inject + @Channel("prices") + Emitter emitterForPrices; + + public void send(double d) { + emitterForPrices.send(d); + } + // end::payload[] + + // tag::cs[] + public void sendAndAwaitAcknowledgement(double d) { + CompletionStage acked = emitterForPrices.send(d); + // sending a payload returns a CompletionStage completed + // when the message is acknowledged + acked.toCompletableFuture().join(); + } + // end::cs[] + + // tag::message[] + public void sendAsMessage(double d) { + emitterForPrices.send(Message.of(d)); + } + // end::message[] + + // tag::message-ack[] + public void sendAsMessageWithAck(double d) { + emitterForPrices.send(Message.of(d, () -> { + // Called when the message is acknowledged. + return CompletableFuture.completedFuture(null); + }, + reason -> { + // Called when the message is acknowledged negatively. + return CompletableFuture.completedFuture(null); + })); + } + // end::message-ack[] + + // tag::message-meta[] + public void sendAsMessageWithAckAndMetadata(double d) { + MyMetadata metadata = new MyMetadata(); + emitterForPrices.send(Message.of(d, Metadata.of(metadata), + () -> { + // Called when the message is acknowledged. + return CompletableFuture.completedFuture(null); + }, + reason -> { + // Called when the message is acknowledged negatively. + return CompletableFuture.completedFuture(null); + })); + } + // end::message-meta[] + + // tag::broadcast[] + @Inject + @Broadcast + @Channel("prices") + Emitter emitter; + + public void emit(double d) { + emitter.send(d); + } + + @Incoming("prices") + public void handle(double d) { + // Handle the new price + } + + @Incoming("prices") + public void audit(double d) { + // Audit the price change + } + // end::broadcast[] + + static class OverflowExample { + // tag::overflow[] + + // Set the max size to 10 and fail if reached + @OnOverflow(value = OnOverflow.Strategy.BUFFER, bufferSize = 10) + @Inject + @Channel("channel") + Emitter emitterWithBuffer; + + // [DANGER ZONE] no limit + @OnOverflow(OnOverflow.Strategy.UNBOUNDED_BUFFER) + @Inject + @Channel("channel") + Emitter danger; + + // Drop the new messages if the size is reached + @OnOverflow(OnOverflow.Strategy.DROP) + @Inject + @Channel("channel") + Emitter dropping; + + // Drop the previously sent messages if the size is reached + @OnOverflow(OnOverflow.Strategy.LATEST) + @Inject + @Channel("channel") + Emitter dropOldMessages; + + // end::overflow[] + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/emitter/EmitterFactoryExamples.java b/content/docs/smallrye-reactive-messaging/main/_examples/emitter/EmitterFactoryExamples.java new file mode 100644 index 000000000..d9884a773 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/emitter/EmitterFactoryExamples.java @@ -0,0 +1,88 @@ +package emitter; + +import java.util.concurrent.Flow.Publisher; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; +import jakarta.enterprise.inject.spi.InjectionPoint; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.ChannelRegistry; +import io.smallrye.reactive.messaging.EmitterConfiguration; +import io.smallrye.reactive.messaging.EmitterFactory; +import io.smallrye.reactive.messaging.EmitterType; +import io.smallrye.reactive.messaging.MessagePublisherProvider; +import io.smallrye.reactive.messaging.annotations.EmitterFactoryFor; +import io.smallrye.reactive.messaging.providers.extension.ChannelProducer; + +public class EmitterFactoryExamples { + + // tag::custom-emitter-declaration[] + + public interface CustomEmitter extends EmitterType { + + > void sendAndForget(M msg); + + } + + public static class CustomEmitterImpl implements CustomEmitter, MessagePublisherProvider { + + Publisher> publisher; + + public CustomEmitterImpl(EmitterConfiguration configuration, long defaultBufferSize) { + //... initialize emitter with configuration + } + + @Override + public Publisher> getPublisher() { + return publisher; + } + + @Override + public > void sendAndForget(M msg) { + //... send to stream + } + } + // end::custom-emitter-declaration[] + + // tag::custom-emitter-factory[] + @EmitterFactoryFor(CustomEmitter.class) + @ApplicationScoped + public static class CustomEmitterFactory implements EmitterFactory> { + + @Inject + ChannelRegistry channelRegistry; + + @Override + public CustomEmitterImpl createEmitter(EmitterConfiguration configuration, long defaultBufferSize) { + return new CustomEmitterImpl<>(configuration, defaultBufferSize); + } + + @Produces + @Channel("") // Stream name is ignored during type-safe resolution + CustomEmitter produce(InjectionPoint injectionPoint) { + String channelName = ChannelProducer.getChannelName(injectionPoint); + return channelRegistry.getEmitter(channelName, CustomEmitter.class); + } + } + // end::custom-emitter-factory[] + + // tag::custom-emitter-usage[] + + @Inject + @Channel("custom-emitter-channel") + CustomEmitter customEmitter; + + //... + + public void emitMessage() { + customEmitter.sendAndForget(Message.of("a")); + customEmitter.sendAndForget(Message.of("b")); + customEmitter.sendAndForget(Message.of("c")); + } + // end::custom-emitter-usage[] + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/emitter/MutinyExamples.java b/content/docs/smallrye-reactive-messaging/main/_examples/emitter/MutinyExamples.java new file mode 100644 index 000000000..d6d2321b5 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/emitter/MutinyExamples.java @@ -0,0 +1,33 @@ +package emitter; + +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; + +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.subscription.Cancellable; +import io.smallrye.reactive.messaging.MutinyEmitter; + +public class MutinyExamples { + // tag::uni[] + @Inject + @Channel("prices") + MutinyEmitter emitter; + + public Uni send(double d) { + return emitter.send(d); + } + // end::uni[] + + // tag::uni-await[] + public void sendAwait(double d) { + emitter.sendAndAwait(d); + } + // end::uni-await[] + + // tag::uni-forget[] + public Cancellable sendForget(double d) { + return emitter.sendAndForget(d); + } + // end::uni-forget[] +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/emitter/MyImperativeBean.java b/content/docs/smallrye-reactive-messaging/main/_examples/emitter/MyImperativeBean.java new file mode 100644 index 000000000..a4619004f --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/emitter/MyImperativeBean.java @@ -0,0 +1,24 @@ +package emitter; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +// tag::intro[] +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Emitter; + +@ApplicationScoped +public class MyImperativeBean { + + @Inject + @Channel("prices") + Emitter emitter; + + // ... + + public void send(double d) { + emitter.send(d); + } +} + +// end::intro[] diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/generation/GenerationExamples.java b/content/docs/smallrye-reactive-messaging/main/_examples/generation/GenerationExamples.java new file mode 100644 index 000000000..c4c0afdc5 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/generation/GenerationExamples.java @@ -0,0 +1,95 @@ +package generation; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.Flow; +import java.util.concurrent.atomic.AtomicInteger; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; + +public class GenerationExamples { + + AtomicInteger counter = new AtomicInteger(); + + // tag::message-sync[] + @Outgoing("my-channel") + public Message generateMessagesSynchronously() { + return Message.of(counter.getAndIncrement()); + } + // end::message-sync[] + + // tag::message-cs[] + @Outgoing("my-channel") + public CompletionStage> generateMessagesAsCompletionStage() { + return asyncClient.poll() + .thenApply(Message::of); + } + // end::message-cs[] + + public static class Price { + final double price; + + public Price(double p) { + this.price = p; + } + } + + private Pollable asyncClient = () -> CompletableFuture.completedFuture(new Price(10.0)); + + private interface Pollable { + CompletionStage poll(); + } + + // tag::message-uni[] + @Outgoing("my-channel") + public Uni> generateMessagesAsync() { + return Uni.createFrom().item(() -> Message.of(counter.getAndIncrement())); + } + // end::message-uni[] + + // tag::message-stream[] + public Flow.Publisher> generateMessageStream() { + Multi multi = reactiveClient.getStream(); + return multi.map(Message::of); + } + // end::message-stream[] + + private ReactiveClient reactiveClient = () -> Multi.createFrom().items("a", "b", "c"); + + private interface ReactiveClient { + Multi getStream(); + } + + // tag::payload-sync[] + @Outgoing("my-channel") + public Integer generatePayloadsSynchronously() { + return counter.getAndIncrement(); + } + // end::payload-sync[] + + // tag::payload-cs[] + @Outgoing("my-channel") + public CompletionStage generatePayloadsAsCompletionStage() { + return asyncClient.poll(); + } + // end::payload-cs[] + + // tag::payload-uni[] + @Outgoing("my-channel") + public Uni generatePayloadsAsync() { + return Uni.createFrom().item(() -> counter.getAndIncrement()); + } + // end::payload-uni[] + + // tag::payload-stream[] + @Outgoing("my-channel") + public Multi generatePayloadsStream() { + Multi multi = reactiveClient.getStream(); + return multi; + } + // end::payload-stream[] +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/genericpayload/GenericPayloadExample.java b/content/docs/smallrye-reactive-messaging/main/_examples/genericpayload/GenericPayloadExample.java new file mode 100644 index 000000000..583cfcec0 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/genericpayload/GenericPayloadExample.java @@ -0,0 +1,49 @@ +package genericpayload; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Metadata; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; +import io.smallrye.reactive.messaging.GenericPayload; +import io.smallrye.reactive.messaging.MutinyEmitter; +import messages.MyMetadata; + +@ApplicationScoped +public class GenericPayloadExample { + + // tag::code[] + @Outgoing("out") + Multi> produce() { + return Multi.createFrom().range(0, 100) + .map(i -> GenericPayload.of(">> " + i, Metadata.of(new MyMetadata()))); + } + // end::code[] + + // tag::injection[] + @Incoming("in") + @Outgoing("out") + GenericPayload process(int payload, MyMetadata metadata) { + // use the injected metadata + String id = metadata.getId(); + return GenericPayload.of(">> " + payload + " " + id, + Metadata.of(metadata, new MyMetadata("Bob", "Alice"))); + } + // end::injection[] + + // tag::emitter[] + @Inject + @Channel("out") + MutinyEmitter> emitter; + + public void emitWithMetadata(String payload) { + emitter.sendAndForget( + GenericPayload.of(payload, Metadata.of(new MyMetadata()))); + } + // end::emitter[] + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/incomings/IncomingsExamples.java b/content/docs/smallrye-reactive-messaging/main/_examples/incomings/IncomingsExamples.java new file mode 100644 index 000000000..f1d727df7 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/incomings/IncomingsExamples.java @@ -0,0 +1,16 @@ +package incomings; + +import org.eclipse.microprofile.reactive.messaging.Incoming; + +public class IncomingsExamples { + + //tag::code[] + @Incoming("channel-1") + @Incoming("channel-2") + public String process(String s) { + // get messages from channel-1 and channel-2 + return s.toUpperCase(); + } + //end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/incomings/MetadataInjectionExample.java b/content/docs/smallrye-reactive-messaging/main/_examples/incomings/MetadataInjectionExample.java new file mode 100644 index 000000000..79c3cc3ef --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/incomings/MetadataInjectionExample.java @@ -0,0 +1,44 @@ +package incomings; + +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Uni; +import jdk.jfr.Period; +import messages.MyMetadata; + +public class MetadataInjectionExample { + + //tag::code[] + @Incoming("in") + @Outgoing("out") + public String process(String payload, MyMetadata metadata) { + // ... + return payload.toUpperCase(); + } + + @Incoming("in") + @Outgoing("out") + public Uni process2(String payload, MyMetadata metadata) { + // ... + return Uni.createFrom().item(payload.toUpperCase()); + } + + // ... + @Incoming("in") + public void consume(String payload, Optional metadata, Period metadata2) { + // ... + } + + @Incoming("in") + public CompletionStage consume2(String payload, Optional metadata, Period metadata2) { + // ... + return CompletableFuture.completedFuture(null); + } + //end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/interceptors/MyIncomingInterceptor.java b/content/docs/smallrye-reactive-messaging/main/_examples/interceptors/MyIncomingInterceptor.java new file mode 100644 index 000000000..c322a7553 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/interceptors/MyIncomingInterceptor.java @@ -0,0 +1,28 @@ +package interceptors; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.common.annotation.Identifier; +import io.smallrye.reactive.messaging.IncomingInterceptor; + +@Identifier("channel-a") +@ApplicationScoped +public class MyIncomingInterceptor implements IncomingInterceptor { + + @Override + public Message afterMessageReceive(Message message) { + return message.withPayload("changed " + message.getPayload()); + } + + @Override + public void onMessageAck(Message message) { + // Called after message ack + } + + @Override + public void onMessageNack(Message message, Throwable failure) { + // Called after message nack + } +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/interceptors/MyOutgoingInterceptor.java b/content/docs/smallrye-reactive-messaging/main/_examples/interceptors/MyOutgoingInterceptor.java new file mode 100644 index 000000000..6efdc8835 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/interceptors/MyOutgoingInterceptor.java @@ -0,0 +1,30 @@ +package interceptors; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.common.annotation.Identifier; +import io.smallrye.reactive.messaging.OutgoingInterceptor; +import io.smallrye.reactive.messaging.OutgoingMessageMetadata; + +@Identifier("channel-a") +@ApplicationScoped +public class MyOutgoingInterceptor implements OutgoingInterceptor { + + @Override + public Message onMessage(Message message) { + return message.withPayload("changed " + message.getPayload()); + } + + @Override + public void onMessageAck(Message message) { + message.getMetadata(OutgoingMessageMetadata.class) + .ifPresent(m -> m.getResult()); + } + + @Override + public void onMessageNack(Message message, Throwable failure) { + + } +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/jms/inbound/JmsMetadataExample.java b/content/docs/smallrye-reactive-messaging/main/_examples/jms/inbound/JmsMetadataExample.java new file mode 100644 index 000000000..adb2a0a65 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/jms/inbound/JmsMetadataExample.java @@ -0,0 +1,25 @@ +package jms.inbound; + +import java.util.Optional; + +import jakarta.jms.Destination; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.jms.IncomingJmsMessageMetadata; + +public class JmsMetadataExample { + + public void metadata() { + Message incoming = Message.of(12.0); + // tag::code[] + Optional metadata = incoming.getMetadata(IncomingJmsMessageMetadata.class); + metadata.ifPresent(meta -> { + long expiration = meta.getExpiration(); + Destination destination = meta.getDestination(); + String value = meta.getStringProperty("my-property"); + }); + // end::code[] + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/jms/inbound/JmsPriceConsumer.java b/content/docs/smallrye-reactive-messaging/main/_examples/jms/inbound/JmsPriceConsumer.java new file mode 100644 index 000000000..ac4cdd774 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/jms/inbound/JmsPriceConsumer.java @@ -0,0 +1,15 @@ +package jms.inbound; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; + +@ApplicationScoped +public class JmsPriceConsumer { + + @Incoming("prices") + public void consume(double price) { + // process your price. + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/jms/inbound/JmsPriceMessageConsumer.java b/content/docs/smallrye-reactive-messaging/main/_examples/jms/inbound/JmsPriceMessageConsumer.java new file mode 100644 index 000000000..6dfdf485e --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/jms/inbound/JmsPriceMessageConsumer.java @@ -0,0 +1,21 @@ +package jms.inbound; + +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; + +@ApplicationScoped +public class JmsPriceMessageConsumer { + + @Incoming("prices") + public CompletionStage consume(Message price) { + // process your price. + + // Acknowledge the incoming message + return price.ack(); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/jms/outbound/JmsOutboundMetadataExample.java b/content/docs/smallrye-reactive-messaging/main/_examples/jms/outbound/JmsOutboundMetadataExample.java new file mode 100644 index 000000000..6142bccf7 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/jms/outbound/JmsOutboundMetadataExample.java @@ -0,0 +1,23 @@ +package jms.outbound; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.jms.JmsProperties; +import io.smallrye.reactive.messaging.jms.OutgoingJmsMessageMetadata; + +public class JmsOutboundMetadataExample { + + public Message metadata(Message incoming) { + + // tag::code[] + OutgoingJmsMessageMetadata metadata = OutgoingJmsMessageMetadata.builder() + .withProperties(JmsProperties.builder().with("some-property", "some-value").build()) + .build(); + + // Create a new message from the `incoming` message + // Add `metadata` to the metadata from the `incoming` message. + return incoming.addMetadata(metadata); + // end::code[] + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/jms/outbound/JmsPriceMessageProducer.java b/content/docs/smallrye-reactive-messaging/main/_examples/jms/outbound/JmsPriceMessageProducer.java new file mode 100644 index 000000000..bd57c863b --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/jms/outbound/JmsPriceMessageProducer.java @@ -0,0 +1,26 @@ +package jms.outbound; + +import java.time.Duration; +import java.util.Random; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +@ApplicationScoped +public class JmsPriceMessageProducer { + + private Random random = new Random(); + + @Outgoing("prices") + public Multi> generate() { + // Build an infinite stream of random prices + // It emits a price every second + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .map(x -> Message.of(random.nextDouble())); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/jms/outbound/JmsPriceProducer.java b/content/docs/smallrye-reactive-messaging/main/_examples/jms/outbound/JmsPriceProducer.java new file mode 100644 index 000000000..b693a91f7 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/jms/outbound/JmsPriceProducer.java @@ -0,0 +1,25 @@ +package jms.outbound; + +import java.time.Duration; +import java.util.Random; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +@ApplicationScoped +public class JmsPriceProducer { + + private Random random = new Random(); + + @Outgoing("prices") + public Multi generate() { + // Build an infinite stream of random prices + // It emits a price every second + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .map(x -> random.nextDouble()); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/kafka/companion/ConsumerTest.java b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/companion/ConsumerTest.java new file mode 100644 index 000000000..fe0e60637 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/companion/ConsumerTest.java @@ -0,0 +1,45 @@ +package kafka.companion; + +import static org.testcontainers.shaded.org.awaitility.Awaitility.await; + +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; + +import org.apache.kafka.common.TopicPartition; +import org.junit.jupiter.api.Test; + +import io.smallrye.reactive.messaging.kafka.companion.ConsumerBuilder; +import io.smallrye.reactive.messaging.kafka.companion.test.KafkaCompanionTestBase; + +public class ConsumerTest extends KafkaCompanionTestBase { + + @Test + public void fromTopics() { + // tag::topics[] + companion.consumeIntegers().fromTopics("topic1", "topic2"); + // end::topics[] + } + + @Test + public void fromOffset() { + // tag::offsets[] + Map offsets = new HashMap<>(); + offsets.put(new TopicPartition("topic1", 0), 100L); + offsets.put(new TopicPartition("topic2", 0), 100L); + companion.consumeIntegers().fromOffsets(offsets, Duration.ofSeconds(10)); + // end::offsets[] + } + + @Test + public void committed() { + // tag::committed[] + ConsumerBuilder consumer = companion.consumeIntegers() + .withAutoCommit(); + consumer.fromTopics("topic"); + // ... + await().untilAsserted(consumer::waitForAssignment); + consumer.committed(new TopicPartition("topic", 1)); + // end::committed[] + } +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/kafka/companion/KafkaTest.java b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/companion/KafkaTest.java new file mode 100644 index 000000000..e10717dc0 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/companion/KafkaTest.java @@ -0,0 +1,76 @@ +package kafka.companion; + +import java.time.Duration; + +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.apache.kafka.common.serialization.Deserializer; +import org.apache.kafka.common.serialization.Serializer; +import org.junit.jupiter.api.Assertions; + +import io.smallrye.reactive.messaging.kafka.companion.ConsumerBuilder; +import io.smallrye.reactive.messaging.kafka.companion.ConsumerTask; +import io.smallrye.reactive.messaging.kafka.companion.KafkaCompanion; +import io.smallrye.reactive.messaging.kafka.companion.ProducerBuilder; + +public class KafkaTest { + + public void newCompanion() { + // tag::companion[] + // Create companion with bootstrap servers and API timeout (default is 10 seconds) + KafkaCompanion companion = new KafkaCompanion("localhost:9092", Duration.ofSeconds(5)); + + // Create producer and start producer task + ProducerBuilder producer = companion.produceIntegers() + .withClientId("my-producer") + .withProp("max.block.ms", "5000"); + producer.usingGenerator(i -> new ProducerRecord<>("topic", i), 100); + + // Create consumer and start consumer task + ConsumerBuilder consumer = companion.consumeIntegers() + .withGroupId("my-group") + .withCommitAsyncWhen(record -> true); + ConsumerTask records = consumer.fromTopics("topic", Duration.ofSeconds(10)); + // Await completion and assert consumed record count + Assertions.assertEquals(records.awaitCompletion().count(), 100); + // end::companion[] + } + + public void serdes() { + // tag::serdes[] + KafkaCompanion companion = new KafkaCompanion("localhost:9092"); + + // Register serde to the companion + companion.registerSerde(Orders.class, new OrdersSerializer(), new OrdersDeserializer()); + + // Companion will configure consumer accordingly + ConsumerTask orders = companion.consume(Integer.class, Orders.class) + .fromTopics("orders", 1000).awaitCompletion(); + + for (ConsumerRecord order : orders) { + // ... check consumed records + } + // end::serdes[] + } + + public static class Orders { + + } + + public static class OrdersSerializer implements Serializer { + + @Override + public byte[] serialize(String s, Orders orders) { + return new byte[0]; + } + } + + public static class OrdersDeserializer implements Deserializer { + + @Override + public Orders deserialize(String s, byte[] bytes) { + return null; + } + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/kafka/companion/KafkaWithBaseTest.java b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/companion/KafkaWithBaseTest.java new file mode 100644 index 000000000..63bd8634a --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/companion/KafkaWithBaseTest.java @@ -0,0 +1,32 @@ +package kafka.companion; + +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import io.smallrye.reactive.messaging.kafka.companion.ConsumerTask; +import io.smallrye.reactive.messaging.kafka.companion.ProducerTask; +import io.smallrye.reactive.messaging.kafka.companion.test.KafkaCompanionTestBase; + +// tag::code[] +public class KafkaWithBaseTest extends KafkaCompanionTestBase { + + @Test + public void testWithBase() { + // companion is created by the base class + + // produce 100 records to messages topic + ProducerTask producerTask = companion.produceIntegers() + .usingGenerator(i -> new ProducerRecord<>("messages", i), 100); + long msgCount = producerTask.awaitCompletion().count(); + Assertions.assertEquals(msgCount, 100); + + // consume 100 records from messages topic + ConsumerTask consumerTask = companion.consumeIntegers() + .fromTopics("messages", 100); + ConsumerRecord lastRecord = consumerTask.awaitCompletion().getLastRecord(); + Assertions.assertEquals(lastRecord.value(), 99); + } +} +// end::code[] diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/kafka/companion/OffsetsTest.java b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/companion/OffsetsTest.java new file mode 100644 index 000000000..f7b00fa6b --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/companion/OffsetsTest.java @@ -0,0 +1,36 @@ +package kafka.companion; + +import java.util.Collection; + +import org.apache.kafka.clients.admin.ConsumerGroupListing; +import org.apache.kafka.clients.admin.OffsetSpec; +import org.apache.kafka.common.TopicPartition; +import org.junit.jupiter.api.Test; + +import io.smallrye.reactive.messaging.kafka.companion.KafkaCompanion; +import io.smallrye.reactive.messaging.kafka.companion.test.KafkaCompanionTestBase; + +public class OffsetsTest extends KafkaCompanionTestBase { + + @Test + public void offsets() { + // tag::offsets[] + TopicPartition topicPartition = KafkaCompanion.tp("topic", 1); + long latestOffset = companion.offsets().get(topicPartition, OffsetSpec.latest()).offset(); + // end::offsets[] + } + + @Test + public void consumerGroups() { + // tag::consumerGroups[] + Collection consumerGroups = companion.consumerGroups().list(); + for (ConsumerGroupListing consumerGroup : consumerGroups) { + // check consumer groups + } + + TopicPartition topicPartition = KafkaCompanion.tp("topic", 1); + companion.consumerGroups().resetOffsets("consumer-group", topicPartition); + // end::consumerGroups[] + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/kafka/companion/ProducerTest.java b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/companion/ProducerTest.java new file mode 100644 index 000000000..a60561829 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/companion/ProducerTest.java @@ -0,0 +1,34 @@ +package kafka.companion; + +import org.apache.kafka.clients.producer.ProducerRecord; +import org.junit.jupiter.api.Test; + +import io.smallrye.reactive.messaging.kafka.companion.test.KafkaCompanionTestBase; + +public class ProducerTest extends KafkaCompanionTestBase { + + @Test + public void fromRecords() { + // tag::records[] + companion.produce(byte[].class).fromRecords( + new ProducerRecord<>("topic1", "k1", "1".getBytes()), + new ProducerRecord<>("topic1", "k2", "2".getBytes()), + new ProducerRecord<>("topic1", "k3", "3".getBytes())); + // end::records[] + } + + @Test + public void fromGenerator() { + // tag::generator[] + companion.produceIntegers().usingGenerator(i -> new ProducerRecord<>("topic", i), 10); + // end::generator[] + } + + @Test + public void fromCsv() { + // tag::csv[] + companion.produceStrings().fromCsv("records.csv"); + // end::csv[] + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/kafka/companion/TopicsTest.java b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/companion/TopicsTest.java new file mode 100644 index 000000000..01e048611 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/companion/TopicsTest.java @@ -0,0 +1,20 @@ +package kafka.companion; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import io.smallrye.reactive.messaging.kafka.companion.test.KafkaCompanionTestBase; + +public class TopicsTest extends KafkaCompanionTestBase { + + @Test + public void topics() { + // tag::code[] + companion.topics().create("topic1", 1); + companion.topics().createAndWait("topic2", 3); + Assertions.assertEquals(companion.topics().list().size(), 2); + + companion.topics().delete("topic1"); + // end::code[] + } +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/kafka/inbound/Converters.java b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/inbound/Converters.java new file mode 100644 index 000000000..646c5ae54 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/inbound/Converters.java @@ -0,0 +1,30 @@ +package kafka.inbound; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.eclipse.microprofile.reactive.messaging.Incoming; + +import io.smallrye.reactive.messaging.kafka.Record; + +@ApplicationScoped +public class Converters { + + // tag::code[] + @Incoming("topic-a") + public void consume(Record record) { + String key = record.key(); // Can be `null` if the incoming record has no key + String value = record.value(); // Can be `null` if the incoming record has no value + } + + @Incoming("topic-b") + public void consume(ConsumerRecord record) { + String key = record.key(); // Can be `null` if the incoming record has no key + String value = record.value(); // Can be `null` if the incoming record has no value + String topic = record.topic(); + int partition = record.partition(); + // ... + } + // end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/kafka/inbound/KafkaCheckpointExample.java b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/inbound/KafkaCheckpointExample.java new file mode 100644 index 000000000..9ae1f1ecb --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/inbound/KafkaCheckpointExample.java @@ -0,0 +1,32 @@ +package kafka.inbound; + +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.kafka.commit.CheckpointMetadata; + +@Deprecated(forRemoval = true) +@ApplicationScoped +public class KafkaCheckpointExample { + + // tag::code[] + @Incoming("prices") + public CompletionStage consume(Message record) { + // Get the `CheckpointMetadata` from the incoming message + CheckpointMetadata checkpoint = CheckpointMetadata.fromMessage(record); + + // `CheckpointMetadata` allows transforming the processing state + // Applies the given function, starting from the value `0.0` when no previous state exists + checkpoint.transform(0.0, current -> current + record.getPayload(), /* persistOnAck */ true); + + // `persistOnAck` flag set to true, ack will persist the processing state + // associated with the latest offset (per partition). + return record.ack(); + } + // end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/kafka/inbound/KafkaCustomCommit.java b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/inbound/KafkaCustomCommit.java new file mode 100644 index 000000000..f57e3b87a --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/inbound/KafkaCustomCommit.java @@ -0,0 +1,60 @@ +package kafka.inbound; + +import java.util.Collection; +import java.util.function.BiConsumer; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.apache.kafka.common.TopicPartition; + +import io.smallrye.common.annotation.Identifier; +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.kafka.IncomingKafkaRecord; +import io.smallrye.reactive.messaging.kafka.KafkaConnectorIncomingConfiguration; +import io.smallrye.reactive.messaging.kafka.KafkaConsumer; +import io.smallrye.reactive.messaging.kafka.commit.KafkaCommitHandler; +import io.vertx.mutiny.core.Vertx; + +public class KafkaCustomCommit implements KafkaCommitHandler { + + @Override + public Uni handle(IncomingKafkaRecord record) { + // called on message ack + return Uni.createFrom().voidItem(); + } + + @Override + public Uni> received(IncomingKafkaRecord record) { + // called before message processing + return Uni.createFrom().item(record); + } + + @Override + public void terminate(boolean graceful) { + // called on channel shutdown + } + + @Override + public void partitionsAssigned(Collection partitions) { + // called on partitions assignment + } + + @Override + public void partitionsRevoked(Collection partitions) { + // called on partitions revoked + } + + @ApplicationScoped + @Identifier("custom") + public static class Factory implements KafkaCommitHandler.Factory { + + @Override + public KafkaCommitHandler create(KafkaConnectorIncomingConfiguration config, + Vertx vertx, + KafkaConsumer consumer, + BiConsumer reportFailure) { + return new KafkaCustomCommit(/* ... */); + } + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/kafka/inbound/KafkaDeadLetterExample.java b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/inbound/KafkaDeadLetterExample.java new file mode 100644 index 000000000..82cb5af95 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/inbound/KafkaDeadLetterExample.java @@ -0,0 +1,30 @@ +package kafka.inbound; + +import java.nio.charset.StandardCharsets; +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.apache.kafka.common.header.internals.RecordHeaders; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Metadata; + +import io.smallrye.reactive.messaging.kafka.api.OutgoingKafkaRecordMetadata; + +@ApplicationScoped +public class KafkaDeadLetterExample { + + // tag::code[] + @Incoming("in") + public CompletionStage consume(Message message) { + return message.nack(new Exception("Failed!"), Metadata.of( + OutgoingKafkaRecordMetadata.builder() + .withKey("failed-record") + .withHeaders(new RecordHeaders() + .add("my-header", "my-header-value".getBytes(StandardCharsets.UTF_8))) + .build())); + } + // end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/kafka/inbound/KafkaManualPartitionPauseResume.java b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/inbound/KafkaManualPartitionPauseResume.java new file mode 100644 index 000000000..9f3852dd1 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/inbound/KafkaManualPartitionPauseResume.java @@ -0,0 +1,37 @@ +package kafka.inbound; + +import java.util.Set; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.apache.kafka.common.TopicPartition; + +import io.smallrye.reactive.messaging.kafka.KafkaClientService; +import io.smallrye.reactive.messaging.kafka.KafkaConsumer; + +@ApplicationScoped +public class KafkaManualPartitionPauseResume { + + @Inject + KafkaClientService kafkaClientService; + + // tag::code[] + public void manualPartitionPauseResume() { + KafkaConsumer consumer = kafkaClientService.getConsumer("my-channel"); + TopicPartition tp = new TopicPartition("my-topic", 0); + + // Pause specific partitions — survives backpressure resume cycles + consumer.pausePartitions(Set.of(tp)).await().indefinitely(); + + // Check which partitions are manually paused + Set paused = consumer.pausedPartitions(); + + // Resume specific partitions + consumer.resumePartitions(Set.of(tp)).await().indefinitely(); + + // Pause all assigned partitions + consumer.pauseAssignedPartitions().await().indefinitely(); + } + // end::code[] +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/kafka/inbound/KafkaMetadataExample.java b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/inbound/KafkaMetadataExample.java new file mode 100644 index 000000000..8400fe0df --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/inbound/KafkaMetadataExample.java @@ -0,0 +1,36 @@ +package kafka.inbound; + +import java.time.Instant; + +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.kafka.api.IncomingKafkaRecordMetadata; + +public class KafkaMetadataExample { + + @SuppressWarnings("unchecked") + public void metadata() { + Message incoming = Message.of(12.0); + // tag::code[] + IncomingKafkaRecordMetadata metadata = incoming.getMetadata(IncomingKafkaRecordMetadata.class) + .orElse(null); + if (metadata != null) { + // The topic + String topic = metadata.getTopic(); + + // The key + String key = metadata.getKey(); + + // The timestamp + Instant timestamp = metadata.getTimestamp(); + + // The underlying record + ConsumerRecord record = metadata.getRecord(); + + // ... + } + // end::code[] + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/kafka/inbound/KafkaPriceConsumer.java b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/inbound/KafkaPriceConsumer.java new file mode 100644 index 000000000..7d6d28b94 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/inbound/KafkaPriceConsumer.java @@ -0,0 +1,15 @@ +package kafka.inbound; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; + +@ApplicationScoped +public class KafkaPriceConsumer { + + @Incoming("prices") + public void consume(double price) { + // process your price. + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/kafka/inbound/KafkaPriceMessageConsumer.java b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/inbound/KafkaPriceMessageConsumer.java new file mode 100644 index 000000000..324b7b94f --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/inbound/KafkaPriceMessageConsumer.java @@ -0,0 +1,21 @@ +package kafka.inbound; + +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; + +@ApplicationScoped +public class KafkaPriceMessageConsumer { + + @Incoming("prices") + public CompletionStage consume(Message price) { + // process your price. + + // Acknowledge the incoming message (commit the offset) + return price.ack(); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/kafka/inbound/KafkaRebalancedConsumer.java b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/inbound/KafkaRebalancedConsumer.java new file mode 100644 index 000000000..814461dc2 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/inbound/KafkaRebalancedConsumer.java @@ -0,0 +1,22 @@ +package kafka.inbound; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Acknowledgment; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; + +@ApplicationScoped +public class KafkaRebalancedConsumer { + + @Incoming("rebalanced-example") + @Acknowledgment(Acknowledgment.Strategy.NONE) + public CompletionStage consume(Message message) { + // We don't need to ACK messages because in this example we set offset during consumer re-balance + return CompletableFuture.completedFuture(null); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/kafka/inbound/KafkaRebalancedConsumerRebalanceListener.java b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/inbound/KafkaRebalancedConsumerRebalanceListener.java new file mode 100644 index 000000000..512b65117 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/inbound/KafkaRebalancedConsumerRebalanceListener.java @@ -0,0 +1,49 @@ +package kafka.inbound; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Logger; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.apache.kafka.clients.consumer.Consumer; +import org.apache.kafka.clients.consumer.OffsetAndTimestamp; + +import io.smallrye.common.annotation.Identifier; +import io.smallrye.reactive.messaging.kafka.KafkaConsumerRebalanceListener; + +@ApplicationScoped +@Identifier("rebalanced-example.rebalancer") +public class KafkaRebalancedConsumerRebalanceListener implements KafkaConsumerRebalanceListener { + + private static final Logger LOGGER = Logger.getLogger(KafkaRebalancedConsumerRebalanceListener.class.getName()); + + /** + * When receiving a list of partitions will search for the earliest offset within 10 minutes + * and seek the consumer to it. + * + * @param consumer underlying consumer + * @param partitions set of assigned topic partitions + */ + @Override + public void onPartitionsAssigned(Consumer consumer, + Collection partitions) { + long now = System.currentTimeMillis(); + long shouldStartAt = now - 600_000L; //10 minute ago + + Map request = new HashMap<>(); + for (org.apache.kafka.common.TopicPartition partition : partitions) { + LOGGER.info("Assigned " + partition); + request.put(partition, shouldStartAt); + } + Map offsets = consumer + .offsetsForTimes(request); + for (Map.Entry position : offsets.entrySet()) { + long target = position.getValue() == null ? 0L : position.getValue().offset(); + LOGGER.info("Seeking position " + target + " for " + position.getKey()); + consumer.seek(position.getKey(), target); + } + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/kafka/inbound/KafkaRecordBatchExample.java b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/inbound/KafkaRecordBatchExample.java new file mode 100644 index 000000000..85d329af6 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/inbound/KafkaRecordBatchExample.java @@ -0,0 +1,60 @@ +package kafka.inbound; + +import java.util.List; +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.clients.consumer.ConsumerRecords; +import org.apache.kafka.common.TopicPartition; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.TracingMetadata; +import io.smallrye.reactive.messaging.kafka.api.IncomingKafkaRecordBatchMetadata; + +@ApplicationScoped +public class KafkaRecordBatchExample { + + // tag::code[] + @Incoming("prices") + public CompletionStage consumeMessage(Message> msg) { + IncomingKafkaRecordBatchMetadata batchMetadata = msg.getMetadata(IncomingKafkaRecordBatchMetadata.class) + .get(); + for (ConsumerRecord record : batchMetadata.getRecords()) { + int partition = record.partition(); + long offset = record.offset(); + long timestamp = record.timestamp(); + } + // ack will commit the latest offsets (per partition) of the batch. + return msg.ack(); + } + + @Incoming("prices") + public void consumeRecords(ConsumerRecords records) { + for (TopicPartition partition : records.partitions()) { + for (ConsumerRecord record : records.records(partition)) { + //... process messages + } + } + } + // end::code[] + + // tag::batch[] + @Incoming("prices") + public void consumeRecords(ConsumerRecords records, + IncomingKafkaRecordBatchMetadata metadata) { + for (TopicPartition partition : records.partitions()) { + for (ConsumerRecord record : records.records(partition)) { + TracingMetadata tracing = metadata.getMetadataForRecord(record, TracingMetadata.class); + if (tracing != null) { + tracing.getCurrentContext().makeCurrent(); + } + //... process messages + } + } + } + // end::batch[] + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/kafka/inbound/KafkaRecordBatchPayloadExample.java b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/inbound/KafkaRecordBatchPayloadExample.java new file mode 100644 index 000000000..9a3d11100 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/inbound/KafkaRecordBatchPayloadExample.java @@ -0,0 +1,21 @@ +package kafka.inbound; + +import java.util.List; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; + +@ApplicationScoped +public class KafkaRecordBatchPayloadExample { + + // tag::code[] + @Incoming("prices") + public void consume(List prices) { + for (double price : prices) { + // process price + } + } + // end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/kafka/inbound/KafkaShareGroupAckExample.java b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/inbound/KafkaShareGroupAckExample.java new file mode 100644 index 000000000..2ee56bd59 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/inbound/KafkaShareGroupAckExample.java @@ -0,0 +1,29 @@ +package kafka.inbound; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; + +import io.smallrye.reactive.messaging.kafka.queues.ShareGroupAcknowledgement; + +@ApplicationScoped +public class KafkaShareGroupAckExample { + + // tag::code[] + @Incoming("queue") + public void consume(double price, ShareGroupAcknowledgement ack) { + if (price < 0) { + ack.reject(); // Permanently reject the record + } else if (needsRetry(price)) { + ack.release(); // Release for re-delivery to another consumer + } else { + ack.accept(); // Mark as successfully processed + } + } + // end::code[] + + private boolean needsRetry(double price) { + return false; + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/kafka/inbound/KafkaShareGroupBatchAckExample.java b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/inbound/KafkaShareGroupBatchAckExample.java new file mode 100644 index 000000000..383d4ec6c --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/inbound/KafkaShareGroupBatchAckExample.java @@ -0,0 +1,30 @@ +package kafka.inbound; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.clients.consumer.ConsumerRecords; +import org.eclipse.microprofile.reactive.messaging.Incoming; + +import io.smallrye.reactive.messaging.kafka.api.IncomingKafkaRecordBatchMetadata; +import io.smallrye.reactive.messaging.kafka.queues.ShareGroupAcknowledgement; + +@ApplicationScoped +public class KafkaShareGroupBatchAckExample { + + // tag::code[] + @Incoming("queue") + public void consume(ConsumerRecords records, + IncomingKafkaRecordBatchMetadata batchMetadata) { + for (ConsumerRecord record : records) { + ShareGroupAcknowledgement ack = batchMetadata.getMetadataForRecord(record, ShareGroupAcknowledgement.class); + if (record.value() < 0) { + ack.reject(); // Reject invalid records + } else { + ack.accept(); // Accept valid records + } + } + } + // end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/kafka/inbound/KafkaShareGroupConsumer.java b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/inbound/KafkaShareGroupConsumer.java new file mode 100644 index 000000000..5df915512 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/inbound/KafkaShareGroupConsumer.java @@ -0,0 +1,15 @@ +package kafka.inbound; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; + +@ApplicationScoped +public class KafkaShareGroupConsumer { + + @Incoming("queue") + public void consume(double price) { + // process your price. + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/kafka/inbound/KafkaShareGroupNackExample.java b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/inbound/KafkaShareGroupNackExample.java new file mode 100644 index 000000000..ffebef1c2 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/inbound/KafkaShareGroupNackExample.java @@ -0,0 +1,41 @@ +package kafka.inbound; + +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.apache.kafka.clients.consumer.AcknowledgeType; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Metadata; + +import io.smallrye.reactive.messaging.kafka.queues.ShareGroupAcknowledgement; + +@ApplicationScoped +public class KafkaShareGroupNackExample { + + // tag::code[] + @Incoming("queue") + public CompletionStage consume(Message msg) { + try { + process(msg.getPayload()); + return msg.ack(); + } catch (TransientException e) { + // Release for re-delivery + return msg.nack(e, + Metadata.of(ShareGroupAcknowledgement.from(AcknowledgeType.RELEASE))); + } catch (Exception e) { + // Reject permanently + return msg.nack(e, + Metadata.of(ShareGroupAcknowledgement.from(AcknowledgeType.REJECT))); + } + } + // end::code[] + + private void process(double price) { + } + + private static class TransientException extends RuntimeException { + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/kafka/inbound/MyCheckpointStateStore.java b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/inbound/MyCheckpointStateStore.java new file mode 100644 index 000000000..5c33ce0c1 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/inbound/MyCheckpointStateStore.java @@ -0,0 +1,69 @@ +package kafka.inbound; + +import java.util.Collection; +import java.util.Map; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.apache.kafka.clients.consumer.ConsumerConfig; +import org.apache.kafka.common.TopicPartition; + +import io.smallrye.common.annotation.Identifier; +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.kafka.KafkaConnectorIncomingConfiguration; +import io.smallrye.reactive.messaging.kafka.KafkaConsumer; +import io.smallrye.reactive.messaging.kafka.commit.CheckpointStateStore; +import io.smallrye.reactive.messaging.kafka.commit.ProcessingState; +import io.vertx.mutiny.core.Vertx; + +@Deprecated(forRemoval = true) +public class MyCheckpointStateStore implements CheckpointStateStore { + + private final String consumerGroupId; + private final Class stateType; + + public MyCheckpointStateStore(String consumerGroupId, Class stateType) { + this.consumerGroupId = consumerGroupId; + this.stateType = stateType; + } + + /** + * Can be used with + * {@code mp.reactive.messaging.incoming.my-channel.commit-strategy=checkpoint} + * {@code mp.reactive.messaging.incoming.my-channel.checkpoint.state-store=my-store} + */ + @ApplicationScoped + @Identifier("my-store") + public static class Factory implements CheckpointStateStore.Factory { + + @Override + public CheckpointStateStore create(KafkaConnectorIncomingConfiguration config, + Vertx vertx, + KafkaConsumer consumer, + Class stateType /* if configured, otherwise null */) { + String consumerGroupId = (String) consumer.configuration().get(ConsumerConfig.GROUP_ID_CONFIG); + return new MyCheckpointStateStore(consumerGroupId, stateType); + } + } + + @Override + public Uni>> fetchProcessingState(Collection partitions) { + // Called on Vert.x event loop + // Return a Uni completing with the map of topic-partition to processing state + // The Uni will be subscribed also on Vert.x event loop + return Uni.createFrom().nullItem(); + } + + @Override + public Uni persistProcessingState(Map> state) { + // Called on Vert.x event loop + // Return a Uni completing with void when the given states are persisted + // The Uni will be subscribed also on Vert.x event loop + return Uni.createFrom().voidItem(); + } + + @Override + public void close() { + /* Called when channel is closing, no-op by default */ + } +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/kafka/outbound/KafkaCloudEventMultipleProcessors.java b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/outbound/KafkaCloudEventMultipleProcessors.java new file mode 100644 index 000000000..17cea321d --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/outbound/KafkaCloudEventMultipleProcessors.java @@ -0,0 +1,39 @@ +package kafka.outbound; + +import java.net.URI; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.reactive.messaging.ce.OutgoingCloudEventMetadata; + +@ApplicationScoped +public class KafkaCloudEventMultipleProcessors { + + @Incoming("source") + @Outgoing("processed") + public Message process(Message in) { + return in.addMetadata(OutgoingCloudEventMetadata.builder() + .withId("id-" + in.getPayload()) + .withType("greeting") + .build()); + } + + @SuppressWarnings("unchecked") + @Incoming("processed") + @Outgoing("cloud-events") + public Message process2(Message in) { + OutgoingCloudEventMetadata metadata = in + .getMetadata(OutgoingCloudEventMetadata.class) + .orElseGet(() -> OutgoingCloudEventMetadata.builder().build()); + + return in.addMetadata(OutgoingCloudEventMetadata.from(metadata) + .withSource(URI.create("source://me")) + .withSubject("test") + .build()); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/kafka/outbound/KafkaCloudEventProcessor.java b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/outbound/KafkaCloudEventProcessor.java new file mode 100644 index 000000000..8b5d4d91c --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/outbound/KafkaCloudEventProcessor.java @@ -0,0 +1,25 @@ +package kafka.outbound; + +import java.net.URI; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.reactive.messaging.ce.OutgoingCloudEventMetadata; + +@ApplicationScoped +public class KafkaCloudEventProcessor { + + @Outgoing("cloud-events") + public Message toCloudEvents(Message in) { + return in.addMetadata(OutgoingCloudEventMetadata.builder() + .withId("id-" + in.getPayload()) + .withType("greetings") + .withSource(URI.create("http://example.com")) + .withSubject("greeting-message") + .build()); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/kafka/outbound/KafkaCustomHeaderReplier.java b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/outbound/KafkaCustomHeaderReplier.java new file mode 100644 index 000000000..9612a1c60 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/outbound/KafkaCustomHeaderReplier.java @@ -0,0 +1,39 @@ +package kafka.outbound; + +import java.util.Random; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.apache.kafka.common.header.Header; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.reactive.messaging.kafka.reply.KafkaRequestReply; + +@ApplicationScoped +public class KafkaCustomHeaderReplier { + + Random rand = new Random(); + + @Incoming("request") + @Outgoing("reply") + ProducerRecord process(ConsumerRecord request) { + Header topicHeader = request.headers().lastHeader("MY_TOPIC"); + if (topicHeader == null) { + // Skip + return null; + } + String myTopic = new String(topicHeader.value()); + int generateValue = rand.nextInt(100); + Header partitionHeader = request.headers().lastHeader("MY_PARTITION"); + if (partitionHeader == null) { + // Propagate incoming headers, including the correlation id header + return new ProducerRecord<>(myTopic, null, request.key(), generateValue, request.headers()); + } + // Send the replies to extracted myTopic-myPartition + int myPartition = KafkaRequestReply.replyPartitionFromBytes(partitionHeader.value()); + return new ProducerRecord<>(myTopic, myPartition, request.key(), generateValue, request.headers()); + } +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/kafka/outbound/KafkaExactlyOnceProcessor.java b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/outbound/KafkaExactlyOnceProcessor.java new file mode 100644 index 000000000..49b973232 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/outbound/KafkaExactlyOnceProcessor.java @@ -0,0 +1,34 @@ +package kafka.outbound; + +import java.util.List; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.kafka.KafkaRecord; +import io.smallrye.reactive.messaging.kafka.Record; +import io.smallrye.reactive.messaging.kafka.transactions.KafkaTransactions; + +@ApplicationScoped +public class KafkaExactlyOnceProcessor { + + @Inject + @Channel("tx-out-example") + KafkaTransactions txProducer; + + @Incoming("in-channel") + public Uni emitInTransaction(Message>> batch) { + return txProducer.withTransaction(batch, emitter -> { + for (Record record : batch.getPayload()) { + emitter.send(KafkaRecord.of(record.key(), record.value() + 1)); + } + return Uni.createFrom().voidItem(); + }); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/kafka/outbound/KafkaOutboundDynamicTopicExample.java b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/outbound/KafkaOutboundDynamicTopicExample.java new file mode 100644 index 000000000..cca404431 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/outbound/KafkaOutboundDynamicTopicExample.java @@ -0,0 +1,25 @@ +package kafka.outbound; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.kafka.api.OutgoingKafkaRecordMetadata; + +public class KafkaOutboundDynamicTopicExample { + + public Message metadata(Message incoming) { + // tag::code[] + String topicName = selectTopicFromIncommingMessage(incoming); + OutgoingKafkaRecordMetadata metadata = OutgoingKafkaRecordMetadata. builder() + .withTopic(topicName) + .build(); + + // Create a new message from the `incoming` message + // Add `metadata` to the metadata from the `incoming` message. + return incoming.addMetadata(metadata); + // end::code[] + } + + private String selectTopicFromIncommingMessage(Message incoming) { + return "fake"; + } +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/kafka/outbound/KafkaOutboundMetadataExample.java b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/outbound/KafkaOutboundMetadataExample.java new file mode 100644 index 000000000..645f290d2 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/outbound/KafkaOutboundMetadataExample.java @@ -0,0 +1,25 @@ +package kafka.outbound; + +import org.apache.kafka.common.header.internals.RecordHeaders; +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.kafka.api.OutgoingKafkaRecordMetadata; + +public class KafkaOutboundMetadataExample { + + public Message metadata(Message incoming) { + // tag::code[] + // Creates an OutgoingKafkaRecordMetadata + // The type parameter is the type of the record's key + OutgoingKafkaRecordMetadata metadata = OutgoingKafkaRecordMetadata. builder() + .withKey("my-key") + .withHeaders(new RecordHeaders().add("my-header", "value".getBytes())) + .build(); + + // Create a new message from the `incoming` message + // Add `metadata` to the metadata from the `incoming` message. + return incoming.addMetadata(metadata); + // end::code[] + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/kafka/outbound/KafkaPooledExactlyOnceProcessor.java b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/outbound/KafkaPooledExactlyOnceProcessor.java new file mode 100644 index 000000000..8bdf4142c --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/outbound/KafkaPooledExactlyOnceProcessor.java @@ -0,0 +1,32 @@ +package kafka.outbound; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Incoming; + +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.annotations.Blocking; +import io.smallrye.reactive.messaging.kafka.KafkaRecord; +import io.smallrye.reactive.messaging.kafka.api.IncomingKafkaRecordMetadata; +import io.smallrye.reactive.messaging.kafka.transactions.KafkaTransactions; + +@ApplicationScoped +public class KafkaPooledExactlyOnceProcessor { + + @Inject + @Channel("tx-out-example") + KafkaTransactions txProducer; + + @Incoming("in-channel") + @Blocking(ordered = false) + public void process(ConsumerRecord record, IncomingKafkaRecordMetadata metadata) { + txProducer.withTransactionAndAwait(metadata, emitter -> { + emitter.send(KafkaRecord.of(record.key(), record.value() + 1)); + return Uni.createFrom().voidItem(); + }); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/kafka/outbound/KafkaPriceMessageProducer.java b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/outbound/KafkaPriceMessageProducer.java new file mode 100644 index 000000000..52e86b093 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/outbound/KafkaPriceMessageProducer.java @@ -0,0 +1,26 @@ +package kafka.outbound; + +import java.time.Duration; +import java.util.Random; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +@ApplicationScoped +public class KafkaPriceMessageProducer { + + private final Random random = new Random(); + + @Outgoing("prices-out") + public Multi> generate() { + // Build an infinite stream of random prices + // It emits a price every second + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .map(x -> Message.of(random.nextDouble())); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/kafka/outbound/KafkaPriceProcessor.java b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/outbound/KafkaPriceProcessor.java new file mode 100644 index 000000000..7d423ae89 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/outbound/KafkaPriceProcessor.java @@ -0,0 +1,19 @@ +package kafka.outbound; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +@ApplicationScoped +public class KafkaPriceProcessor { + + // tag::code[] + @Incoming("in") + @Outgoing("out") + public double process(int in) { + return in * 0.88; + } + // end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/kafka/outbound/KafkaPriceProducer.java b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/outbound/KafkaPriceProducer.java new file mode 100644 index 000000000..220a0214f --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/outbound/KafkaPriceProducer.java @@ -0,0 +1,25 @@ +package kafka.outbound; + +import java.time.Duration; +import java.util.Random; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +@ApplicationScoped +public class KafkaPriceProducer { + + private final Random random = new Random(); + + @Outgoing("prices-out") + public Multi generate() { + // Build an infinite stream of random prices + // It emits a price every second + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .map(x -> random.nextDouble()); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/kafka/outbound/KafkaProducerRecordExample.java b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/outbound/KafkaProducerRecordExample.java new file mode 100644 index 000000000..0fb3f3dc4 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/outbound/KafkaProducerRecordExample.java @@ -0,0 +1,18 @@ +package kafka.outbound; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.apache.kafka.clients.producer.ProducerRecord; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +@ApplicationScoped +public class KafkaProducerRecordExample { + + // tag::code[] + @Outgoing("out") + public ProducerRecord generate() { + return new ProducerRecord<>("my-topic", "key", "value"); + } + // end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/kafka/outbound/KafkaRecordExample.java b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/outbound/KafkaRecordExample.java new file mode 100644 index 000000000..560850605 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/outbound/KafkaRecordExample.java @@ -0,0 +1,21 @@ +package kafka.outbound; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.reactive.messaging.kafka.Record; + +@ApplicationScoped +public class KafkaRecordExample { + + // tag::code[] + @Incoming("in") + @Outgoing("out") + public Record process(String in) { + return Record.of("my-key", in); + } + // end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/kafka/outbound/KafkaReplier.java b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/outbound/KafkaReplier.java new file mode 100644 index 000000000..8bad449e8 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/outbound/KafkaReplier.java @@ -0,0 +1,20 @@ +package kafka.outbound; + +import java.util.Random; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +@ApplicationScoped +public class KafkaReplier { + + Random rand = new Random(); + + @Incoming("request") + @Outgoing("reply") + int handleRequest(String request) { + return rand.nextInt(100); + } +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/kafka/outbound/KafkaRequestReplyEmitter.java b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/outbound/KafkaRequestReplyEmitter.java new file mode 100644 index 000000000..c1994d090 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/outbound/KafkaRequestReplyEmitter.java @@ -0,0 +1,21 @@ +package kafka.outbound; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; + +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.kafka.reply.KafkaRequestReply; + +@ApplicationScoped +public class KafkaRequestReplyEmitter { + + @Inject + @Channel("my-request") + KafkaRequestReply quoteRequest; + + public Uni requestQuote(String request) { + return quoteRequest.request(request); + } +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/kafka/outbound/KafkaRequestReplyMessageEmitter.java b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/outbound/KafkaRequestReplyMessageEmitter.java new file mode 100644 index 000000000..e70379b62 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/outbound/KafkaRequestReplyMessageEmitter.java @@ -0,0 +1,26 @@ +package kafka.outbound; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.kafka.api.OutgoingKafkaRecordMetadata; +import io.smallrye.reactive.messaging.kafka.reply.KafkaRequestReply; + +@ApplicationScoped +public class KafkaRequestReplyMessageEmitter { + + @Inject + @Channel("my-request") + KafkaRequestReply quoteRequest; + + public Uni> requestMessage(String request) { + return quoteRequest.request(Message.of(request) + .addMetadata(OutgoingKafkaRecordMetadata.builder() + .withKey(request) + .build())); + } +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/kafka/outbound/KafkaRequestReplyMultiEmitter.java b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/outbound/KafkaRequestReplyMultiEmitter.java new file mode 100644 index 000000000..8d8897ba1 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/outbound/KafkaRequestReplyMultiEmitter.java @@ -0,0 +1,21 @@ +package org.acme; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; + +import io.smallrye.mutiny.Multi; +import io.smallrye.reactive.messaging.kafka.reply.KafkaRequestReply; + +@ApplicationScoped +public class KafkaRequestReplyMultiEmitter { + + @Inject + @Channel("my-request") + KafkaRequestReply quoteRequest; + + public Multi requestQuote(String request) { + return quoteRequest.requestMulti(request).select().first(5); + } +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/kafka/outbound/KafkaTransactionalProducer.java b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/outbound/KafkaTransactionalProducer.java new file mode 100644 index 000000000..597c33cc2 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/outbound/KafkaTransactionalProducer.java @@ -0,0 +1,28 @@ +package kafka.outbound; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; + +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.kafka.KafkaRecord; +import io.smallrye.reactive.messaging.kafka.transactions.KafkaTransactions; + +@ApplicationScoped +public class KafkaTransactionalProducer { + + @Inject + @Channel("tx-out-example") + KafkaTransactions txProducer; + + public Uni emitInTransaction() { + return txProducer.withTransaction(emitter -> { + emitter.send(KafkaRecord.of(1, "a")); + emitter.send(KafkaRecord.of(2, "b")); + emitter.send(KafkaRecord.of(3, "c")); + return Uni.createFrom().voidItem(); + }); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/kafka/outbound/MyReplyFailureHandler.java b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/outbound/MyReplyFailureHandler.java new file mode 100644 index 000000000..fcd7f8f9c --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/outbound/MyReplyFailureHandler.java @@ -0,0 +1,23 @@ +package kafka.outbound; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.apache.kafka.common.header.Header; + +import io.smallrye.common.annotation.Identifier; +import io.smallrye.reactive.messaging.kafka.KafkaRecord; +import io.smallrye.reactive.messaging.kafka.reply.ReplyFailureHandler; + +@ApplicationScoped +@Identifier("my-reply-error") +public class MyReplyFailureHandler implements ReplyFailureHandler { + + @Override + public Throwable handleReply(KafkaRecord replyRecord) { + Header header = replyRecord.getHeaders().lastHeader("REPLY_ERROR"); + if (header != null) { + return new IllegalArgumentException(new String(header.value())); + } + return null; + } +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/kafka/outbound/ProducerInterceptorBeanExample.java b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/outbound/ProducerInterceptorBeanExample.java new file mode 100644 index 000000000..a01e555cd --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/kafka/outbound/ProducerInterceptorBeanExample.java @@ -0,0 +1,37 @@ +package kafka.outbound; + +import java.util.Map; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.apache.kafka.clients.producer.ProducerInterceptor; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.apache.kafka.clients.producer.RecordMetadata; + +import io.smallrye.common.annotation.Identifier; + +@ApplicationScoped +@Identifier("my-producer-interceptor") +public class ProducerInterceptorBeanExample implements ProducerInterceptor { + + @Override + public ProducerRecord onSend(ProducerRecord producerRecord) { + // called before send + return producerRecord; + } + + @Override + public void onAcknowledgement(RecordMetadata recordMetadata, Exception e) { + // called on send acknowledgement callback + } + + @Override + public void close() { + // called on client close + } + + @Override + public void configure(Map map) { + // called on client configuration + } +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/keyed/KeyValueExtractorFromPayload.java b/content/docs/smallrye-reactive-messaging/main/_examples/keyed/KeyValueExtractorFromPayload.java new file mode 100644 index 000000000..6191d081b --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/keyed/KeyValueExtractorFromPayload.java @@ -0,0 +1,35 @@ +package keyed; + +import java.lang.reflect.Type; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; + +// tag::code[] +import io.smallrye.reactive.messaging.keyed.KeyValueExtractor; + +@ApplicationScoped +public class KeyValueExtractorFromPayload implements KeyValueExtractor { + + @Override + public boolean canExtract(Message msg, Type keyType, Type valueType) { + // Called for the first message of the stream to select the extractor. + // Here we only check for the type, but the logic can be more complex + return keyType.equals(String.class) && valueType.equals(String.class); + } + + @Override + public String extractKey(Message message, Type keyType) { + String string = message.getPayload().toString(); + return string.substring(0, string.indexOf(":")); + } + + @Override + public String extractValue(Message message, Type valueType) { + String string = message.getPayload().toString(); + return string.substring(string.indexOf(":") + 1); + } + +} +// end::code[] diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/keyed/KeyedExample.java b/content/docs/smallrye-reactive-messaging/main/_examples/keyed/KeyedExample.java new file mode 100644 index 000000000..438a0185e --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/keyed/KeyedExample.java @@ -0,0 +1,25 @@ +package keyed; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; +import io.smallrye.reactive.messaging.keyed.Keyed; +import io.smallrye.reactive.messaging.keyed.KeyedMulti; + +public class KeyedExample { + + // tag::code[] + @Incoming("in") + @Outgoing("out") + public Multi reshape( + @Keyed(KeyValueExtractorFromPayload.class) KeyedMulti multi) { + return multi.onItem().scan(AtomicInteger::new, (i, s) -> { + i.incrementAndGet(); + return i; + }).map(i -> Integer.toString(i.get())); + } + // end::code[] +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/keyed/KeyedMultiExample.java b/content/docs/smallrye-reactive-messaging/main/_examples/keyed/KeyedMultiExample.java new file mode 100644 index 000000000..c917cc8d1 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/keyed/KeyedMultiExample.java @@ -0,0 +1,27 @@ +package keyed; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; +import io.smallrye.reactive.messaging.keyed.KeyedMulti; + +public class KeyedMultiExample { + + // tag::code[] + @Incoming("in") + @Outgoing("out") + public Multi reshape(KeyedMulti multi) { + // Called once per key and receive the stream of value for that specific key + String key = multi.key(); + return multi.onItem().scan(AtomicInteger::new, (i, s) -> { + i.incrementAndGet(); + return i; + }) + .map(i -> Integer.toString(i.get())); + } + // end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/merge/MergeExamples.java b/content/docs/smallrye-reactive-messaging/main/_examples/merge/MergeExamples.java new file mode 100644 index 000000000..fbf4d4ac8 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/merge/MergeExamples.java @@ -0,0 +1,31 @@ +package merge; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.reactive.messaging.annotations.Merge; + +public class MergeExamples { + + // tag::chain[] + @Incoming("in1") + @Outgoing("out") + public int increment(int i) { + return i + 1; + } + + @Incoming("in2") + @Outgoing("out") + public int multiply(int i) { + return i * 2; + } + + @Incoming("out") + @Merge + public void getAll(int i) { + //... + } + + // end::chain[] + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/messages/MessageExamples.java b/content/docs/smallrye-reactive-messaging/main/_examples/messages/MessageExamples.java new file mode 100644 index 000000000..15dbf7e0f --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/messages/MessageExamples.java @@ -0,0 +1,96 @@ +package messages; + +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Metadata; + +@SuppressWarnings({ "unused" }) +public class MessageExamples { + + public void example1() { + Message message = Message.of("hello", Metadata.of(new MyMetadata())); + // tag::message[] + String payload = message.getPayload(); + Optional metadata = message.getMetadata(MyMetadata.class); + // end::message[] + } + + public void creation() { + Price price = new Price(20.5); + // tag::creation[] + // Create a simple message wrapping a payload + Message m1 = Message.of(price); + + // Create a message with metadata + Message m2 = Message.of(price, Metadata.of(new PriceMetadata())); + + // Create a message with several metadata + Message m3 = Message.of(price, + Metadata.of(new PriceMetadata(), new MyMetadata())); + + // Create a message with an acknowledgement callback + Message m4 = Message.of(price, () -> { + // Called when the message is acknowledged by the next consumer. + return CompletableFuture.completedFuture(null); + }); + + // Create a message with both metadata and acknowledgement callback + Message m5 = Message.of(price, + Metadata.of(new PriceMetadata()), + () -> { + // Called when the message is acknowledged by the next consumer. + return CompletableFuture.completedFuture(null); + }); + // end::creation[] + } + + public void copy() { + Price price = new Price(20.5); + Message message = Message.of(price); + // tag::copy[] + + // Create a new message with a new payload but with the same metadata + Message m1 = message.withPayload(new Price(12.4)); + + // Create a new message with a new payload and add another metadata + Message m2 = message + .withPayload(new Price(15.0)) + .withMetadata(Metadata.of(new PriceMetadata())); + + // Create a new message with a new payload and a custom acknowledgement + Message m3 = message + .withPayload(new Price(15.0)) + .withAck(() -> + // acknowledge the incoming message + message.ack() + .thenAccept(x -> { + // do something + })); + // end::copy[] + } + + public void nullPayload() { + // tag::null-payload[] + // Create a message with null payload + Message m6 = Message.of(null, Metadata.of(new PriceMetadata())); + // end::null-payload[] + } + + public static class Price { + final double price; + + public Price(double p) { + this.price = p; + } + } + + static class PriceMetadata { + final long timestamp; + + public PriceMetadata() { + this.timestamp = System.currentTimeMillis(); + } + } +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/mqtt/customization/ClientProducers.java b/content/docs/smallrye-reactive-messaging/main/_examples/mqtt/customization/ClientProducers.java new file mode 100644 index 000000000..fbcaaea08 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/mqtt/customization/ClientProducers.java @@ -0,0 +1,32 @@ +package mqtt.customization; + +import jakarta.enterprise.inject.Produces; + +import io.smallrye.common.annotation.Identifier; +import io.smallrye.reactive.messaging.mqtt.session.MqttClientSessionOptions; +import io.vertx.core.net.PemKeyCertOptions; +import io.vertx.core.net.PemTrustOptions; + +public class ClientProducers { + + // tag::named[] + @Produces + @Identifier("my-options") + public MqttClientSessionOptions getOptions() { + // You can use the produced options to configure the TLS connection + PemKeyCertOptions keycert = new PemKeyCertOptions() + .addCertPath("./tls/tls.crt") + .addKeyPath("./tls/tls.key"); + PemTrustOptions trust = new PemTrustOptions().addCertPath("./tlc/ca.crt"); + + return new MqttClientSessionOptions() + .setSsl(true) + .setKeyCertOptions(keycert) + .setTrustOptions(trust) + .setHostnameVerificationAlgorithm("HTTPS") + .setConnectTimeout(30000) + .setReconnectInterval(5000); + } + // end::named[] + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/mqtt/inbound/MqttPriceConsumer.java b/content/docs/smallrye-reactive-messaging/main/_examples/mqtt/inbound/MqttPriceConsumer.java new file mode 100644 index 000000000..9115d809a --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/mqtt/inbound/MqttPriceConsumer.java @@ -0,0 +1,17 @@ +package mqtt.inbound; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; + +@ApplicationScoped +public class MqttPriceConsumer { + + @Incoming("prices") + public void consume(byte[] raw) { + double price = Double.parseDouble(new String(raw)); + + // process your price. + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/mqtt/inbound/MqttPriceMessageConsumer.java b/content/docs/smallrye-reactive-messaging/main/_examples/mqtt/inbound/MqttPriceMessageConsumer.java new file mode 100644 index 000000000..ed40c96db --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/mqtt/inbound/MqttPriceMessageConsumer.java @@ -0,0 +1,21 @@ +package mqtt.inbound; + +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; + +@ApplicationScoped +public class MqttPriceMessageConsumer { + + @Incoming("prices") + public CompletionStage consume(Message price) { + // process your price. + + // Acknowledge the incoming message + return price.ack(); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/mqtt/outbound/MqttPriceMessageProducer.java b/content/docs/smallrye-reactive-messaging/main/_examples/mqtt/outbound/MqttPriceMessageProducer.java new file mode 100644 index 000000000..744ce54b5 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/mqtt/outbound/MqttPriceMessageProducer.java @@ -0,0 +1,26 @@ +package mqtt.outbound; + +import java.time.Duration; +import java.util.Random; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +@ApplicationScoped +public class MqttPriceMessageProducer { + + private Random random = new Random(); + + @Outgoing("prices") + public Multi> generate() { + // Build an infinite stream of random prices + // It emits a price every second + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .map(x -> Message.of(random.nextDouble())); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/mqtt/outbound/MqttPriceProducer.java b/content/docs/smallrye-reactive-messaging/main/_examples/mqtt/outbound/MqttPriceProducer.java new file mode 100644 index 000000000..22c0998e9 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/mqtt/outbound/MqttPriceProducer.java @@ -0,0 +1,25 @@ +package mqtt.outbound; + +import java.time.Duration; +import java.util.Random; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +@ApplicationScoped +public class MqttPriceProducer { + + private Random random = new Random(); + + @Outgoing("prices") + public Multi generate() { + // Build an infinite stream of random prices + // It emits a price every second + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .map(x -> random.nextDouble()); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/observability/ContextMessageObservationCollector.java b/content/docs/smallrye-reactive-messaging/main/_examples/observability/ContextMessageObservationCollector.java new file mode 100644 index 000000000..608e368dc --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/observability/ContextMessageObservationCollector.java @@ -0,0 +1,57 @@ +package observability; + +import java.time.Duration; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.observation.DefaultMessageObservation; +import io.smallrye.reactive.messaging.observation.MessageObservation; +import io.smallrye.reactive.messaging.observation.MessageObservationCollector; +import io.smallrye.reactive.messaging.observation.ObservationContext; + +@ApplicationScoped +public class ContextMessageObservationCollector + implements MessageObservationCollector { + + @Override + public MyContext initObservation(String channel, boolean incoming, boolean emitter) { + // Called on observation setup, per channel + // if returned null the observation for that channel is disabled + return new MyContext(channel, incoming, emitter); + } + + @Override + public MessageObservation onNewMessage(String channel, Message message, MyContext ctx) { + // Called after message has been created + return new DefaultMessageObservation(channel); + } + + public static class MyContext implements ObservationContext { + + private final String channel; + private final boolean incoming; + private final boolean emitter; + + public MyContext(String channel, boolean incoming, boolean emitter) { + this.channel = channel; + this.incoming = incoming; + this.emitter = emitter; + } + + @Override + public void complete(MessageObservation observation) { + // called after message processing has completed and observation is done + // register duration + Duration duration = observation.getCompletionDuration(); + Throwable reason = observation.getReason(); + if (reason != null) { + // message was nacked + } else { + // message was acked successfully + } + } + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/observability/SimpleMessageObservationCollector.java b/content/docs/smallrye-reactive-messaging/main/_examples/observability/SimpleMessageObservationCollector.java new file mode 100644 index 000000000..838bceb5f --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/observability/SimpleMessageObservationCollector.java @@ -0,0 +1,21 @@ +package observability; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.observation.DefaultMessageObservation; +import io.smallrye.reactive.messaging.observation.MessageObservation; +import io.smallrye.reactive.messaging.observation.MessageObservationCollector; +import io.smallrye.reactive.messaging.observation.ObservationContext; + +@ApplicationScoped +public class SimpleMessageObservationCollector implements MessageObservationCollector { + + @Override + public MessageObservation onNewMessage(String channel, Message message, ObservationContext ctx) { + // Called after message has been created + return new DefaultMessageObservation(channel); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/outgoings/OutgoingsExample.java b/content/docs/smallrye-reactive-messaging/main/_examples/outgoings/OutgoingsExample.java new file mode 100644 index 000000000..4e5bf44db --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/outgoings/OutgoingsExample.java @@ -0,0 +1,18 @@ +package outgoings; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +public class OutgoingsExample { + + //tag::code[] + @Incoming("in") + @Outgoing("out1") + @Outgoing("out2") + public String process(String s) { + // send messages from channel-in to both channel-out1 and channel-out2 + return s.toUpperCase(); + } + //end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/outgoings/SplitterMultiExample.java b/content/docs/smallrye-reactive-messaging/main/_examples/outgoings/SplitterMultiExample.java new file mode 100644 index 000000000..550443c61 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/outgoings/SplitterMultiExample.java @@ -0,0 +1,37 @@ +package outgoings; + +import java.util.Objects; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.operators.multi.split.MultiSplitter; + +public class SplitterMultiExample { + + // tag::code[] + enum Caps { + ALL_CAPS, + ALL_LOW, + MIXED + } + + @Incoming("in") + @Outgoing("sink1") + @Outgoing("sink2") + @Outgoing("sink3") + public MultiSplitter reshape(Multi in) { + return in.split(Caps.class, s -> { + if (Objects.equals(s, s.toLowerCase())) { + return Caps.ALL_LOW; + } else if (Objects.equals(s, s.toUpperCase())) { + return Caps.ALL_CAPS; + } else { + return Caps.MIXED; + } + }); + } + // end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/outgoings/TargetedExample.java b/content/docs/smallrye-reactive-messaging/main/_examples/outgoings/TargetedExample.java new file mode 100644 index 000000000..7835ef9d8 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/outgoings/TargetedExample.java @@ -0,0 +1,44 @@ +package outgoings; + +import java.util.Map; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.reactive.messaging.Messages; +import io.smallrye.reactive.messaging.Targeted; +import io.smallrye.reactive.messaging.TargetedMessages; + +public class TargetedExample { + + //tag::code[] + @Incoming("in") + @Outgoing("out1") + @Outgoing("out2") + @Outgoing("out3") + public Targeted process(double price) { + // send messages from channel-in to both channel-out1 and channel-out2 + Targeted targeted = Targeted.of("out1", "Price: " + price, + "out2", "Quote: " + price); + if (price > 90.0) { + return targeted.with("out3", price); + } + return targeted; + } + //end::code[] + + //tag::targeted-messages[] + @Incoming("channel-in") + @Outgoing("channel-out1") + @Outgoing("channel-out2") + @Outgoing("channel-out3") + public TargetedMessages processMessage(Message msg) { + // send messages from channel-in to both channel-out1 and channel-out2 + return Messages.chain(msg) + .with(Map.of("channel-out1", msg.withPayload(msg.getPayload().toUpperCase()), + "channel-out2", msg.withPayload(msg.getPayload().toLowerCase()))); + } + //end::targeted-messages[] + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/pausable/PausableController.java b/content/docs/smallrye-reactive-messaging/main/_examples/pausable/PausableController.java new file mode 100644 index 000000000..f95612833 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/pausable/PausableController.java @@ -0,0 +1,46 @@ +package pausable; + +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Incoming; + +import io.smallrye.reactive.messaging.ChannelRegistry; +import io.smallrye.reactive.messaging.PausableChannel; + +@ApplicationScoped +public class PausableController { + + // Option 1: Inject ChannelRegistry and retrieve the pausable channel + @Inject + ChannelRegistry registry; + + // Option 2: Directly inject the pausable channel + @Inject + @Channel("my-channel") + PausableChannel myChannel; + + @PostConstruct + public void resume() { + // Wait for the application to be ready + // Option 1: Retrieve the pausable channel from the registry + PausableChannel pausable = registry.getPausable("my-channel"); + // Resume the processing of the messages + pausable.resume(); + } + + public void pause() { + // Option 2: Use the injected pausable channel + + // Pause the processing of the messages + myChannel.pause(); + } + + @Incoming("my-channel") + void process(String message) { + // Process the message + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/processing/ProcessingExamples.java b/content/docs/smallrye-reactive-messaging/main/_examples/processing/ProcessingExamples.java new file mode 100644 index 000000000..548722816 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/processing/ProcessingExamples.java @@ -0,0 +1,72 @@ +package processing; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Uni; + +public class ProcessingExamples { + + // tag::process-message[] + @Incoming("in") + @Outgoing("out") + public Message processMessage(Message in) { + return in.withPayload(Integer.toString(in.getPayload())); + } + // end::process-message[] + + // tag::process-message-cs[] + @Incoming("in") + @Outgoing("out") + public CompletionStage> processMessageCS(Message in) { + CompletionStage cs = invokeService(in.getPayload()); + return cs.thenApply(in::withPayload); + } + // end::process-message-cs[] + + // tag::process-message-uni[] + @Incoming("in") + @Outgoing("out") + public Uni> processMessageUni(Message in) { + return invokeService(in.getPayload()) + .map(in::withPayload); + } + // end::process-message-uni[] + + // tag::process-payload[] + @Incoming("in") + @Outgoing("out") + public String processPayload(int in) { + return Integer.toString(in); + } + // end::process-payload[] + + // tag::process-payload-cs[] + @Incoming("in") + @Outgoing("out") + public CompletionStage processPayloadCS(int in) { + return invokeService(in); + } + // end::process-payload-cs[] + + // tag::process-payload-uni[] + @Incoming("in") + @Outgoing("out") + public Uni processPayload(String in) { + return invokeService(in); + } + // end::process-payload-uni[] + + private CompletionStage invokeService(int param) { + return CompletableFuture.completedFuture("ok"); + } + + private Uni invokeService(String param) { + return Uni.createFrom().item("ok"); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/processing/StreamExamples.java b/content/docs/smallrye-reactive-messaging/main/_examples/processing/StreamExamples.java new file mode 100644 index 000000000..c33bfecd8 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/processing/StreamExamples.java @@ -0,0 +1,41 @@ +package processing; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; + +public class StreamExamples { + + // tag::processing-stream-message[] + @Incoming("in") + @Outgoing("out") + public Multi> processMessageStream(Multi> stream) { + return stream + .onItem().transformToUni(message -> invokeService(message.getPayload()) + .onFailure().recoverWithItem("fallback") + .onItem().transform(message::withPayload)) + .concatenate(); + + } + // end::processing-stream-message[] + + // tag::processing-stream-payload[] + @Incoming("in") + @Outgoing("out") + public Multi processPayloadStream(Multi stream) { + return stream + .onItem().transformToUni(payload -> invokeService(payload) + .onFailure().recoverWithItem("fallback")) + .concatenate(); + + } + // end::processing-stream-payload[] + + private Uni invokeService(int i) { + return Uni.createFrom().item("ok " + i); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/pulsar/configuration/PulsarClientConfigProducers.java b/content/docs/smallrye-reactive-messaging/main/_examples/pulsar/configuration/PulsarClientConfigProducers.java new file mode 100644 index 000000000..89f1d31c5 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/pulsar/configuration/PulsarClientConfigProducers.java @@ -0,0 +1,66 @@ +package pulsar.configuration; + +import java.util.HashMap; +import java.util.Map; + +import jakarta.enterprise.inject.Produces; + +import org.apache.pulsar.client.api.BatcherBuilder; +import org.apache.pulsar.client.api.PulsarClientSharedResources; +import org.apache.pulsar.client.impl.AutoClusterFailover; +import org.apache.pulsar.client.impl.DefaultCryptoKeyReader; +import org.apache.pulsar.client.impl.conf.ClientConfigurationData; +import org.apache.pulsar.client.impl.conf.ConsumerConfigurationData; +import org.apache.pulsar.client.impl.customroute.PartialRoundRobinMessageRouterImpl; + +import io.smallrye.common.annotation.Identifier; + +public class PulsarClientConfigProducers { + + // tag::consumer[] + @Produces + @Identifier("my-consumer-options") + public ConsumerConfigurationData getConsumerConfig() { + ConsumerConfigurationData data = new ConsumerConfigurationData<>(); + data.setAckReceiptEnabled(true); + data.setCryptoKeyReader(DefaultCryptoKeyReader.builder() + //... + .build()); + return data; + } + // end::consumer[] + + // tag::client[] + @Produces + @Identifier("prices") + public ClientConfigurationData getClientConfig() { + ClientConfigurationData data = new ClientConfigurationData(); + data.setEnableTransaction(true); + data.setServiceUrlProvider(AutoClusterFailover.builder() + // ... + .build()); + return data; + } + // end::client[] + + // tag::shared-resources[] + @Produces + public PulsarClientSharedResources getSharedResources() { + return PulsarClientSharedResources.builder() + .configureEventLoop(config -> config.numberOfThreads(4)) + .build(); + } + // end::shared-resources[] + + // tag::producer[] + @Produces + @Identifier("prices") + public Map getProducerconfig() { + Map map = new HashMap<>(); + map.put("batcherBuilder", BatcherBuilder.KEY_BASED); + map.put("sendTimeoutMs", 3000); + map.put("customMessageRouter", new PartialRoundRobinMessageRouterImpl(4)); + return map; + } + // end::producer[] +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/pulsar/configuration/PulsarSchemaProvider.java b/content/docs/smallrye-reactive-messaging/main/_examples/pulsar/configuration/PulsarSchemaProvider.java new file mode 100644 index 000000000..3fe6141d0 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/pulsar/configuration/PulsarSchemaProvider.java @@ -0,0 +1,30 @@ +package pulsar.configuration; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; + +import org.apache.pulsar.client.api.Schema; +import org.apache.pulsar.common.schema.KeyValue; +import org.apache.pulsar.common.schema.KeyValueEncodingType; + +import io.smallrye.common.annotation.Identifier; + +@ApplicationScoped +public class PulsarSchemaProvider { + + @Produces + @Identifier("user-schema") + Schema userSchema = Schema.AVRO(User.class); + + @Produces + @Identifier("a-channel") + Schema> keyValueSchema() { + return Schema.KeyValue(Schema.INT32, Schema.JSON(User.class), KeyValueEncodingType.SEPARATED); + } + + public static class User { + String name; + int age; + + } +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/pulsar/inbound/PulsarMessageBatchExample.java b/content/docs/smallrye-reactive-messaging/main/_examples/pulsar/inbound/PulsarMessageBatchExample.java new file mode 100644 index 000000000..9ce9eead1 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/pulsar/inbound/PulsarMessageBatchExample.java @@ -0,0 +1,54 @@ +package pulsar.inbound; + +import java.util.List; +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.TracingMetadata; +import io.smallrye.reactive.messaging.pulsar.PulsarIncomingBatchMessageMetadata; + +@ApplicationScoped +public class PulsarMessageBatchExample { + + // tag::code[] + @Incoming("prices") + public CompletionStage consumeMessage(Message> messages) { + messages.getMetadata(PulsarIncomingBatchMessageMetadata.class).ifPresent(metadata -> { + for (org.apache.pulsar.client.api.Message message : metadata.getMessages()) { + String key = message.getKey(); + String topic = message.getTopicName(); + long timestamp = message.getEventTime(); + //... process messages + } + }); + // ack will commit the latest offsets (per partition) of the batch. + return messages.ack(); + } + + @Incoming("prices") + public void consumeMessages(org.apache.pulsar.client.api.Messages messages) { + for (org.apache.pulsar.client.api.Message msg : messages) { + //... process messages + } + } + // end::code[] + + // tag::batch[] + @Incoming("prices") + public void consumeMessages(org.apache.pulsar.client.api.Messages messages, + PulsarIncomingBatchMessageMetadata metadata) { + for (org.apache.pulsar.client.api.Message message : messages) { + TracingMetadata tracing = metadata.getMetadataForMessage(message, TracingMetadata.class); + if (tracing != null) { + tracing.getCurrentContext().makeCurrent(); + } + //... process messages + } + } + // end::batch[] + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/pulsar/inbound/PulsarMessageBatchPayloadExample.java b/content/docs/smallrye-reactive-messaging/main/_examples/pulsar/inbound/PulsarMessageBatchPayloadExample.java new file mode 100644 index 000000000..649b7d1ae --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/pulsar/inbound/PulsarMessageBatchPayloadExample.java @@ -0,0 +1,21 @@ +package pulsar.inbound; + +import java.util.List; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; + +@ApplicationScoped +public class PulsarMessageBatchPayloadExample { + + // tag::code[] + @Incoming("prices") + public void consume(List prices) { + for (double price : prices) { + // process price + } + } + // end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/pulsar/inbound/PulsarMetadataExample.java b/content/docs/smallrye-reactive-messaging/main/_examples/pulsar/inbound/PulsarMetadataExample.java new file mode 100644 index 000000000..a4add7ea1 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/pulsar/inbound/PulsarMetadataExample.java @@ -0,0 +1,35 @@ +package pulsar.inbound; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.pulsar.PulsarIncomingMessageMetadata; + +public class PulsarMetadataExample { + + @SuppressWarnings("unchecked") + public void metadata() { + Message incoming = Message.of(12.0); + // tag::code[] + PulsarIncomingMessageMetadata metadata = incoming.getMetadata(PulsarIncomingMessageMetadata.class).orElse(null); + if (metadata != null) { + // The topic + String topic = metadata.getTopicName(); + + // The key + String key = metadata.getKey(); + + // The event time + long timestamp = metadata.getEventTime(); + + // The raw data + byte[] rawData = metadata.getData(); + + // The underlying message + org.apache.pulsar.client.api.Message message = metadata.getMessage(); + + // ... + } + // end::code[] + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/pulsar/inbound/PulsarPriceConsumer.java b/content/docs/smallrye-reactive-messaging/main/_examples/pulsar/inbound/PulsarPriceConsumer.java new file mode 100644 index 000000000..9973a6158 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/pulsar/inbound/PulsarPriceConsumer.java @@ -0,0 +1,15 @@ +package pulsar.inbound; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; + +@ApplicationScoped +public class PulsarPriceConsumer { + + @Incoming("prices") + public void consume(double price) { + // process your price. + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/pulsar/inbound/PulsarPriceMessageConsumer.java b/content/docs/smallrye-reactive-messaging/main/_examples/pulsar/inbound/PulsarPriceMessageConsumer.java new file mode 100644 index 000000000..4cdc3f12a --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/pulsar/inbound/PulsarPriceMessageConsumer.java @@ -0,0 +1,21 @@ +package pulsar.inbound; + +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; + +@ApplicationScoped +public class PulsarPriceMessageConsumer { + + @Incoming("prices") + public CompletionStage consume(Message price) { + // process your price. + + // Acknowledge the incoming message + return price.ack(); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/pulsar/outbound/PulsarExactlyOnceProcessor.java b/content/docs/smallrye-reactive-messaging/main/_examples/pulsar/outbound/PulsarExactlyOnceProcessor.java new file mode 100644 index 000000000..c207cba70 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/pulsar/outbound/PulsarExactlyOnceProcessor.java @@ -0,0 +1,35 @@ +package pulsar.outbound; + +import java.util.List; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.pulsar.PulsarIncomingBatchMessageMetadata; +import io.smallrye.reactive.messaging.pulsar.PulsarMessage; +import io.smallrye.reactive.messaging.pulsar.transactions.PulsarTransactions; + +@ApplicationScoped +public class PulsarExactlyOnceProcessor { + + @Inject + @Channel("tx-out-example") + PulsarTransactions txProducer; + + @Incoming("in-channel") + public Uni emitInTransaction(Message> batch) { + return txProducer.withTransactionAndAck(batch, emitter -> { + PulsarIncomingBatchMessageMetadata metadata = batch.getMetadata(PulsarIncomingBatchMessageMetadata.class).get(); + for (org.apache.pulsar.client.api.Message message : metadata. getMessages()) { + emitter.send(PulsarMessage.of(message.getValue() + 1, message.getKey())); + } + return Uni.createFrom().voidItem(); + }); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/pulsar/outbound/PulsarKeyValueExample.java b/content/docs/smallrye-reactive-messaging/main/_examples/pulsar/outbound/PulsarKeyValueExample.java new file mode 100644 index 000000000..ab0b43a6c --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/pulsar/outbound/PulsarKeyValueExample.java @@ -0,0 +1,28 @@ +package pulsar.outbound; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; + +import org.apache.pulsar.client.api.Schema; +import org.apache.pulsar.common.schema.KeyValue; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.common.annotation.Identifier; + +@ApplicationScoped +public class PulsarKeyValueExample { + + // tag::code[] + @Identifier("out") + @Produces + Schema> schema = Schema.KeyValue(Schema.STRING, Schema.INT64); + + @Incoming("in") + @Outgoing("out") + public KeyValue process(long in) { + return new KeyValue<>("my-key", in); + } + // end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/pulsar/outbound/PulsarOutboundMetadataExample.java b/content/docs/smallrye-reactive-messaging/main/_examples/pulsar/outbound/PulsarOutboundMetadataExample.java new file mode 100644 index 000000000..152974fe2 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/pulsar/outbound/PulsarOutboundMetadataExample.java @@ -0,0 +1,26 @@ +package pulsar.outbound; + +import java.util.Map; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.pulsar.PulsarOutgoingMessageMetadata; + +public class PulsarOutboundMetadataExample { + + public Message metadata(Message incoming) { + // tag::code[] + // Creates an PulsarOutgoingMessageMetadata + // The type parameter is the type of the record's key + PulsarOutgoingMessageMetadata metadata = PulsarOutgoingMessageMetadata.builder() + .withKey("my-key") + .withProperties(Map.of("property-key", "value")) + .build(); + + // Create a new message from the `incoming` message + // Add `metadata` to the metadata from the `incoming` message. + return incoming.addMetadata(metadata); + // end::code[] + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/pulsar/outbound/PulsarOutgoingMessageExample.java b/content/docs/smallrye-reactive-messaging/main/_examples/pulsar/outbound/PulsarOutgoingMessageExample.java new file mode 100644 index 000000000..f452f437e --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/pulsar/outbound/PulsarOutgoingMessageExample.java @@ -0,0 +1,18 @@ +package pulsar.outbound; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.reactive.messaging.pulsar.OutgoingMessage; + +public class PulsarOutgoingMessageExample { + + // tag::code[] + @Incoming("in") + @Outgoing("out") + OutgoingMessage process(org.apache.pulsar.client.api.Message in) { + return OutgoingMessage.from(in) + .withValue(Long.valueOf(in.getValue())); + } + // end::code[] +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/pulsar/outbound/PulsarPriceMessageProducer.java b/content/docs/smallrye-reactive-messaging/main/_examples/pulsar/outbound/PulsarPriceMessageProducer.java new file mode 100644 index 000000000..1f53f7c68 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/pulsar/outbound/PulsarPriceMessageProducer.java @@ -0,0 +1,26 @@ +package pulsar.outbound; + +import java.time.Duration; +import java.util.Random; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +@ApplicationScoped +public class PulsarPriceMessageProducer { + + private Random random = new Random(); + + @Outgoing("prices") + public Multi> generate() { + // Build an infinite stream of random prices + // It emits a price every second + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .map(x -> Message.of(random.nextDouble())); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/pulsar/outbound/PulsarPriceProducer.java b/content/docs/smallrye-reactive-messaging/main/_examples/pulsar/outbound/PulsarPriceProducer.java new file mode 100644 index 000000000..2073b7763 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/pulsar/outbound/PulsarPriceProducer.java @@ -0,0 +1,25 @@ +package pulsar.outbound; + +import java.time.Duration; +import java.util.Random; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +@ApplicationScoped +public class PulsarPriceProducer { + + private Random random = new Random(); + + @Outgoing("prices") + public Multi generate() { + // Build an infinite stream of random prices + // It emits a price every second + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .map(x -> random.nextDouble()); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/pulsar/outbound/PulsarTransactionalProducer.java b/content/docs/smallrye-reactive-messaging/main/_examples/pulsar/outbound/PulsarTransactionalProducer.java new file mode 100644 index 000000000..8bee09bb3 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/pulsar/outbound/PulsarTransactionalProducer.java @@ -0,0 +1,38 @@ +package pulsar.outbound; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.pulsar.OutgoingMessage; +import io.smallrye.reactive.messaging.pulsar.transactions.PulsarTransactions; + +@ApplicationScoped +public class PulsarTransactionalProducer { + + @Inject + @Channel("tx-out-example") + PulsarTransactions> txProducer; + + @Inject + @Channel("other-producer") + PulsarTransactions producer; + + @Incoming("in") + public Uni emitInTransaction(Message in) { + return txProducer.withTransaction(emitter -> { + emitter.send(OutgoingMessage.of("a", 1)); + emitter.send(OutgoingMessage.of("b", 2)); + emitter.send(OutgoingMessage.of("c", 3)); + producer.send(emitter, "4"); + producer.send(emitter, "5"); + producer.send(emitter, "6"); + return Uni.createFrom().completionStage(in::ack); + }); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/quickstart/ReactiveMessagingExample.java b/content/docs/smallrye-reactive-messaging/main/_examples/quickstart/ReactiveMessagingExample.java new file mode 100644 index 000000000..318b39739 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/quickstart/ReactiveMessagingExample.java @@ -0,0 +1,35 @@ +package quickstart; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +@ApplicationScoped +public class ReactiveMessagingExample { + + @Outgoing("source") + public Multi source() { + return Multi.createFrom().items("hello", "from", "SmallRye", "reactive", "messaging"); + } + + @Incoming("source") + @Outgoing("processed-a") + public String toUpperCase(String payload) { + return payload.toUpperCase(); + } + + @Incoming("processed-a") + @Outgoing("processed-b") + public Multi filter(Multi input) { + return input.select().where(item -> item.length() > 4); + } + + @Incoming("processed-b") + public void sink(String word) { + System.out.println(">> " + word); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/rabbitmq/outbound/MyRabbitMQReplyFailureHandler.java b/content/docs/smallrye-reactive-messaging/main/_examples/rabbitmq/outbound/MyRabbitMQReplyFailureHandler.java new file mode 100644 index 000000000..637edb2bc --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/rabbitmq/outbound/MyRabbitMQReplyFailureHandler.java @@ -0,0 +1,21 @@ +package rabbitmq.outbound; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.smallrye.common.annotation.Identifier; +import io.smallrye.reactive.messaging.rabbitmq.IncomingRabbitMQMessage; +import io.smallrye.reactive.messaging.rabbitmq.reply.ReplyFailureHandler; + +@ApplicationScoped +@Identifier("my-reply-error") +public class MyRabbitMQReplyFailureHandler implements ReplyFailureHandler { + + @Override + public Throwable handleReply(IncomingRabbitMQMessage replyMessage) { + var error = (String) replyMessage.getHeaders().get("REPLY_ERROR"); + if (error != null) { + return (Throwable) new IllegalArgumentException(error); + } + return null; + } +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/rabbitmq/outbound/RabbitMQReplier.java b/content/docs/smallrye-reactive-messaging/main/_examples/rabbitmq/outbound/RabbitMQReplier.java new file mode 100644 index 000000000..aec8bfa73 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/rabbitmq/outbound/RabbitMQReplier.java @@ -0,0 +1,20 @@ +package rabbitmq.outbound; + +import java.util.Random; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +@ApplicationScoped +public class RabbitMQReplier { + + Random rand = new Random(); + + @Incoming("request") + @Outgoing("reply") + String handleRequest(String request) { + return request + "-" + rand.nextInt(100); + } +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/rabbitmq/outbound/RabbitMQRequestReplyEmitter.java b/content/docs/smallrye-reactive-messaging/main/_examples/rabbitmq/outbound/RabbitMQRequestReplyEmitter.java new file mode 100644 index 000000000..019255318 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/rabbitmq/outbound/RabbitMQRequestReplyEmitter.java @@ -0,0 +1,21 @@ +package rabbitmq.outbound; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; + +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.rabbitmq.reply.RabbitMQRequestReply; + +@ApplicationScoped +public class RabbitMQRequestReplyEmitter { + + @Inject + @Channel("my-request") + RabbitMQRequestReply quoteRequest; + + public Uni requestQuote(String request) { + return quoteRequest.request(request); + } +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/rabbitmq/outbound/RabbitMQRequestReplyMessageEmitter.java b/content/docs/smallrye-reactive-messaging/main/_examples/rabbitmq/outbound/RabbitMQRequestReplyMessageEmitter.java new file mode 100644 index 000000000..96b9b0f81 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/rabbitmq/outbound/RabbitMQRequestReplyMessageEmitter.java @@ -0,0 +1,22 @@ +package rabbitmq.outbound; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.rabbitmq.reply.RabbitMQRequestReply; + +@ApplicationScoped +public class RabbitMQRequestReplyMessageEmitter { + + @Inject + @Channel("my-request") + RabbitMQRequestReply quoteRequest; + + public Uni> requestMessage(String request) { + return quoteRequest.request(Message.of(request)); + } +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/rabbitmq/outbound/RabbitMQRequestReplyMultiEmitter.java b/content/docs/smallrye-reactive-messaging/main/_examples/rabbitmq/outbound/RabbitMQRequestReplyMultiEmitter.java new file mode 100644 index 000000000..2069164d0 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/rabbitmq/outbound/RabbitMQRequestReplyMultiEmitter.java @@ -0,0 +1,21 @@ +package rabbitmq.outbound; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; + +import io.smallrye.mutiny.Multi; +import io.smallrye.reactive.messaging.rabbitmq.reply.RabbitMQRequestReply; + +@ApplicationScoped +public class RabbitMQRequestReplyMultiEmitter { + + @Inject + @Channel("my-request") + RabbitMQRequestReply quoteRequest; + + public Multi requestQuote(String request) { + return quoteRequest.requestMulti(request).select().first(5); + } +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/skip/SingleSkip.java b/content/docs/smallrye-reactive-messaging/main/_examples/skip/SingleSkip.java new file mode 100644 index 000000000..f7209220b --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/skip/SingleSkip.java @@ -0,0 +1,59 @@ +package skip; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Uni; + +public class SingleSkip { + + // tag::skip[] + // Skip when processing payload synchronously - returning `null` + @Incoming("in") + @Outgoing("out") + public String processPayload(String s) { + if (s.equalsIgnoreCase("skip")) { + return null; + } + return s.toUpperCase(); + } + + // Skip when processing message synchronously - returning `null` + @Incoming("in") + @Outgoing("out") + public Message processMessage(Message m) { + String s = m.getPayload(); + if (s.equalsIgnoreCase("skip")) { + m.ack(); + return null; + } + return m.withPayload(s.toUpperCase()); + } + + // Skip when processing payload asynchronously - returning a `Uni` with a `null` value + @Incoming("in") + @Outgoing("out") + public Uni processPayloadAsync(String s) { + if (s.equalsIgnoreCase("skip")) { + // Important, you must not return `null`, but a `null` content + return Uni.createFrom().nullItem(); + } + return Uni.createFrom().item(s.toUpperCase()); + } + + // Skip when processing message asynchronously - returning a `Uni` with a `null` value + @Incoming("in") + @Outgoing("out") + public Uni> processMessageAsync(Message m) { + String s = m.getPayload(); + if (s.equalsIgnoreCase("skip")) { + m.ack(); + return Uni.createFrom().nullItem(); + } + return Uni.createFrom().item(m.withPayload(s.toUpperCase())); + } + + // end::skip[] + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/skip/StreamSkip.java b/content/docs/smallrye-reactive-messaging/main/_examples/skip/StreamSkip.java new file mode 100644 index 000000000..2108a1077 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/skip/StreamSkip.java @@ -0,0 +1,48 @@ +package skip; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +public class StreamSkip { + + // tag::skip[] + @Incoming("in") + @Outgoing("out-1") + public Multi processPayload(String s) { + if (s.equalsIgnoreCase("skip")) { + return Multi.createFrom().empty(); + } + return Multi.createFrom().item(s.toUpperCase()); + } + + @Incoming("in") + @Outgoing("out-2") + public Multi> processMessage(Message m) { + String s = m.getPayload(); + if (s.equalsIgnoreCase("skip")) { + return Multi.createFrom().empty(); + } + return Multi.createFrom().item(m.withPayload(s.toUpperCase())); + } + + @Incoming("in") + @Outgoing("out-3") + public Multi processPayloadStream(Multi stream) { + return stream + .select().where(s -> !s.equalsIgnoreCase("skip")) + .onItem().transform(String::toUpperCase); + } + + @Incoming("in") + @Outgoing("out-4") + public Multi> processMessageStream(Multi> stream) { + return stream + .select().where(m -> !m.getPayload().equalsIgnoreCase("skip")) + .onItem().transform(m -> m.withPayload(m.getPayload().toUpperCase())); + } + // end::skip[] + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/sns/outbound/SnsMessageProducer.java b/content/docs/smallrye-reactive-messaging/main/_examples/sns/outbound/SnsMessageProducer.java new file mode 100644 index 000000000..cc2e815d6 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/sns/outbound/SnsMessageProducer.java @@ -0,0 +1,27 @@ +package sns.outbound; + +import java.time.Duration; +import java.util.UUID; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Metadata; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; +import io.smallrye.reactive.messaging.aws.sns.SnsOutboundMetadata; + +@ApplicationScoped +public class SnsMessageProducer { + + @Outgoing("prices") + public Multi> generate() { + // It emits a UUID every second + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .map(x -> Message.of(UUID.randomUUID().toString(), + Metadata.of(SnsOutboundMetadata.builder() + .groupId("group-1") + .build()))); + } +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/sns/outbound/SnsOutboundMetadataExample.java b/content/docs/smallrye-reactive-messaging/main/_examples/sns/outbound/SnsOutboundMetadataExample.java new file mode 100644 index 000000000..9d44dacb0 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/sns/outbound/SnsOutboundMetadataExample.java @@ -0,0 +1,29 @@ +package sns.outbound; + +import java.util.Map; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Metadata; + +import io.smallrye.reactive.messaging.aws.sns.SnsOutboundMetadata; +import software.amazon.awssdk.services.sns.model.MessageAttributeValue; + +@ApplicationScoped +public class SnsOutboundMetadataExample { + + public Message metadata(Message incoming) { + // tag::code[] + final SnsOutboundMetadata metadata = SnsOutboundMetadata.builder() + .messageAttributes(Map.of("my-attribute", MessageAttributeValue.builder() + .dataType("String").stringValue("my-value").build())) + .groupId("group-1") + .smsPhoneNumber("+1234567890") + .build(); + + // Add `metadata` to the metadata of the outgoing message. + return Message.of("Hello", Metadata.of(metadata)); + // end::code[] + } +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/sns/outbound/SnsProducer.java b/content/docs/smallrye-reactive-messaging/main/_examples/sns/outbound/SnsProducer.java new file mode 100644 index 000000000..cde773fa6 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/sns/outbound/SnsProducer.java @@ -0,0 +1,22 @@ +package sns.outbound; + +import java.time.Duration; +import java.util.UUID; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +@ApplicationScoped +public class SnsProducer { + + @Outgoing("data") + public Multi generate() { + // It emits a UUID every second + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .map(x -> UUID.randomUUID().toString()); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/sqs/inbound/SqsCustomAckStrategy.java b/content/docs/smallrye-reactive-messaging/main/_examples/sqs/inbound/SqsCustomAckStrategy.java new file mode 100644 index 000000000..ad8b679ba --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/sqs/inbound/SqsCustomAckStrategy.java @@ -0,0 +1,36 @@ +package sqs.inbound; + +import java.util.function.BiConsumer; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.smallrye.common.annotation.Identifier; +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.aws.sqs.SqsAckHandler; +import io.smallrye.reactive.messaging.aws.sqs.SqsConnectorIncomingConfiguration; +import io.smallrye.reactive.messaging.aws.sqs.SqsMessage; +import io.vertx.mutiny.core.Vertx; +import software.amazon.awssdk.services.sqs.SqsAsyncClient; + +public class SqsCustomAckStrategy implements SqsAckHandler { + + @ApplicationScoped + @Identifier("custom") + public static class Factory implements SqsAckHandler.Factory { + + @Override + public SqsAckHandler create(SqsConnectorIncomingConfiguration conf, + Vertx vertx, + SqsAsyncClient client, + Uni queueUrlUni, + BiConsumer reportFailure) { + return new SqsCustomAckStrategy(); + } + } + + @Override + public Uni handle(SqsMessage message) { + return Uni.createFrom().voidItem() + .emitOn(message::runOnMessageContext); + } +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/sqs/inbound/SqsCustomNackStrategy.java b/content/docs/smallrye-reactive-messaging/main/_examples/sqs/inbound/SqsCustomNackStrategy.java new file mode 100644 index 000000000..6daeec399 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/sqs/inbound/SqsCustomNackStrategy.java @@ -0,0 +1,33 @@ +package sqs.inbound; + +import java.util.function.BiConsumer; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Metadata; + +import io.smallrye.common.annotation.Identifier; +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.aws.sqs.SqsFailureHandler; +import io.smallrye.reactive.messaging.aws.sqs.SqsMessage; +import software.amazon.awssdk.services.sqs.SqsAsyncClient; + +public class SqsCustomNackStrategy implements SqsFailureHandler { + + @ApplicationScoped + @Identifier("custom") + public static class Factory implements SqsFailureHandler.Factory { + + @Override + public SqsFailureHandler create(String channel, SqsAsyncClient client, Uni queueUrlUni, + BiConsumer reportFailure) { + return new SqsCustomNackStrategy(); + } + } + + @Override + public Uni handle(SqsMessage message, Metadata metadata, Throwable throwable) { + return Uni.createFrom().voidItem() + .emitOn(message::runOnMessageContext); + } +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/sqs/inbound/SqsMessageStringConsumer.java b/content/docs/smallrye-reactive-messaging/main/_examples/sqs/inbound/SqsMessageStringConsumer.java new file mode 100644 index 000000000..218e26f0c --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/sqs/inbound/SqsMessageStringConsumer.java @@ -0,0 +1,17 @@ +package sqs.inbound; + +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; + +@ApplicationScoped +public class SqsMessageStringConsumer { + @Incoming("data") + CompletionStage consume(Message msg) { + System.out.println("Received: " + msg.getPayload()); + return msg.ack(); + } +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/sqs/inbound/SqsMetadataExample.java b/content/docs/smallrye-reactive-messaging/main/_examples/sqs/inbound/SqsMetadataExample.java new file mode 100644 index 000000000..cd1b4a5aa --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/sqs/inbound/SqsMetadataExample.java @@ -0,0 +1,21 @@ +package sqs.inbound; + +import java.util.Map; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; + +import io.smallrye.reactive.messaging.aws.sqs.SqsIncomingMetadata; +import software.amazon.awssdk.services.sqs.model.MessageAttributeValue; + +@ApplicationScoped +public class SqsMetadataExample { + + @Incoming("queue") + public void metadata(String body, SqsIncomingMetadata metadata) { + Map attributes = metadata.getMessage().messageAttributes(); + attributes.forEach((k, v) -> System.out.println(k + " -> " + v.stringValue())); + System.out.println("Message body: " + body); + } +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/sqs/inbound/SqsReceiveMessageRequestCustomizerExample.java b/content/docs/smallrye-reactive-messaging/main/_examples/sqs/inbound/SqsReceiveMessageRequestCustomizerExample.java new file mode 100644 index 000000000..1316a6c0a --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/sqs/inbound/SqsReceiveMessageRequestCustomizerExample.java @@ -0,0 +1,17 @@ +package sqs.inbound; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.smallrye.common.annotation.Identifier; +import io.smallrye.reactive.messaging.aws.sqs.SqsReceiveMessageRequestCustomizer; +import software.amazon.awssdk.services.sqs.model.ReceiveMessageRequest; + +@Identifier("my-customizer") // or with channel name @Identifier("data") +@ApplicationScoped +public class SqsReceiveMessageRequestCustomizerExample implements SqsReceiveMessageRequestCustomizer { + @Override + public void customize(ReceiveMessageRequest.Builder builder) { + builder.visibilityTimeout(10) + .messageAttributeNames("my-attribute"); + } +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/sqs/inbound/SqsSdkMessageConsumer.java b/content/docs/smallrye-reactive-messaging/main/_examples/sqs/inbound/SqsSdkMessageConsumer.java new file mode 100644 index 000000000..bdaf6fd90 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/sqs/inbound/SqsSdkMessageConsumer.java @@ -0,0 +1,21 @@ +package sqs.inbound; + +import java.util.Map; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; + +import software.amazon.awssdk.services.sqs.model.Message; +import software.amazon.awssdk.services.sqs.model.MessageAttributeValue; + +@ApplicationScoped +public class SqsSdkMessageConsumer { + + @Incoming("data") + void consume(Message msg) { + System.out.println("Received: " + msg.body()); + Map attributes = msg.messageAttributes(); + // ... + } +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/sqs/inbound/SqsStringConsumer.java b/content/docs/smallrye-reactive-messaging/main/_examples/sqs/inbound/SqsStringConsumer.java new file mode 100644 index 000000000..ffdfcf6d9 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/sqs/inbound/SqsStringConsumer.java @@ -0,0 +1,13 @@ +package sqs.inbound; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; + +@ApplicationScoped +public class SqsStringConsumer { + @Incoming("data") + void consume(String messageBody) { + System.out.println("Received: " + messageBody); + } +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/sqs/json/SqsJsonMapping.java b/content/docs/smallrye-reactive-messaging/main/_examples/sqs/json/SqsJsonMapping.java new file mode 100644 index 000000000..4980edbb8 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/sqs/json/SqsJsonMapping.java @@ -0,0 +1,56 @@ +package sqs.json; + +import java.time.Duration; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +public class SqsJsonMapping { + + // tag::code[] + @ApplicationScoped + public static class Generator { + + @Outgoing("to-rabbitmq") + public Multi prices() { + AtomicInteger count = new AtomicInteger(); + return Multi.createFrom().ticks().every(Duration.ofMillis(1000)) + .map(l -> new Price().setPrice(count.incrementAndGet())) + .onOverflow().drop(); + } + + } + + @ApplicationScoped + public static class Consumer { + + List prices = new CopyOnWriteArrayList<>(); + + @Incoming("from-rabbitmq") + public void consume(Price price) { + prices.add(price); + } + + public List list() { + return prices; + } + } + + public static class Price { + public int price; + + public Price setPrice(int price) { + this.price = price; + return this; + } + } + // end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/sqs/outbound/SqsMessageStringProducer.java b/content/docs/smallrye-reactive-messaging/main/_examples/sqs/outbound/SqsMessageStringProducer.java new file mode 100644 index 000000000..eac978671 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/sqs/outbound/SqsMessageStringProducer.java @@ -0,0 +1,27 @@ +package sqs.outbound; + +import java.time.Duration; +import java.util.UUID; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Metadata; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; +import io.smallrye.reactive.messaging.aws.sqs.SqsOutboundMetadata; + +@ApplicationScoped +public class SqsMessageStringProducer { + + @Outgoing("prices") + public Multi> generate() { + // It emits a UUID every second + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .map(x -> Message.of(UUID.randomUUID().toString(), + Metadata.of(SqsOutboundMetadata.builder() + .groupId("group-1") + .build()))); + } +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/sqs/outbound/SqsOutboundMetadataExample.java b/content/docs/smallrye-reactive-messaging/main/_examples/sqs/outbound/SqsOutboundMetadataExample.java new file mode 100644 index 000000000..e512f4743 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/sqs/outbound/SqsOutboundMetadataExample.java @@ -0,0 +1,27 @@ +package sqs.outbound; + +import java.util.Map; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Metadata; + +import io.smallrye.reactive.messaging.aws.sqs.SqsOutboundMetadata; +import software.amazon.awssdk.services.sqs.model.MessageAttributeValue; + +public class SqsOutboundMetadataExample { + + public Message metadata(Message incoming) { + + // tag::code[] + final SqsOutboundMetadata metadata = SqsOutboundMetadata.builder() + .messageAttributes(Map.of("my-attribute", MessageAttributeValue.builder() + .dataType("String").stringValue("my-value").build())) + .groupId("group-1") + .build(); + + // Add `metadata` to the metadata of the outgoing message. + return Message.of("Hello", Metadata.of(metadata)); + // end::code[] + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/sqs/outbound/SqsStringProducer.java b/content/docs/smallrye-reactive-messaging/main/_examples/sqs/outbound/SqsStringProducer.java new file mode 100644 index 000000000..4a39f5b4e --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/sqs/outbound/SqsStringProducer.java @@ -0,0 +1,22 @@ +package sqs.outbound; + +import java.time.Duration; +import java.util.UUID; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +@ApplicationScoped +public class SqsStringProducer { + + @Outgoing("data") + public Multi generate() { + // It emits a UUID every second + return Multi.createFrom().ticks().every(Duration.ofSeconds(1)) + .map(x -> UUID.randomUUID().toString()); + } + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/testing/MyTest.java b/content/docs/smallrye-reactive-messaging/main/_examples/testing/MyTest.java new file mode 100644 index 000000000..1e1451f67 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/testing/MyTest.java @@ -0,0 +1,52 @@ +package testing; + +import jakarta.enterprise.inject.Any; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import io.smallrye.reactive.messaging.memory.TestIncoming; +import io.smallrye.reactive.messaging.memory.TestOutgoing; +import io.smallrye.reactive.messaging.memory.TestingConnector; + +// @io.quarkus.test.junit.QuarkusTest or the Junit 5 extension that allows injection in tests +public class MyTest { + + // 1. Switch the channels to the test connector: + @BeforeAll + public static void switchMyChannels() { + TestingConnector.switchIncomingChannelsToTesting("prices"); + TestingConnector.switchOutgoingChannelsToTesting("processed-prices"); + } + + // 2. Don't forget to reset the channel after the tests: + @AfterAll + public static void revertMyChannels() { + TestingConnector.clear(); + } + + // 3. Inject the test connector in your test, + // or use the bean manager to retrieve the instance + @Inject + @Any + TestingConnector connector; + + @Test + void test() { + // 4. Retrieves the incoming channel to deliver messages + TestIncoming prices = connector.incoming("prices"); + // 5. Retrieves the outgoing channel to verify what the app sent + TestOutgoing results = connector.outgoing("processed-prices"); + + // 6. Deliver fake messages to the app: + prices.deliver(1); + prices.deliver(2); + prices.deliver(3); + + // 7. Verify what the app sent + Assertions.assertEquals(3, results.sent().size()); + } +} diff --git a/content/docs/smallrye-reactive-messaging/main/_examples/testing/MyTestSetup.java b/content/docs/smallrye-reactive-messaging/main/_examples/testing/MyTestSetup.java new file mode 100644 index 000000000..ae5b9a3f0 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_examples/testing/MyTestSetup.java @@ -0,0 +1,23 @@ +package testing; + +import java.util.HashMap; +import java.util.Map; + +import io.smallrye.reactive.messaging.memory.TestingConnector; + +public class MyTestSetup { + + // tag::code[] + public Map start() { + Map env = new HashMap<>(); + env.putAll(TestingConnector.switchIncomingChannelsToTesting("prices")); + env.putAll(TestingConnector.switchOutgoingChannelsToTesting("my-data-stream")); + return env; + } + + public void stop() { + TestingConnector.clear(); + } + // end::code[] + +} diff --git a/content/docs/smallrye-reactive-messaging/main/_generated/smallrye-amqp-incoming.adoc b/content/docs/smallrye-reactive-messaging/main/_generated/smallrye-amqp-incoming.adoc new file mode 100644 index 000000000..9804e5883 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_generated/smallrye-amqp-incoming.adoc @@ -0,0 +1,37 @@ +[cols="<,<,^,^,<",options="header",] +|=== +|Attribute (_alias_) |Description |Type |Mandatory |Default +|_address_ |The AMQP address. If not set, the channel name is used |string |false | +|_auto-acknowledgement_ |Whether the received AMQP messages must be acknowledged when received |boolean |false |`false` +|_broadcast_ |Whether the received AMQP messages must be dispatched to multiple _subscribers_ |boolean |false |`false` +|_capabilities_ |A comma-separated list of capabilities proposed by the sender or receiver client. |string |false | +|_cbs.audience_ |The audience used to authenticate with the broker using the CBS exchange. If not set, the audience is derived from the address. |string |false |++``++ +|_cbs.enabled_ |Whether the CBS authorization is enabled. If enabled, the connector will use the claims-based security to authenticate with the broker. If disabled, the connector will not use the CBS exchange and will not authenticate with the broker. |boolean |false |`false` +|_cbs.exchange_ |The name of the CBS exchange used to authenticate with the broker. |string |false |`set-token` +|_cbs.scopes_ |The scopes used to authenticate with the broker using the CBS exchange. If not set, the scopes are derived from the address. |string |false |++``++ +|_cbs.token-manager_ |The name of the CbsTokenManager bean used to manage the CBS tokens. |string |false |`default-cbs-token-manager` +|_client-options-name_ _(amqp-client-options-name)_ |The name of the AMQP Client Option bean used to customize the AMQP client configuration |string |false | +|_cloud-events_ |Enables (default) or disables the Cloud Event support. If enabled on an _incoming_ channel, the connector analyzes the incoming records and try to create Cloud Event metadata. If enabled on an _outgoing_, the connector sends the outgoing messages as Cloud Event if the message includes Cloud Event Metadata. |boolean |false |`true` +|_connect-timeout_ _(amqp-connect-timeout)_ |The connection timeout in milliseconds |int |false |`1000` +|_container-id_ |The AMQP container id. Channels with the same container id share the same AMQP connection. |string |false | +|_durable_ |Whether AMQP subscription is durable |boolean |false |`false` +|_failure-strategy_ |Specify the failure strategy to apply when a message produced from an AMQP message is nacked. Accepted values are `fail` (default), `accept`, `release`, `reject`, `modified-failed`, `modified-failed-undeliverable-here` |string |false |`fail` +|_health-enabled_ |Whether health reporting is enabled (default) or disabled |boolean |false |`true` +|_health-timeout_ |The max number of seconds to wait to determine if the connection with the broker is still established for the readiness check. After that threshold, the check is considered as failed. |int |false |`3` +|_host_ _(amqp-host)_ |The broker hostname |string |false |`localhost` +|_initial-credits_ |The number of credits initially requested for incoming messages. This controls how many messages the receiver can buffer. |int |false |`1000` +|_link-name_ |The name of the link. If not set, the channel name is used. |string |false | +|_password_ _(amqp-password)_ |The password used to authenticate to the broker |string |false | +|_port_ _(amqp-port)_ |The broker port |int |false |`5672` +|_qos_ |Sets the local QOS config, values can be ++{++@code null}, ++{++@code AT++_++MOST++_++ONCE} or ++{++@code AT++_++LEAST++_++ONCE}. |string |false | +|_reconnect-attempts_ _(amqp-reconnect-attempts)_ |The number of reconnection attempts |int |false |`100` +|_reconnect-interval_ _(amqp-reconnect-interval)_ |The interval in second between two reconnection attempts |int |false |`10` +|_retry-on-fail-attempts_ |The number of tentative to retry on failure |int |false |`6` +|_retry-on-fail-interval_ |The interval (in seconds) between two sending attempts |int |false |`5` +|_selector_ |Sets a message selector. This attribute is used to define an `apache.org:selector-filter:string` filter on the source terminus, using SQL-based syntax to request the server filters which messages are delivered to the receiver (if supported by the server in question). Precise functionality supported and syntax needed can vary depending on the server. |string |false | +|_sni-server-name_ _(amqp-sni-server-name)_ |If set, explicitly override the hostname to use for the TLS SNI server name |string |false | +|_tracing-enabled_ |Whether tracing is enabled (default) or disabled |boolean |false |`true` +|_use-ssl_ _(amqp-use-ssl)_ |Whether the AMQP connection uses SSL/TLS |boolean |false |`false` +|_username_ _(amqp-username)_ |The username used to authenticate to the broker |string |false | +|_virtual-host_ _(amqp-virtual-host)_ |If set, configure the hostname value used for the connection AMQP Open frame and TLS SNI server name (if TLS is in use) |string |false | +|=== diff --git a/content/docs/smallrye-reactive-messaging/main/_generated/smallrye-amqp-outgoing.adoc b/content/docs/smallrye-reactive-messaging/main/_generated/smallrye-amqp-outgoing.adoc new file mode 100644 index 000000000..e38833d3d --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_generated/smallrye-amqp-outgoing.adoc @@ -0,0 +1,44 @@ +[cols="<,<,^,^,<",options="header",] +|=== +|Attribute (_alias_) |Description |Type |Mandatory |Default +|_address_ |The AMQP address. If not set, the channel name is used |string |false | +|_capabilities_ |A comma-separated list of capabilities proposed by the sender or receiver client. |string |false | +|_cbs.audience_ |The audience used to authenticate with the broker using the CBS exchange. If not set, the audience is derived from the address. |string |false |++``++ +|_cbs.enabled_ |Whether the CBS authorization is enabled. If enabled, the connector will use the claims-based security to authenticate with the broker. If disabled, the connector will not use the CBS exchange and will not authenticate with the broker. |boolean |false |`false` +|_cbs.exchange_ |The name of the CBS exchange used to authenticate with the broker. |string |false |`set-token` +|_cbs.scopes_ |The scopes used to authenticate with the broker using the CBS exchange. If not set, the scopes are derived from the address. |string |false |++``++ +|_cbs.token-manager_ |The name of the CbsTokenManager bean used to manage the CBS tokens. |string |false |`default-cbs-token-manager` +|_client-options-name_ _(amqp-client-options-name)_ |The name of the AMQP Client Option bean used to customize the AMQP client configuration |string |false | +|_cloud-events_ |Enables (default) or disables the Cloud Event support. If enabled on an _incoming_ channel, the connector analyzes the incoming records and try to create Cloud Event metadata. If enabled on an _outgoing_, the connector sends the outgoing messages as Cloud Event if the message includes Cloud Event Metadata. |boolean |false |`true` +|_cloud-events-data-content-type_ _(cloud-events-default-data-content-type)_ |Configure the default `datacontenttype` attribute of the outgoing Cloud Event. Requires `cloud-events` to be set to `true`. This value is used if the message does not configure the `datacontenttype` attribute itself |string |false | +|_cloud-events-data-schema_ _(cloud-events-default-data-schema)_ |Configure the default `dataschema` attribute of the outgoing Cloud Event. Requires `cloud-events` to be set to `true`. This value is used if the message does not configure the `dataschema` attribute itself |string |false | +|_cloud-events-insert-timestamp_ _(cloud-events-default-timestamp)_ |Whether or not the connector should insert automatically the `time` attribute into the outgoing Cloud Event. Requires `cloud-events` to be set to `true`. This value is used if the message does not configure the `time` attribute itself |boolean |false |`true` +|_cloud-events-mode_ |The Cloud Event mode (`structured` or `binary` (default)). Indicates how are written the cloud events in the outgoing record |string |false |`binary` +|_cloud-events-source_ _(cloud-events-default-source)_ |Configure the default `source` attribute of the outgoing Cloud Event. Requires `cloud-events` to be set to `true`. This value is used if the message does not configure the `source` attribute itself |string |false | +|_cloud-events-subject_ _(cloud-events-default-subject)_ |Configure the default `subject` attribute of the outgoing Cloud Event. Requires `cloud-events` to be set to `true`. This value is used if the message does not configure the `subject` attribute itself |string |false | +|_cloud-events-type_ _(cloud-events-default-type)_ |Configure the default `type` attribute of the outgoing Cloud Event. Requires `cloud-events` to be set to `true`. This value is used if the message does not configure the `type` attribute itself |string |false | +|_connect-timeout_ _(amqp-connect-timeout)_ |The connection timeout in milliseconds |int |false |`1000` +|_container-id_ |The AMQP container id. Channels with the same container id share the same AMQP connection. |string |false | +|_credit-retrieval-period_ |The period (in milliseconds) between two attempts to retrieve the credits granted by the broker. This time is used when the sender run out of credits. |int |false |`2000` +|_durable_ |Whether sent AMQP messages are marked durable |boolean |false |`false` +|_health-enabled_ |Whether health reporting is enabled (default) or disabled |boolean |false |`true` +|_health-timeout_ |The max number of seconds to wait to determine if the connection with the broker is still established for the readiness check. After that threshold, the check is considered as failed. |int |false |`3` +|_host_ _(amqp-host)_ |The broker hostname |string |false |`localhost` +|_lazy-client_ |Whether to create the connection and sender at startup or at first send request |boolean |false |`true` +|_link-name_ |The name of the link. If not set, the channel name is used. |string |false | +|_max-inflight-messages_ |The maximum number of messages to be written to the broker concurrently. The number of sent messages waiting to be acknowledged by the broker are limited by this value and credits granted by the broker. The default value `0` means only credits apply. |long |false |`0` +|_merge_ |Whether the connector should allow multiple upstreams |boolean |false |`false` +|_password_ _(amqp-password)_ |The password used to authenticate to the broker |string |false | +|_port_ _(amqp-port)_ |The broker port |int |false |`5672` +|_reconnect-attempts_ _(amqp-reconnect-attempts)_ |The number of reconnection attempts |int |false |`100` +|_reconnect-interval_ _(amqp-reconnect-interval)_ |The interval in second between two reconnection attempts |int |false |`10` +|_retry-on-fail-attempts_ |The number of tentative to retry on failure |int |false |`6` +|_retry-on-fail-interval_ |The interval (in seconds) between two sending attempts |int |false |`5` +|_sni-server-name_ _(amqp-sni-server-name)_ |If set, explicitly override the hostname to use for the TLS SNI server name |string |false | +|_tracing-enabled_ |Whether tracing is enabled (default) or disabled |boolean |false |`true` +|_ttl_ |The time-to-live of the send AMQP messages. 0 to disable the TTL |long |false |`0` +|_use-anonymous-sender_ |Whether or not the connector should use an anonymous sender. Default value is `true` if the broker supports it, `false` otherwise. If not supported, it is not possible to dynamically change the destination address. |boolean |false | +|_use-ssl_ _(amqp-use-ssl)_ |Whether the AMQP connection uses SSL/TLS |boolean |false |`false` +|_username_ _(amqp-username)_ |The username used to authenticate to the broker |string |false | +|_virtual-host_ _(amqp-virtual-host)_ |If set, configure the hostname value used for the connection AMQP Open frame and TLS SNI server name (if TLS is in use) |string |false | +|=== diff --git a/content/docs/smallrye-reactive-messaging/main/_generated/smallrye-camel-incoming.adoc b/content/docs/smallrye-reactive-messaging/main/_generated/smallrye-camel-incoming.adoc new file mode 100644 index 000000000..e14c417cf --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_generated/smallrye-camel-incoming.adoc @@ -0,0 +1,6 @@ +[cols="<,<,^,^,<",options="header",] +|=== +|Attribute (_alias_) |Description |Type |Mandatory |Default +|_endpoint-uri_ |The URI of the Camel endpoint (read from or written to) |string |true | +|_failure-strategy_ |Specify the failure strategy to apply when a message produced from a Camel exchange is nacked. Values can be `fail` (default) or `ignore` |string |false |`fail` +|=== diff --git a/content/docs/smallrye-reactive-messaging/main/_generated/smallrye-camel-outgoing.adoc b/content/docs/smallrye-reactive-messaging/main/_generated/smallrye-camel-outgoing.adoc new file mode 100644 index 000000000..ae1860580 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_generated/smallrye-camel-outgoing.adoc @@ -0,0 +1,6 @@ +[cols="<,<,^,^,<",options="header",] +|=== +|Attribute (_alias_) |Description |Type |Mandatory |Default +|_endpoint-uri_ |The URI of the Camel endpoint (read from or written to) |string |true | +|_merge_ |Whether the connector should allow multiple upstreams |boolean |false |`false` +|=== diff --git a/content/docs/smallrye-reactive-messaging/main/_generated/smallrye-jms-incoming.adoc b/content/docs/smallrye-reactive-messaging/main/_generated/smallrye-jms-incoming.adoc new file mode 100644 index 000000000..d93940a5d --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_generated/smallrye-jms-incoming.adoc @@ -0,0 +1,25 @@ +[cols="<,<,^,^,<",options="header",] +|=== +|Attribute (_alias_) |Description |Type |Mandatory |Default +|_broadcast_ |Whether or not the JMS message should be dispatched to multiple consumers |boolean |false |`false` +|_client-id_ |The client id |String |false | +|_connection-factory-name_ |The name of the JMS connection factory (`jakarta.jms.ConnectionFactory`) to be used. If not set, it uses any exposed JMS connection factory |String |false | +|_dead-letter-queue.destination_ |When the `failure-strategy` is set to `dead-letter-queue` indicates on which queue the message is sent. Defaults is `dead-letter-topic-$channel` |string |false | +|_dead-letter-queue.producer-client-id_ |When the `failure-strategy` is set to `dead-letter-queue` indicates what client id the generated producer should use. Defaults is `jms-dead-letter-topic-producer-$client-id` |string |false | +|_destination_ |The name of the JMS destination. If not set the name of the channel is used |String |false | +|_destination-type_ |The type of destination. It can be either `queue` or `topic` |string |false |`queue` +|_durable_ |Set to `true` to use a durable subscription |boolean |false |`false` +|_failure-strategy_ |Specify the failure strategy to apply when a message produced from a record is acknowledged negatively (nack). Values can be `fail` (default), `ignore`, or `dead-letter-queue` |string |false |`fail` +|_no-local_ |Enable or disable local delivery |boolean |false |`false` +|_password_ |The password to connect to to the JMS server |String |false | +|_retry_ |Whether to retry on terminal stream errors. |boolean |false |`true` +|_retry.initial-delay_ |The initial delay for the retry. |string |false |`PT1S` +|_retry.jitter_ |How much the delay jitters as a multiplier between 0 and 1. The formula is current delay ++*++ jitter. For example, with a current delay of 2H, a jitter of 0.5 will result in an actual delay somewhere between 1H and 3H. |double |false |`0.5` +|_retry.max-delay_ |The maximum delay |string |false |`PT10S` +|_retry.max-retries_ |Maximum number of retries for terminal stream errors. |int |false |`3` +|_reuse-jms-context_ |Whether to reuse JMS contexts by creating child contexts from a parent context. When enabled, child contexts will be created using JMSContext.createContext() instead of ConnectionFactory.createContext() |boolean |false |`false` +|_selector_ |The JMS selector |String |false | +|_session-mode_ |The session mode. Accepted values are AUTO++_++ACKNOWLEDGE, SESSION++_++TRANSACTED, CLIENT++_++ACKNOWLEDGE, DUPS++_++OK++_++ACKNOWLEDGE |String |false |`AUTO++_++ACKNOWLEDGE` +|_tracing-enabled_ |Whether tracing is enabled (default) or disabled |boolean |false |`true` +|_username_ |The username to connect to to the JMS server |String |false | +|=== diff --git a/content/docs/smallrye-reactive-messaging/main/_generated/smallrye-jms-outgoing.adoc b/content/docs/smallrye-reactive-messaging/main/_generated/smallrye-jms-outgoing.adoc new file mode 100644 index 000000000..335b694e3 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_generated/smallrye-jms-outgoing.adoc @@ -0,0 +1,28 @@ +[cols="<,<,^,^,<",options="header",] +|=== +|Attribute (_alias_) |Description |Type |Mandatory |Default +|_client-id_ |The client id |String |false | +|_connection-factory-name_ |The name of the JMS connection factory (`jakarta.jms.ConnectionFactory`) to be used. If not set, it uses any exposed JMS connection factory |String |false | +|_correlation-id_ |The JMS Message correlation id |string |false | +|_delivery-delay_ |The delivery delay |long |false | +|_delivery-mode_ |The delivery mode. Either `persistent` or `non++_++persistent` |string |false | +|_destination_ |The name of the JMS destination. If not set the name of the channel is used |String |false | +|_destination-type_ |The type of destination. It can be either `queue` or `topic` |string |false |`queue` +|_disable-message-id_ |Omit the message id in the outbound JMS message |boolean |false | +|_disable-message-timestamp_ |Omit the message timestamp in the outbound JMS message |boolean |false | +|_merge_ |Whether the connector should allow multiple upstreams |boolean |false |`false` +|_password_ |The password to connect to to the JMS server |String |false | +|_priority_ |The JMS Message priority |int |false | +|_reply-to_ |The reply to destination if any |string |false | +|_reply-to-destination-type_ |The type of destination for the response. It can be either `queue` or `topic` |string |false |`queue` +|_retry_ |Whether to retry on terminal stream errors. |boolean |false |`true` +|_retry.initial-delay_ |The initial delay for the retry. |string |false |`PT1S` +|_retry.jitter_ |How much the delay jitters as a multiplier between 0 and 1. The formula is current delay ++*++ jitter. For example, with a current delay of 2H, a jitter of 0.5 will result in an actual delay somewhere between 1H and 3H. |double |false |`0.5` +|_retry.max-delay_ |The maximum delay |string |false |`PT10S` +|_retry.max-retries_ |Maximum number of retries for terminal stream errors. |int |false |`3` +|_reuse-jms-context_ |Whether to reuse JMS contexts by creating child contexts from a parent context. When enabled, child contexts will be created using JMSContext.createContext() instead of ConnectionFactory.createContext() |boolean |false |`false` +|_session-mode_ |The session mode. Accepted values are AUTO++_++ACKNOWLEDGE, SESSION++_++TRANSACTED, CLIENT++_++ACKNOWLEDGE, DUPS++_++OK++_++ACKNOWLEDGE |String |false |`AUTO++_++ACKNOWLEDGE` +|_tracing-enabled_ |Whether tracing is enabled (default) or disabled |boolean |false |`true` +|_ttl_ |The JMS Message time-to-live |long |false | +|_username_ |The username to connect to to the JMS server |String |false | +|=== diff --git a/content/docs/smallrye-reactive-messaging/main/_generated/smallrye-kafka-incoming.adoc b/content/docs/smallrye-reactive-messaging/main/_generated/smallrye-kafka-incoming.adoc new file mode 100644 index 000000000..54dfa4117 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_generated/smallrye-kafka-incoming.adoc @@ -0,0 +1,65 @@ +[cols="<,<,^,^,<",options="header",] +|=== +|Attribute (_alias_) |Description |Type |Mandatory |Default +|_assign-seek_ |Assign partitions and optionally seek to offsets, instead of subscribing to topics. A comma-separating list of triplets in form of ++`++: | |:`to assign statically to the consumer and seek to the given offsets. Offset`0`seeks to beginning and offset`-1++`++ seeks to the end of the topic-partition. If the topic is omitted the configured topic will be used. If the offset is omitted partitions are assigned to the consumer but won't be seeked to offset. |string +|_auto.offset.reset_ |What to do when there is no initial offset in Kafka.Accepted values are earliest, latest and none |string |false |`latest` +|_batch_ |Whether the Kafka records are consumed in batch. The channel injection point must consume a compatible type, such as `List++<++Payload++>++` or `KafkaRecordBatch++<++Payload++>++`. |boolean |false |`false` +|_bootstrap.servers_ _(kafka.bootstrap.servers)_ |A comma-separated list of host:port to use for establishing the initial connection to the Kafka cluster. |string |false |`localhost:9092` +|_broadcast_ |Whether the Kafka records should be dispatched to multiple consumer |boolean |false |`false` +|_checkpoint.state-store_ |While using the `checkpoint` commit-strategy, the name set in `@Identifier` of a bean that implements `io.smallrye.reactive.messaging.kafka.StateStore.Factory` to specify the state store implementation. |string |false | +|_checkpoint.state-type_ |While using the `checkpoint` commit-strategy, the fully qualified type name of the state object to persist in the state store. When provided, it can be used by the state store implementation to help persisting the processing state object. |string |false | +|_checkpoint.unsynced-state-max-age.ms_ |While using the `checkpoint` commit-strategy, specify the max age in milliseconds that the processing state must be persisted before the connector is marked as unhealthy. Setting this attribute to 0 disables this monitoring. |int |false |`10000` +|_client-id-prefix_ |Prefix for Kafka client `client.id` attribute. If defined configured or generated `client.id` will be prefixed with the given value. |string |false | +|_cloud-events_ |Enables (default) or disables the Cloud Event support. If enabled on an _incoming_ channel, the connector analyzes the incoming records and try to create Cloud Event metadata. If enabled on an _outgoing_, the connector sends the outgoing messages as Cloud Event if the message includes Cloud Event Metadata. |boolean |false |`true` +|_commit-strategy_ |Specify the commit strategy to apply when a message produced from a record is acknowledged. Values can be `latest`, `ignore` or `throttled`. If `enable.auto.commit` is true then the default is `ignore` otherwise it is `throttled` |string |false | +|_consumer-rebalance-listener.name_ |The name set in `@Identifier` of a bean that implements `io.smallrye.reactive.messaging.kafka.KafkaConsumerRebalanceListener`. If set, this rebalance listener is applied to the consumer. |string |false | +|_dead-letter-queue.key.serializer_ |When the `failure-strategy` is set to `dead-letter-queue` indicates the key serializer to use. If not set the serializer associated to the key deserializer is used |string |false | +|_dead-letter-queue.producer-client-id_ |When the `failure-strategy` is set to `dead-letter-queue` indicates what client id the generated producer should use. Defaults is `kafka-dead-letter-topic-producer-$client-id` |string |false | +|_dead-letter-queue.topic_ |When the `failure-strategy` is set to `dead-letter-queue` indicates on which topic the record is sent. Defaults is `dead-letter-topic-$channel` |string |false | +|_dead-letter-queue.value.serializer_ |When the `failure-strategy` is set to `dead-letter-queue` indicates the value serializer to use. If not set the serializer associated to the value deserializer is used |string |false | +|_delayed-retry-topic.max-retries_ |When the `failure-strategy` is set to `delayed-retry-topic` indicates the maximum number of retries. If higher than the number of delayed retry topics, last topic is used. |int |false | +|_delayed-retry-topic.timeout_ |When the `failure-strategy` is set to `delayed-retry-topic` indicates the global timeout per record. |int |false |`120000` +|_delayed-retry-topic.topics_ |When the `failure-strategy` is set to `delayed-retry-topic` indicates topics to use. If not set the source channel name is used, with 10, 20 and 50 seconds delayed topics. |string |false | +|_enable.auto.commit_ |If enabled, consumer's offset will be periodically committed in the background by the underlying Kafka client, ignoring the actual processing outcome of the records. It is recommended to NOT enable this setting and let Reactive Messaging handles the commit. |boolean |false |`false` +|_fail-on-deserialization-failure_ |When no deserialization failure handler is set and a deserialization failure happens, report the failure and mark the application as unhealthy. If set to `false` and a deserialization failure happens, a `null` value is forwarded. |boolean |false |`true` +|_failure-strategy_ |Specify the failure strategy to apply when a message produced from a record is acknowledged negatively (nack). Values can be `fail` (default), `ignore`, or `dead-letter-queue` |string |false |`fail` +|_fetch.min.bytes_ |The minimum amount of data the server should return for a fetch request. The default setting of 1 byte means that fetch requests are answered as soon as a single byte of data is available or the fetch request times out waiting for data to arrive. |int |false |`1` +|_graceful-shutdown_ |Whether or not a graceful shutdown should be attempted when the application terminates. |boolean |false |`true` +|_group.id_ |A unique string that identifies the consumer group the application belongs to. If not set, a unique, generated id is used |string |false | +|_health-enabled_ |Whether health reporting is enabled (default) or disabled |boolean |false |`true` +|_health-readiness-enabled_ |Whether readiness health reporting is enabled (default) or disabled |boolean |false |`true` +|_health-readiness-timeout_ |_deprecated_ - During the readiness health check, the connector connects to the broker and retrieves the list of topics. This attribute specifies the maximum duration (in ms) for the retrieval. If exceeded, the channel is considered not-ready. Deprecated: Use 'health-topic-verification-timeout' instead. |long |false | +|_health-readiness-topic-verification_ |_deprecated_ - Whether the readiness check should verify that topics exist on the broker. Default to false. Enabling it requires an admin connection. Deprecated: Use 'health-topic-verification-enabled' instead. |boolean |false | +|_health-topic-verification-enabled_ |Whether the startup and readiness check should verify that topics exist on the broker. Default to false. Enabling it requires an admin client connection. |boolean |false |`false` +|_health-topic-verification-readiness-disabled_ |Whether the topic verification is disabled for the readiness health check. |boolean |false |`false` +|_health-topic-verification-startup-disabled_ |Whether the topic verification is disabled for the startup health check. |boolean |false |`false` +|_health-topic-verification-timeout_ |During the startup and readiness health check, the connector connects to the broker and retrieves the list of topics. This attribute specifies the maximum duration (in ms) for the retrieval. If exceeded, the channel is considered not-ready. |long |false |`2000` +|_kafka-configuration_ |Identifier of a CDI bean that provides the default Kafka consumer/producer configuration for this channel. The channel configuration can still override any attribute. The bean must have a type of Map++<++String, Object++>++ and must use the @io.smallrye.common.annotation.Identifier qualifier to set the identifier. |string |false | +|_key-deserialization-failure-handler_ |The name set in `@Identifier` of a bean that implements `io.smallrye.reactive.messaging.kafka.DeserializationFailureHandler`. If set, deserialization failure happening when deserializing keys are delegated to this handler which may retry or provide a fallback value. |string |false | +|_key.deserializer_ |The deserializer classname used to deserialize the record's key |string |false |`org.apache.kafka.common.serialization.StringDeserializer` +|_lazy-client_ |Whether Kafka client is created lazily or eagerly. |boolean |false |`false` +|_max-queue-size-factor_ |Multiplier factor to determine maximum number of records queued for processing, using `max.poll.records` ++*++ `max-queue-size-factor`. Defaults to 2. In `batch` mode `max.poll.records` is considered `1`. |int |false |`2` +|_ordered_ |Configures ordering guarantees for concurrent processing. Possible values: `key` for per-key sequential processing, or `partition` for per-partition sequential processing. Requires `@Blocking(ordered = false)` for concurrency. Works with any commit strategy. |string |false | +|_ordered.max-concurrency_ |Maximum number of groups (keys or partitions) that can be processed concurrently. Defaults to `max.poll.records`. |int |false | +|_partitions_ |_deprecated_ - The number of partitions to be consumed concurrently. The connector creates the specified amount of Kafka consumers. It should match the number of partition of the targeted topic. Deprecated: Use `concurrency` channel attribute instead. |int |false |`1` +|_pattern_ |Indicate that the `topic` property is a regular expression. Must be used with the `topic` property. Cannot be used with the `topics` property |boolean |false |`false` +|_pause-if-no-requests_ |Whether the polling must be paused when the application does not request items and resume when it does. This allows implementing back-pressure based on the application capacity. Note that polling is not stopped, but will not retrieve any records when paused. |boolean |false |`true` +|_poll-timeout_ |The polling timeout in milliseconds. When polling records, the poll will wait at most that duration before returning records. Default is 1000ms |int |false |`1000` +|_requests_ |_deprecated_ - When `partitions` is greater than 1, this attribute allows configuring how many records are requested by each consumers every time. Deprecated: Use `concurrency` channel attribute instead. |int |false |`128` +|_retry_ |Whether or not the connection to the broker is re-attempted in case of failure |boolean |false |`true` +|_retry-attempts_ |The maximum number of reconnection before failing. -1 means infinite retry |int |false |`-1` +|_retry-max-wait_ |The max delay (in seconds) between 2 reconnects |int |false |`30` +|_share-group_ |_deprecated_ - Deprecated, use 'share-group.enabled' instead |boolean |false |`false` +|_share-group.enabled_ |Whether to use Kafka Share Groups for consumption. When enabled, the consumer will use a ShareConsumer which provides cooperative record processing across multiple consumers without explicit partition assignment. |boolean |false |`false` +|_share-group.failure-acknowledgement-type_ |Default acknowledgement type to apply to the record, when the message is nacked. |string |false |`release` +|_share-group.failure-deserialization-acknowledgement-type_ |Default acknowledgement type to apply to the record, when the message is nacked because of failure on deserialization. |string |false |`reject` +|_share-group.unprocessed-record-max-age.ms_ |While using share groups, specify the max age in milliseconds that an unprocessed record can be before the connector reports a failure. Setting this attribute to 0 disables this monitoring. |int |false |`60000` +|_throttled.ordered_ |_deprecated_ - While using the `throttled` commit-strategy, configures ordering guarantees for concurrent processing. Possible values: `key` for per-key sequential processing within partitions, or `partition` for per-partition sequential processing. Requires `@Blocking(ordered = false)` for concurrency. Deprecated: Use `ordered` instead. |string |false | +|_throttled.ordered.max-concurrency_ |_deprecated_ - While using the `throttled` commit-strategy with ordered processing, specifies the maximum number of groups (keys or partitions) that can be processed concurrently. This should typically match your worker pool's `max-concurrency` setting. Defaults to `max.poll.records`. Deprecated: Use `ordered.max-concurrency` instead. |int |false | +|_throttled.unprocessed-record-max-age.ms_ |While using the `throttled` commit-strategy, specify the max age in milliseconds that an unprocessed message can be before the connector is marked as unhealthy. Setting this attribute to 0 disables this monitoring. |int |false |`60000` +|_topic_ |The consumed / populated Kafka topic. If neither this property nor the `topics` properties are set, the channel name is used |string |false | +|_topics_ |A comma-separating list of topics to be consumed. Cannot be used with the `topic` or `pattern` properties |string |false | +|_tracing-enabled_ |Whether tracing is enabled (default) or disabled |boolean |false |`true` +|_value-deserialization-failure-handler_ |The name set in `@Identifier` of a bean that implements `io.smallrye.reactive.messaging.kafka.DeserializationFailureHandler`. If set, deserialization failure happening when deserializing values are delegated to this handler which may retry or provide a fallback value. |string |false | +|_value.deserializer_ |The deserializer classname used to deserialize the record's value |string |true | +|=== diff --git a/content/docs/smallrye-reactive-messaging/main/_generated/smallrye-kafka-outgoing.adoc b/content/docs/smallrye-reactive-messaging/main/_generated/smallrye-kafka-outgoing.adoc new file mode 100644 index 000000000..85f3102a9 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_generated/smallrye-kafka-outgoing.adoc @@ -0,0 +1,46 @@ +[cols="<,<,^,^,<",options="header",] +|=== +|Attribute (_alias_) |Description |Type |Mandatory |Default +|_acks_ |The number of acknowledgments the producer requires the leader to have received before considering a request complete. This controls the durability of records that are sent. Accepted values are: 0, 1, all |string |false |`1` +|_bootstrap.servers_ _(kafka.bootstrap.servers)_ |A comma-separated list of host:port to use for establishing the initial connection to the Kafka cluster. |string |false |`localhost:9092` +|_buffer.memory_ |The total bytes of memory the producer can use to buffer records waiting to be sent to the server. |long |false |`33554432` +|_client-id-prefix_ |Prefix for Kafka client `client.id` attribute. If defined configured or generated `client.id` will be prefixed with the given value. |string |false | +|_close-timeout_ |The amount of milliseconds waiting for a graceful shutdown of the Kafka producer |int |false |`10000` +|_cloud-events_ |Enables (default) or disables the Cloud Event support. If enabled on an _incoming_ channel, the connector analyzes the incoming records and try to create Cloud Event metadata. If enabled on an _outgoing_, the connector sends the outgoing messages as Cloud Event if the message includes Cloud Event Metadata. |boolean |false |`true` +|_cloud-events-data-content-type_ _(cloud-events-default-data-content-type)_ |Configure the default `datacontenttype` attribute of the outgoing Cloud Event. Requires `cloud-events` to be set to `true`. This value is used if the message does not configure the `datacontenttype` attribute itself |string |false | +|_cloud-events-data-schema_ _(cloud-events-default-data-schema)_ |Configure the default `dataschema` attribute of the outgoing Cloud Event. Requires `cloud-events` to be set to `true`. This value is used if the message does not configure the `dataschema` attribute itself |string |false | +|_cloud-events-insert-timestamp_ _(cloud-events-default-timestamp)_ |Whether or not the connector should insert automatically the `time` attribute into the outgoing Cloud Event. Requires `cloud-events` to be set to `true`. This value is used if the message does not configure the `time` attribute itself |boolean |false |`true` +|_cloud-events-mode_ |The Cloud Event mode (`structured` or `binary` (default)). Indicates how are written the cloud events in the outgoing record |string |false |`binary` +|_cloud-events-source_ _(cloud-events-default-source)_ |Configure the default `source` attribute of the outgoing Cloud Event. Requires `cloud-events` to be set to `true`. This value is used if the message does not configure the `source` attribute itself |string |false | +|_cloud-events-subject_ _(cloud-events-default-subject)_ |Configure the default `subject` attribute of the outgoing Cloud Event. Requires `cloud-events` to be set to `true`. This value is used if the message does not configure the `subject` attribute itself |string |false | +|_cloud-events-type_ _(cloud-events-default-type)_ |Configure the default `type` attribute of the outgoing Cloud Event. Requires `cloud-events` to be set to `true`. This value is used if the message does not configure the `type` attribute itself |string |false | +|_health-enabled_ |Whether health reporting is enabled (default) or disabled |boolean |false |`true` +|_health-readiness-enabled_ |Whether readiness health reporting is enabled (default) or disabled |boolean |false |`true` +|_health-readiness-timeout_ |_deprecated_ - During the readiness health check, the connector connects to the broker and retrieves the list of topics. This attribute specifies the maximum duration (in ms) for the retrieval. If exceeded, the channel is considered not-ready. Deprecated: Use 'health-topic-verification-timeout' instead. |long |false | +|_health-readiness-topic-verification_ |_deprecated_ - Whether the readiness check should verify that topics exist on the broker. Default to false. Enabling it requires an admin connection. Deprecated: Use 'health-topic-verification-enabled' instead. |boolean |false | +|_health-topic-verification-enabled_ |Whether the startup and readiness check should verify that topics exist on the broker. Default to false. Enabling it requires an admin client connection. |boolean |false |`false` +|_health-topic-verification-readiness-disabled_ |Whether the topic verification is disabled for the readiness health check. |boolean |false |`false` +|_health-topic-verification-startup-disabled_ |Whether the topic verification is disabled for the startup health check. |boolean |false |`false` +|_health-topic-verification-timeout_ |During the startup and readiness health check, the connector connects to the broker and retrieves the list of topics. This attribute specifies the maximum duration (in ms) for the retrieval. If exceeded, the channel is considered not-ready. |long |false |`2000` +|_interceptor-bean_ |The name set in `@Identifier` of a bean that implements `org.apache.kafka.clients.producer.ProducerInterceptor`. If set, the identified bean will be used as the producer interceptor. |string |false | +|_kafka-configuration_ |Identifier of a CDI bean that provides the default Kafka consumer/producer configuration for this channel. The channel configuration can still override any attribute. The bean must have a type of Map++<++String, Object++>++ and must use the @io.smallrye.common.annotation.Identifier qualifier to set the identifier. |string |false | +|_key_ |A key to used when writing the record |string |false | +|_key-serialization-failure-handler_ |The name set in `@Identifier` of a bean that implements `io.smallrye.reactive.messaging.kafka.SerializationFailureHandler`. If set, serialization failure happening when serializing keys are delegated to this handler which may provide a fallback value. |string |false | +|_key.serializer_ |The serializer classname used to serialize the record's key |string |false |`org.apache.kafka.common.serialization.StringSerializer` +|_lazy-client_ |Whether Kafka client is created lazily or eagerly. |boolean |false |`false` +|_max-inflight-messages_ |The maximum number of messages to be written to Kafka concurrently. It limits the number of messages waiting to be written and acknowledged by the broker. You can set this attribute to `0` remove the limit |long |false |`1024` +|_merge_ |Whether the connector should allow multiple upstreams |boolean |false |`false` +|_partition_ |The target partition id. -1 to let the client determine the partition |int |false |`-1` +|_pooled-producer_ |_deprecated_ - Deprecated, use 'pooled-producer.enabled' instead |boolean |false |`false` +|_pooled-producer.enabled_ |Whether to use a pool of Kafka producers for concurrent transactions. Each transaction scope acquires a producer from the pool, enabling concurrent exactly-once processing. Requires transactional.id to be set. |boolean |false |`false` +|_pooled-producer.initial-pool-size_ |Number of Kafka producers to pre-create in the pool at startup. Respects the lazy-client setting. Defaults to 0 (lazy creation). |int |false |`0` +|_pooled-producer.max-pool-size_ |Maximum number of Kafka producers in the pool. When all producers are in use and the pool is exhausted, an exception is thrown. Defaults to 10. |int |false |`10` +|_propagate-headers_ |A comma-separating list of incoming record headers to be propagated to the outgoing record |string |false |++``++ +|_propagate-record-key_ |Propagate incoming record key to the outgoing record |boolean |false |`false` +|_retries_ |If set to a positive number, the connector will try to resend any record that was not delivered successfully (with a potentially transient error) until the number of retries is reached. If set to 0, retries are disabled. If not set, the connector tries to resend any record that failed to be delivered (because of a potentially transient error) during an amount of time configured by `delivery.timeout.ms`. |long |false |`2147483647` +|_topic_ |The consumed / populated Kafka topic. If neither this property nor the `topics` properties are set, the channel name is used |string |false | +|_tracing-enabled_ |Whether tracing is enabled (default) or disabled |boolean |false |`true` +|_value-serialization-failure-handler_ |The name set in `@Identifier` of a bean that implements `io.smallrye.reactive.messaging.kafka.SerializationFailureHandler`. If set, serialization failure happening when serializing values are delegated to this handler which may provide a fallback value. |string |false | +|_value.serializer_ |The serializer classname used to serialize the payload |string |true | +|_waitForWriteCompletion_ |Whether the client waits for Kafka to acknowledge the written record before acknowledging the message |boolean |false |`true` +|=== diff --git a/content/docs/smallrye-reactive-messaging/main/_generated/smallrye-mqtt-incoming.adoc b/content/docs/smallrye-reactive-messaging/main/_generated/smallrye-mqtt-incoming.adoc new file mode 100644 index 000000000..69d568103 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_generated/smallrye-mqtt-incoming.adoc @@ -0,0 +1,45 @@ +[cols="<,<,^,^,<",options="header",] +|=== +|Attribute (_alias_) |Description |Type |Mandatory |Default +|_authentication-method_ |MQTT 5.0: the authentication method for enhanced authentication |string |false | +|_auto-clean-session_ |Set to start with a clean session (`true` by default) |boolean |false |`true` +|_auto-generated-client-id_ |Set if the MQTT client must generate clientId automatically |boolean |false |`true` +|_auto-keep-alive_ |Set if the MQTT client must handle `PINGREQ` automatically |boolean |false |`true` +|_broadcast_ |Whether or not the messages should be dispatched to multiple consumers |boolean |false |`false` +|_buffer-size_ |The size buffer of incoming messages waiting to be processed |int |false |`128` +|_client-id_ |Set the client identifier |string |false | +|_client-options-name_ _(mqtt-client-options-name)_ |The name of the MQTT Client Option bean (`io.smallrye.reactive.messaging.mqtt.session.MqttClientSessionOptions`) used to customize the MQTT client configuration |string |false | +|_connect-timeout-seconds_ |Set the connect timeout (in seconds) |int |false |`60` +|_failure-strategy_ |Specify the failure strategy to apply when a message produced from a MQTT message is nacked. Values can be `fail` (default), or `ignore` |string |false |`fail` +|_health-enabled_ |Whether health reporting is enabled (default) or disabled |boolean |false |`true` +|_host_ |Set the MQTT server host name/IP |string |true | +|_keep-alive-seconds_ |Set the keep alive timeout in seconds |int |false |`30` +|_max-inflight-queue_ |Set max count of unacknowledged messages |int |false |`10` +|_max-message-size_ |Set max MQTT message size in bytes |int |false |`8092` +|_mqtt-version_ |Set the MQTT protocol version (4 for 3.1.1 or 5 for 5.0) |int |false |`4` +|_no-local_ |MQTT 5.0: if true, the server will not forward messages published by this client back to this client |boolean |false |`false` +|_password_ |Set the password to connect to the server |string |false | +|_port_ |Set the MQTT server port. Default to 8883 if ssl is enabled, or 1883 without ssl |int |false | +|_qos_ |Set the QoS level when subscribing to the topic or when sending a message (0, 1, or 2) |int |false |`0` +|_reconnect-interval-seconds_ |Set the reconnect interval in seconds |int |false |`1` +|_retain-as-published_ |MQTT 5.0: if true, the server keeps the retain flag as set by the publishing client |boolean |false |`false` +|_retain-handling_ |MQTT 5.0: retain handling option (0=send on subscribe, 1=send if new subscription, 2=do not send) |int |false |`0` +|_server-name_ |Set the SNI server name |string |false | +|_session-expiry-interval_ |Set the session expiry interval in seconds (MQTT 5.0 only) |int |false | +|_ssl_ |Set whether SSL/TLS is enabled |boolean |false |`false` +|_ssl.hostname-verification-algorithm_ |Set the hostname verifier algorithm for the TLS connection.Accepted values are `HTTPS`, `LDAPS`, and `NONE` (defaults). `NONE` disables the hostname verification. |string |false |`NONE` +|_ssl.keystore.location_ |Set the keystore location. In case of `pem` type this is the server ca cert path |string |false | +|_ssl.keystore.password_ |Set the keystore password. In case of `pem` type this is the key path |string |false | +|_ssl.keystore.type_ |Set the keystore type ++[++`pkcs12`, `jks`, `pem`++]++ |string |false |`pkcs12` +|_ssl.truststore.location_ |Set the truststore location. In case of `pem` type this is the client cert path |string |false | +|_ssl.truststore.password_ |Set the truststore password. In case of `pem` type this is not necessary |string |false | +|_ssl.truststore.type_ |Set the truststore type ++[++`pkcs12`, `jks`, `pem`++]++ |string |false |`pkcs12` +|_subscription-identifier_ |MQTT 5.0: subscription identifier to include in the SUBSCRIBE packet |int |false | +|_topic_ |Set the MQTT topic. If not set, the channel name is used |string |false | +|_trust-all_ |Set whether all server certificates should be trusted |boolean |false |`false` +|_unsubscribe-on-disconnection_ |This flag restore the old behavior to unsubscribe from the broken on disconnection |boolean |false |`false` +|_username_ |Set the username to connect to the server |string |false | +|_will-flag_ |Set if will information are provided on connection |boolean |false |`false` +|_will-qos_ |Set the QoS level for the will message |int |false |`0` +|_will-retain_ |Set if the will message must be retained |boolean |false |`false` +|=== diff --git a/content/docs/smallrye-reactive-messaging/main/_generated/smallrye-mqtt-outgoing.adoc b/content/docs/smallrye-reactive-messaging/main/_generated/smallrye-mqtt-outgoing.adoc new file mode 100644 index 000000000..7f079e6ca --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_generated/smallrye-mqtt-outgoing.adoc @@ -0,0 +1,40 @@ +[cols="<,<,^,^,<",options="header",] +|=== +|Attribute (_alias_) |Description |Type |Mandatory |Default +|_authentication-method_ |MQTT 5.0: the authentication method for enhanced authentication |string |false | +|_auto-clean-session_ |Set to start with a clean session (`true` by default) |boolean |false |`true` +|_auto-generated-client-id_ |Set if the MQTT client must generate clientId automatically |boolean |false |`true` +|_auto-keep-alive_ |Set if the MQTT client must handle `PINGREQ` automatically |boolean |false |`true` +|_client-id_ |Set the client identifier |string |false | +|_client-options-name_ _(mqtt-client-options-name)_ |The name of the MQTT Client Option bean (`io.smallrye.reactive.messaging.mqtt.session.MqttClientSessionOptions`) used to customize the MQTT client configuration |string |false | +|_connect-timeout-seconds_ |Set the connect timeout (in seconds) |int |false |`60` +|_health-enabled_ |Whether health reporting is enabled (default) or disabled |boolean |false |`true` +|_host_ |Set the MQTT server host name/IP |string |true | +|_keep-alive-seconds_ |Set the keep alive timeout in seconds |int |false |`30` +|_max-inflight-queue_ |Set max count of unacknowledged messages |int |false |`10` +|_max-message-size_ |Set max MQTT message size in bytes |int |false |`8092` +|_merge_ |Whether the connector should allow multiple upstreams |boolean |false |`false` +|_mqtt-version_ |Set the MQTT protocol version (4 for 3.1.1 or 5 for 5.0) |int |false |`4` +|_password_ |Set the password to connect to the server |string |false | +|_port_ |Set the MQTT server port. Default to 8883 if ssl is enabled, or 1883 without ssl |int |false | +|_qos_ |Set the QoS level when subscribing to the topic or when sending a message (0, 1, or 2) |int |false |`0` +|_reconnect-interval-seconds_ |Set the reconnect interval in seconds |int |false |`1` +|_retain_ |Whether the published message should be retained |boolean |false |`false` +|_server-name_ |Set the SNI server name |string |false | +|_session-expiry-interval_ |Set the session expiry interval in seconds (MQTT 5.0 only) |int |false | +|_ssl_ |Set whether SSL/TLS is enabled |boolean |false |`false` +|_ssl.hostname-verification-algorithm_ |Set the hostname verifier algorithm for the TLS connection.Accepted values are `HTTPS`, `LDAPS`, and `NONE` (defaults). `NONE` disables the hostname verification. |string |false |`NONE` +|_ssl.keystore.location_ |Set the keystore location. In case of `pem` type this is the server ca cert path |string |false | +|_ssl.keystore.password_ |Set the keystore password. In case of `pem` type this is the key path |string |false | +|_ssl.keystore.type_ |Set the keystore type ++[++`pkcs12`, `jks`, `pem`++]++ |string |false |`pkcs12` +|_ssl.truststore.location_ |Set the truststore location. In case of `pem` type this is the client cert path |string |false | +|_ssl.truststore.password_ |Set the truststore password. In case of `pem` type this is not necessary |string |false | +|_ssl.truststore.type_ |Set the truststore type ++[++`pkcs12`, `jks`, `pem`++]++ |string |false |`pkcs12` +|_topic_ |Set the MQTT topic. If not set, the channel name is used |string |false | +|_trust-all_ |Set whether all server certificates should be trusted |boolean |false |`false` +|_unsubscribe-on-disconnection_ |This flag restore the old behavior to unsubscribe from the broken on disconnection |boolean |false |`false` +|_username_ |Set the username to connect to the server |string |false | +|_will-flag_ |Set if will information are provided on connection |boolean |false |`false` +|_will-qos_ |Set the QoS level for the will message |int |false |`0` +|_will-retain_ |Set if the will message must be retained |boolean |false |`false` +|=== diff --git a/content/docs/smallrye-reactive-messaging/main/_generated/smallrye-pulsar-incoming.adoc b/content/docs/smallrye-reactive-messaging/main/_generated/smallrye-pulsar-incoming.adoc new file mode 100644 index 000000000..6d8f6f7e1 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_generated/smallrye-pulsar-incoming.adoc @@ -0,0 +1,21 @@ +[cols="<,<,^,^,<",options="header",] +|=== +|Attribute (_alias_) |Description |Type |Mandatory |Default +|_ack-strategy_ |Specify the commit strategy to apply when a message produced from a record is acknowledged. Values can be `ack`, `cumulative`. |string |false |`ack` +|_ackTimeout.redeliveryBackoff_ |Comma separated values for configuring ack timeout MultiplierRedeliveryBackoff, min delay, max delay, multiplier. |string |false | +|_batchReceive_ |Whether batch receive is used to consume messages |boolean |false |`false` +|_client-configuration_ |Identifier of a CDI bean that provides the default Pulsar client configuration for this channel. The channel configuration can still override any attribute. The bean must have a type of Map++<++String, Object++>++ and must use the @io.smallrye.common.annotation.Identifier qualifier to set the identifier. |string |false | +|_consumer-configuration_ |Identifier of a CDI bean that provides the default Pulsar consumer configuration for this channel. The channel configuration can still override any attribute. The bean must have a type of Map++<++String, Object++>++ and must use the @io.smallrye.common.annotation.Identifier qualifier to set the identifier. |string |false | +|_deadLetterPolicy.deadLetterTopic_ |Name of the dead letter topic where the failing messages will be sent |string |false | +|_deadLetterPolicy.initialSubscriptionName_ |Name of the initial subscription name of the dead letter topic |string |false | +|_deadLetterPolicy.maxRedeliverCount_ |Maximum number of times that a message will be redelivered before being sent to the dead letter topic |int |false | +|_deadLetterPolicy.retryLetterTopic_ |Name of the retry topic where the failing messages will be sent |string |false | +|_failure-strategy_ |Specify the failure strategy to apply when a message produced from a record is acknowledged negatively (nack). Values can be `nack` (default), `fail`, `ignore` or ++`++reconsume-later |string |false |`nack` +|_health-enabled_ |Whether health reporting is enabled (default) or disabled |boolean |false |`true` +|_negativeAck.redeliveryBackoff_ |Comma separated values for configuring negative ack MultiplierRedeliveryBackoff, min delay, max delay, multiplier. |string |false | +|_reconsumeLater.delay_ |Default delay for reconsume failure-strategy, in seconds |long |false |`3` +|_schema_ |The Pulsar schema type of this channel. When configured a schema is built with the given SchemaType and used for the channel. When absent, the schema is resolved searching for a CDI bean typed `Schema` qualified with `@Identifier` and the channel name. As a fallback AUTO++_++CONSUME or AUTO++_++PRODUCE are used. |string |false | +|_serviceUrl_ |The service URL for the Pulsar service |string |false |`pulsar://localhost:6650` +|_topic_ |The consumed / populated Pulsar topic. If not set, the channel name is used |string |false | +|_tracing-enabled_ |Whether tracing is enabled (default) or disabled |boolean |false |`true` +|=== diff --git a/content/docs/smallrye-reactive-messaging/main/_generated/smallrye-pulsar-outgoing.adoc b/content/docs/smallrye-reactive-messaging/main/_generated/smallrye-pulsar-outgoing.adoc new file mode 100644 index 000000000..9d44f7b1d --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_generated/smallrye-pulsar-outgoing.adoc @@ -0,0 +1,13 @@ +[cols="<,<,^,^,<",options="header",] +|=== +|Attribute (_alias_) |Description |Type |Mandatory |Default +|_client-configuration_ |Identifier of a CDI bean that provides the default Pulsar client configuration for this channel. The channel configuration can still override any attribute. The bean must have a type of Map++<++String, Object++>++ and must use the @io.smallrye.common.annotation.Identifier qualifier to set the identifier. |string |false | +|_health-enabled_ |Whether health reporting is enabled (default) or disabled |boolean |false |`true` +|_max-inflight-messages_ |The maximum size of a queue holding pending messages, i.e messages waiting to receive an acknowledgment from a broker. Defaults to 1000 messages |int |false | +|_producer-configuration_ |Identifier of a CDI bean that provides the default Pulsar producer configuration for this channel. The channel configuration can still override any attribute. The bean must have a type of Map++<++String, Object++>++ and must use the @io.smallrye.common.annotation.Identifier qualifier to set the identifier. |string |false | +|_schema_ |The Pulsar schema type of this channel. When configured a schema is built with the given SchemaType and used for the channel. When absent, the schema is resolved searching for a CDI bean typed `Schema` qualified with `@Identifier` and the channel name. As a fallback AUTO++_++CONSUME or AUTO++_++PRODUCE are used. |string |false | +|_serviceUrl_ |The service URL for the Pulsar service |string |false |`pulsar://localhost:6650` +|_topic_ |The consumed / populated Pulsar topic. If not set, the channel name is used |string |false | +|_tracing-enabled_ |Whether tracing is enabled (default) or disabled |boolean |false |`true` +|_waitForWriteCompletion_ |Whether the client waits for the broker to acknowledge the written record before acknowledging the message |boolean |false |`true` +|=== diff --git a/content/docs/smallrye-reactive-messaging/main/_generated/smallrye-sns-outgoing.adoc b/content/docs/smallrye-reactive-messaging/main/_generated/smallrye-sns-outgoing.adoc new file mode 100644 index 000000000..c25ed7a4f --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_generated/smallrye-sns-outgoing.adoc @@ -0,0 +1,17 @@ +[cols="<,<,^,^,<",options="header",] +|=== +|Attribute (_alias_) |Description |Type |Mandatory |Default +|_batch_ |When set, sends messages in batches of maximum 10 messages |boolean |false |`false` +|_batch-delay_ |In batch send mode, the maximum delay in milliseconds to wait for messages to be included in the batch. Defaults to 3000 ms |int |false |`3000` +|_batch-size_ |In batch send mode, the maximum number of messages to include in batch, currently SNS maximum is 10 messages |int |false |`10` +|_credentials-provider_ |The credential provider to be used in the client, defaults to AWS default provider chain |string |false | +|_email.subject_ |When set, sends messages with the specified subject |string |false | +|_endpoint-override_ |The endpoint override |string |false | +|_group.id_ |When set, sends messages with the specified group id |string |false | +|_health-enabled_ |Whether health reporting is enabled (default) or disabled |boolean |false |`true` +|_message-structure_ |Set to json if you want to send a different message for each protocol. |string |false | +|_region_ |The name of the SNS region, defaults to AWS region resolver |string |false | +|_sms.phoneNumber_ |When set, sends messages to the specified phone number. Not supported in combination with batching. |string |false | +|_topic_ |The name of the SNS topic, defaults to channel name if not provided |string |false | +|_topic.arn_ |The arn of the SNS topic |string |false | +|=== diff --git a/content/docs/smallrye-reactive-messaging/main/_generated/smallrye-sqs-incoming.adoc b/content/docs/smallrye-reactive-messaging/main/_generated/smallrye-sqs-incoming.adoc new file mode 100644 index 000000000..b461eea07 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_generated/smallrye-sqs-incoming.adoc @@ -0,0 +1,21 @@ +[cols="<,<,^,^,<",options="header",] +|=== +|Attribute (_alias_) |Description |Type |Mandatory |Default +|_ack-strategy_ |The identifier for the bean implementing ack strategy factory. Strategies: 'delete', 'ignore' |string |false |`delete` +|_ack.delete_ |_deprecated_ - Whether the acknowledgement deletes the message from the queue. Deprecated, use ack-strategy instead |boolean |false | +|_credentials-provider_ |The credential provider to be used in the client |string |false | +|_endpoint-override_ |The endpoint override |string |false | +|_failure-strategy_ |The identifier for the bean implementing failure strategy factory. Strategies: 'ignore', 'fail', 'visibility', 'delete' |string |false |`ignore` +|_health-enabled_ |Whether health reporting is enabled (default) or disabled |boolean |false |`true` +|_max-number-of-messages_ |The maximum number of messages to receive |int |false |`10` +|_queue_ |The name of the SQS queue, defaults to channel name if not provided |string |false | +|_queue.url_ |The url of the SQS queue |string |false | +|_receive.request.customizer_ |The identifier for the bean implementing a customizer to receive requests, defaults to channel name if not provided |string |false | +|_receive.request.message-attribute-names_ |The message attribute names to retrieve when receiving messages. |string |false | +|_receive.request.pause.resume_ |Whether the polling must be paused when the application does not request items and resume when it does. This allows implementing back-pressure based on the application capacity. Note that polling is not stopped, but will not retrieve any records when paused. |boolean |false |`true` +|_receive.request.retries_ |If set to a positive number, the connector will try to retry the request that was not delivered successfully (with a potentially transient error) until the number of retries is reached. If set to 0, retries are disabled. |long |false |`2147483647` +|_region_ |The name of the SQS region |string |false | +|_tracing-enabled_ |Whether tracing is enabled (default) or disabled |boolean |false |`true` +|_visibility-timeout_ |The duration in seconds that the received messages are hidden from subsequent retrieve requests after being retrieved by a receive request |int |false | +|_wait-time-seconds_ |The maximum amount of time in seconds to wait for messages to be received |int |false |`1` +|=== diff --git a/content/docs/smallrye-reactive-messaging/main/_generated/smallrye-sqs-outgoing.adoc b/content/docs/smallrye-reactive-messaging/main/_generated/smallrye-sqs-outgoing.adoc new file mode 100644 index 000000000..28e23dc2c --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_generated/smallrye-sqs-outgoing.adoc @@ -0,0 +1,15 @@ +[cols="<,<,^,^,<",options="header",] +|=== +|Attribute (_alias_) |Description |Type |Mandatory |Default +|_batch_ |When set, sends messages in batches of maximum 10 messages |boolean |false |`false` +|_batch-delay_ |In batch send mode, the maximum delay in milliseconds to wait for messages to be included in the batch |int |false |`3000` +|_batch-size_ |In batch send mode, the maximum number of messages to include in batch, currently SQS maximum is 10 messages |int |false |`10` +|_credentials-provider_ |The credential provider to be used in the client |string |false | +|_endpoint-override_ |The endpoint override |string |false | +|_group.id_ |When set, sends messages with the specified group id |string |false | +|_health-enabled_ |Whether health reporting is enabled (default) or disabled |boolean |false |`true` +|_queue_ |The name of the SQS queue, defaults to channel name if not provided |string |false | +|_queue.url_ |The url of the SQS queue |string |false | +|_region_ |The name of the SQS region |string |false | +|_tracing-enabled_ |Whether tracing is enabled (default) or disabled |boolean |false |`true` +|=== diff --git a/content/docs/smallrye-reactive-messaging/main/_nav.yml b/content/docs/smallrye-reactive-messaging/main/_nav.yml new file mode 100644 index 000000000..d70bbd297 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_nav.yml @@ -0,0 +1,194 @@ +title: SmallRye Reactive Messaging +nav: +- title: Home + page: index +- title: Core + items: + - title: Getting Started + page: getting-started + - title: Concepts + items: + - title: Introduction + page: concepts/concepts + - title: Development Model + page: concepts/model + - title: Emitters and Channel + page: concepts/emitter + - title: Connectors + page: concepts/connectors + - title: Contributing Connectors + page: concepts/contributing-connectors + - title: Acknowledgement + page: concepts/acknowledgement + - title: Blocking Processing + page: concepts/blocking + - title: Method Signatures + page: concepts/signatures + - title: Skipping Messages + page: concepts/skipping + - title: Message Converters + page: concepts/converters + - title: Keyed Streams + page: concepts/keyed-multi + - title: Channel Decorators and Interceptors + page: concepts/decorators + - title: Broadcast + page: concepts/broadcast + - title: Merge channels + page: concepts/merge + - title: Incoming Channel Concurrency + page: concepts/incoming-concurrency + - title: '@Incomings' + page: concepts/incomings + - title: '@Outgoings' + page: concepts/outgoings + - title: Testing + page: concepts/testing + - title: Logging + page: concepts/logging + - title: Observability API + page: concepts/observability + - title: Advanced Configuration + page: concepts/advanced-config + - title: Message Context + page: concepts/message-context + - title: Metadata Injection + page: concepts/incoming-metadata-injection + - title: Generic Payloads + page: concepts/generic-payloads + - title: Pausable Channels + page: concepts/pausable-channels +- title: Kafka + items: + - page: kafka/kafka + - title: Receiving records + page: kafka/receiving-kafka-records + - title: Writing records + page: kafka/writing-kafka-records + - title: Health Checks + page: kafka/health + - title: Using Avro + page: kafka/avro-configuration + - title: Using Protobuf + page: kafka/protobuf-configuration + - title: Rebalance Listeners + page: kafka/consumer-rebalance-listener + - title: Kerberos authentication + page: kafka/kerberos + - title: Accessing the client + page: kafka/client-service + - title: Customizing Default Kafka Configuration + page: kafka/default-configuration + - title: Test Companion for Kafka + page: kafka/test-companion + - title: Kafka Transactions and Exactly-Once Processing + page: kafka/transactions + - title: Kafka Request/Reply + page: kafka/request-reply +- title: AMQP 1.0 + items: + - page: amqp/amqp + - title: Receiving messages + page: amqp/receiving-amqp-messages + - title: Sending messages + page: amqp/sending-amqp-messages + - title: AMQP Request/Reply + page: amqp/request-reply + - title: CBS Authentication + page: amqp/cbs-authentication + - title: Health Checks + page: amqp/health + - title: Client Customization + page: amqp/client-customization + - title: Using RabbitMQ + page: amqp/rabbitmq +- title: RabbitMQ + items: + - page: rabbitmq/rabbitmq + - title: Receiving messages + page: rabbitmq/receiving-messages-from-rabbitmq + - title: Sending messages + page: rabbitmq/sending-messages-to-rabbitmq + - title: Health Checks + page: rabbitmq/rabbitmq-health + - title: Client Customization + page: rabbitmq/rabbitmq-client-customization + - title: Connecting to managed instances + page: rabbitmq/rabbitmq-cloud + - title: RabbitMQ Request/Reply + page: rabbitmq/request-reply +- title: RabbitMQ OG + items: + - page: rabbitmq-og/rabbitmq-og + - title: Receiving messages + page: rabbitmq-og/receiving-messages-from-rabbitmq + - title: Sending messages + page: rabbitmq-og/sending-messages-to-rabbitmq + - title: Health Checks + page: rabbitmq-og/rabbitmq-og-health + - title: Client Customization + page: rabbitmq-og/rabbitmq-og-client-customization + - title: OpenTelemetry Tracing + page: rabbitmq-og/rabbitmq-og-tracing + - title: Connecting to managed instances + page: rabbitmq-og/rabbitmq-og-cloud +- title: Pulsar + items: + - page: pulsar/pulsar + - title: Receiving messages + page: pulsar/receiving-pulsar-messages + - title: Sending messages + page: pulsar/sending-messages-to-pulsar + - title: Configuring the Schema + page: pulsar/schema-configuration + - title: Configuring the Pulsar client + page: pulsar/client-configuration + - title: Health Checks + page: pulsar/health + - title: Accessing the client + page: pulsar/client-service + - title: Pulsar Transactions and Exactly-Once Processing + page: pulsar/transactions +- title: Apache Camel + items: + - page: camel/camel + - title: Receiving messages from Camel + page: camel/receiving-messages-from-camel + - title: Sending messages to Camel + page: camel/sending-messages-to-camel + - title: Implementing Camel processor + page: camel/camel-processor + - title: Using existing Camel routes + page: camel/using-existing-routes +- title: JMS + items: + - page: jms/jms + - title: Receiving JMS messages + page: jms/receiving-jms-messages + - title: Sending JMS messages + page: jms/sending-jms-messages + - title: JMS Transactions + page: jms/jms-transactions + - title: Advanced configuration + page: jms/advanced-jms +- title: MQTT + items: + - page: mqtt/mqtt + - title: Receiving MQTT messages + page: mqtt/receiving-mqtt-messages + - title: Sending MQTT messages + page: mqtt/sending-messages-to-mqtt + - title: Customizing the MQTT client + page: mqtt/client-customization +- title: AWS SQS + items: + - page: sqs/sqs + - title: Receiving AWS SQS messages + page: sqs/receiving-aws-sqs-messages + - title: Sending AWS SQS messages + page: sqs/sending-aws-sqs-messages +- title: AWS SNS + items: + - page: sns/sns + - title: Sending AWS SNS messages + page: sns/sending-aws-sns-messages diff --git a/content/docs/smallrye-reactive-messaging/main/_partials/pulsar/config/smallrye-pulsar-client.adoc b/content/docs/smallrye-reactive-messaging/main/_partials/pulsar/config/smallrye-pulsar-client.adoc new file mode 100644 index 000000000..513f4c895 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_partials/pulsar/config/smallrye-pulsar-client.adoc @@ -0,0 +1,58 @@ +[width="100%",cols="<13%,<42%,^17%,^17%,<11%",options="header",] +|=== +|Attribute |Description |Type |Config file |Default +|_serviceUrl_ |Pulsar cluster HTTP URL to connect to a broker. |String |true | +|_serviceUrlProvider_ |The implementation class of ServiceUrlProvider used to generate ServiceUrl. |ServiceUrlProvider |false | +|_authentication_ |Authentication settings of the client. |Authentication |false | +|_authPluginClassName_ |Class name of authentication plugin of the client. |String |true | +|_authParams_ |Authentication parameter of the client. |String |true | +|_authParamMap_ |Authentication map of the client. |Map |true | +|_operationTimeoutMs_ |Client operation timeout (in milliseconds). |long |true |30000 +|_lookupTimeoutMs_ |Client lookup timeout (in milliseconds). |long |true |-1 +|_statsIntervalSeconds_ |Interval to print client stats (in seconds). |long |true |60 +|_numIoThreads_ |Number of IO threads. |int |true |10 +|_numListenerThreads_ |Number of consumer listener threads. |int |true |10 +|_connectionsPerBroker_ |Number of connections established between the client and each Broker. A value of 0 means to disable connection pooling. |int |true |1 +|_connectionMaxIdleSeconds_ |Release the connection if it is not used for more than ++[++connectionMaxIdleSeconds++]++ seconds. If ++[++connectionMaxIdleSeconds++]++ ++<++ 0, disabled the feature that auto release the idle connections |int |true |180 +|_useTcpNoDelay_ |Whether to use TCP NoDelay option. |boolean |true |true +|_useTls_ |Whether to use TLS. |boolean |true |false +|_tlsKeyFilePath_ |Path to the TLS key file. |String |true | +|_tlsCertificateFilePath_ |Path to the TLS certificate file. |String |true | +|_tlsTrustCertsFilePath_ |Path to the trusted TLS certificate file. |String |true | +|_tlsAllowInsecureConnection_ |Whether the client accepts untrusted TLS certificates from the broker. |boolean |true |false +|_tlsHostnameVerificationEnable_ |Whether the hostname is validated when the client creates a TLS connection with brokers. |boolean |true |false +|_concurrentLookupRequest_ |The number of concurrent lookup requests that can be sent on each broker connection. Setting a maximum prevents overloading a broker. |int |true |5000 +|_maxLookupRequest_ |Maximum number of lookup requests allowed on each broker connection to prevent overloading a broker. |int |true |50000 +|_maxLookupRedirects_ |Maximum times of redirected lookup requests. |int |true |20 +|_maxNumberOfRejectedRequestPerConnection_ |Maximum number of rejected requests of a broker in a certain time frame (60 seconds) after the current connection is closed and the client creating a new connection to connect to a different broker. |int |true |50 +|_keepAliveIntervalSeconds_ |Seconds of keeping alive interval for each client broker connection. |int |true |30 +|_connectionTimeoutMs_ |Duration of waiting for a connection to a broker to be established.If the duration passes without a response from a broker, the connection attempt is dropped. |int |true |10000 +|_requestTimeoutMs_ |Maximum duration for completing a request. |int |true |60000 +|_readTimeoutMs_ |Maximum read time of a request. |int |true |60000 +|_autoCertRefreshSeconds_ |Seconds of auto refreshing certificate. |int |true |300 +|_initialBackoffIntervalNanos_ |Initial backoff interval (in nanosecond). |long |true |100000000 +|_maxBackoffIntervalNanos_ |Max backoff interval (in nanosecond). |long |true |60000000000 +|_enableBusyWait_ |Whether to enable BusyWait for EpollEventLoopGroup. |boolean |true |false +|_listenerName_ |Listener name for lookup. Clients can use listenerName to choose one of the listeners as the service URL to create a connection to the broker as long as the network is accessible."advertisedListeners" must enabled in broker side. |String |true | +|_useKeyStoreTls_ |Set TLS using KeyStore way. |boolean |true |false +|_sslProvider_ |The TLS provider used by an internal client to authenticate with other Pulsar brokers. |String |true | +|_tlsKeyStoreType_ |TLS KeyStore type configuration. |String |true |JKS +|_tlsKeyStorePath_ |Path of TLS KeyStore. |String |true | +|_tlsKeyStorePassword_ |Password of TLS KeyStore. |String |true | +|_tlsTrustStoreType_ |TLS TrustStore type configuration. You need to set this configuration when client authentication is required. |String |true |JKS +|_tlsTrustStorePath_ |Path of TLS TrustStore. |String |true | +|_tlsTrustStorePassword_ |Password of TLS TrustStore. |String |true | +|_tlsCiphers_ |Set of TLS Ciphers. |Set |true |++[]++ +|_tlsProtocols_ |Protocols of TLS. |Set |true |++[]++ +|_memoryLimitBytes_ |Limit of client memory usage (in byte). The 64M default can guarantee a high producer throughput. |long |true |67108864 +|_proxyServiceUrl_ |URL of proxy service. proxyServiceUrl and proxyProtocol must be mutually inclusive. |String |true | +|_proxyProtocol_ |Protocol of proxy service. proxyServiceUrl and proxyProtocol must be mutually inclusive. |ProxyProtocol |true | +|_enableTransaction_ |Whether to enable transaction. |boolean |true |false +|_clock_ | |Clock |false | +|_dnsLookupBindAddress_ |The Pulsar client dns lookup bind address, default behavior is bind on 0.0.0.0 |String |true | +|_dnsLookupBindPort_ |The Pulsar client dns lookup bind port, takes effect when dnsLookupBindAddress is configured, default value is 0. |int |true |0 +|_socks5ProxyAddress_ |Address of SOCKS5 proxy. |InetSocketAddress |true | +|_socks5ProxyUsername_ |User name of SOCKS5 proxy. |String |true | +|_socks5ProxyPassword_ |Password of SOCKS5 proxy. |String |true | +|_description_ |The extra description of the client version. The length cannot exceed 64. |String |true | +|=== diff --git a/content/docs/smallrye-reactive-messaging/main/_partials/pulsar/config/smallrye-pulsar-consumer.adoc b/content/docs/smallrye-reactive-messaging/main/_partials/pulsar/config/smallrye-pulsar-consumer.adoc new file mode 100644 index 000000000..1f4b499aa --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_partials/pulsar/config/smallrye-pulsar-consumer.adoc @@ -0,0 +1,137 @@ +[width="100%",cols="<13%,<42%,^17%,^17%,<11%",options="header",] +|=== +|Attribute |Description |Type |Config file |Default +|_topicNames_ |Topic name |Set |true |++[]++ +|_topicsPattern_ |Topic pattern |Pattern |true | +|_subscriptionName_ |Subscription name |String |true | +|_subscriptionType_ |Subscription type. + +Four subscription types are available: + +++*++ Exclusive + +++*++ Failover + +++*++ Shared + +++*++ Key++_++Shared |SubscriptionType |true |Exclusive +|_subscriptionProperties_ | |Map |true | +|_subscriptionMode_ | |SubscriptionMode |true |Durable +|_messageListener_ | |MessageListener |false | +|_consumerEventListener_ | |ConsumerEventListener |false | +|_negativeAckRedeliveryBackoff_ |Interface for custom message is negativeAcked policy. You can specify `RedeliveryBackoff` for a consumer. |RedeliveryBackoff |false | +|_ackTimeoutRedeliveryBackoff_ |Interface for custom message is ackTimeout policy. You can specify `RedeliveryBackoff` for a consumer. |RedeliveryBackoff |false | +|_receiverQueueSize_ |Size of a consumer's receiver queue. + + + +For example, the number of messages accumulated by a consumer before an application calls `Receive`. + + + +A value higher than the default value increases consumer throughput, though at the expense of more memory utilization. |int |true |1000 +|_acknowledgementsGroupTimeMicros_ |Group a consumer acknowledgment for a specified time. + + + +By default, a consumer uses 100ms grouping time to send out acknowledgments to a broker. + + + +Setting a group time of 0 sends out acknowledgments immediately. + + + +A longer ack group time is more efficient at the expense of a slight increase in message re-deliveries after a failure. |long |true |100000 +|_maxAcknowledgmentGroupSize_ |Group a consumer acknowledgment for the number of messages. |int |true |1000 +|_negativeAckRedeliveryDelayMicros_ |Delay to wait before redelivering messages that failed to be processed. + + + +When an application uses `Consumer#negativeAcknowledge(Message)`, failed messages are redelivered after a fixed timeout. |long |true |60000000 +|_maxTotalReceiverQueueSizeAcrossPartitions_ |The max total receiver queue size across partitions. + + + +This setting reduces the receiver queue size for individual partitions if the total receiver queue size exceeds this value. |int |true |50000 +|_consumerName_ |Consumer name |String |true | +|_ackTimeoutMillis_ |Timeout of unacked messages |long |true |0 +|_tickDurationMillis_ |Granularity of the ack-timeout redelivery. + + + +Using an higher `tickDurationMillis` reduces the memory overhead to track messages when setting ack-timeout to a bigger value (for example, 1 hour). |long |true |1000 +|_priorityLevel_ |Priority level for a consumer to which a broker gives more priority while dispatching messages in Shared subscription type. + + + +The broker follows descending priorities. For example, 0=max-priority, 1, 2,... + + + +In Shared subscription type, the broker *first dispatches messages to the max priority level consumers if they have permits*. Otherwise, the broker considers next priority level consumers. + + + +**Example 1** + +If a subscription has consumerA with `priorityLevel` 0 and consumerB with `priorityLevel` 1, then the broker *only dispatches messages to consumerA until it runs out permits* and then starts dispatching messages to consumerB. + + + +**Example 2** + +Consumer Priority, Level, Permits + +C1, 0, 2 + +C2, 0, 1 + +C3, 0, 1 + +C4, 1, 2 + +C5, 1, 1 + + + +Order in which a broker dispatches messages to consumers is: C1, C2, C3, C1, C4, C5, C4. |int |true |0 +|_maxPendingChunkedMessage_ |The maximum size of a queue holding pending chunked messages. When the threshold is reached, the consumer drops pending messages to optimize memory utilization. |int |true |10 +|_autoAckOldestChunkedMessageOnQueueFull_ |Whether to automatically acknowledge pending chunked messages when the threshold of `maxPendingChunkedMessage` is reached. If set to `false`, these messages will be redelivered by their broker. |boolean |true |false +|_expireTimeOfIncompleteChunkedMessageMillis_ |The time interval to expire incomplete chunks if a consumer fails to receive all the chunks in the specified time period. The default value is 1 minute. |long |true |60000 +|_cryptoKeyReader_ | |CryptoKeyReader |false | +|_messageCrypto_ | |MessageCrypto |false | +|_cryptoFailureAction_ |Consumer should take action when it receives a message that can not be decrypted. + +++*++ *FAIL*: this is the default option to fail messages until crypto succeeds. + +++*++ *DISCARD*:silently acknowledge and not deliver message to an application. + +++*++ *CONSUME*: deliver encrypted messages to applications. It is the application's responsibility to decrypt the message. + + + +The decompression of message fails. + + + +If messages contain batch messages, a client is not be able to retrieve individual messages in batch. + + + +Delivered encrypted message contains `EncryptionContext` which contains encryption and compression information in it using which application can decrypt consumed message payload. |ConsumerCryptoFailureAction |true |FAIL +|_properties_ |A name or value property of this consumer. + + + +`properties` is application defined metadata attached to a consumer. + + + +When getting a topic stats, associate this metadata with the consumer stats for easier identification. |SortedMap |true |++{++} +|_readCompacted_ |If enabling `readCompacted`, a consumer reads messages from a compacted topic rather than reading a full message backlog of a topic. + + + +A consumer only sees the latest value for each key in the compacted topic, up until reaching the point in the topic message when compacting backlog. Beyond that point, send messages as normal. + + + +Only enabling `readCompacted` on subscriptions to persistent topics, which have a single active consumer (like failure or exclusive subscriptions). + + + +Attempting to enable it on subscriptions to non-persistent topics or on shared subscriptions leads to a subscription call throwing a `PulsarClientException`. |boolean |true |false +|_subscriptionInitialPosition_ |Initial position at which to set cursor when subscribing to a topic at first time. |SubscriptionInitialPosition |true |Latest +|_patternAutoDiscoveryPeriod_ |Topic auto discovery period when using a pattern for topic's consumer. + + + +The default and minimum value is 1 minute. |int |true |60 +|_regexSubscriptionMode_ |When subscribing to a topic using a regular expression, you can pick a certain type of topics. + + + +++*++ *PersistentOnly*: only subscribe to persistent topics. + +++*++ *NonPersistentOnly*: only subscribe to non-persistent topics. + +++*++ *AllTopics*: subscribe to both persistent and non-persistent topics. |RegexSubscriptionMode |true |PersistentOnly +|_deadLetterPolicy_ |Dead letter policy for consumers. + + + +By default, some messages are probably redelivered many times, even to the extent that it never stops. + + + +By using the dead letter mechanism, messages have the max redelivery count. *When exceeding the maximum number of redeliveries, messages are sent to the Dead Letter Topic and acknowledged automatically*. + + + +You can enable the dead letter mechanism by setting `deadLetterPolicy`. + + + +**Example** + + + +client.newConsumer() + +.deadLetterPolicy(DeadLetterPolicy.builder().maxRedeliverCount(10).build()) + +.subscribe(); + + + +Default dead letter topic name is `++{++TopicName}-++{++Subscription}-DLQ`. + + + +To set a custom dead letter topic name: + + + +client.newConsumer() + +.deadLetterPolicy(DeadLetterPolicy.builder().maxRedeliverCount(10) + +.deadLetterTopic("your-topic-name").build()) + +.subscribe(); + + + +When specifying the dead letter policy while not specifying `ackTimeoutMillis`, you can set the ack timeout to 30000 millisecond. |DeadLetterPolicy |true | +|_retryEnable_ | |boolean |true |false +|_batchReceivePolicy_ | |BatchReceivePolicy |false | +|_autoUpdatePartitions_ |If `autoUpdatePartitions` is enabled, a consumer subscribes to partition increasement automatically. + + + +**Note**: this is only for partitioned consumers. |boolean |true |true +|_autoUpdatePartitionsIntervalSeconds_ | |long |true |60 +|_replicateSubscriptionState_ |If `replicateSubscriptionState` is enabled, a subscription state is replicated to geo-replicated clusters. |boolean |true |false +|_resetIncludeHead_ | |boolean |true |false +|_keySharedPolicy_ | |KeySharedPolicy |false | +|_batchIndexAckEnabled_ | |boolean |true |false +|_ackReceiptEnabled_ | |boolean |true |false +|_poolMessages_ | |boolean |true |false +|_payloadProcessor_ | |MessagePayloadProcessor |false | +|_startPaused_ | |boolean |true |false +|_autoScaledReceiverQueueSizeEnabled_ | |boolean |true |false +|_topicConfigurations_ | |List |true |++[]++ +|=== diff --git a/content/docs/smallrye-reactive-messaging/main/_partials/pulsar/config/smallrye-pulsar-producer.adoc b/content/docs/smallrye-reactive-messaging/main/_partials/pulsar/config/smallrye-pulsar-producer.adoc new file mode 100644 index 000000000..e8755e86b --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/_partials/pulsar/config/smallrye-pulsar-producer.adoc @@ -0,0 +1,60 @@ +[width="100%",cols="<13%,<42%,^17%,^17%,<11%",options="header",] +|=== +|Attribute |Description |Type |Config file |Default +|_topicName_ |Topic name |String |true | +|_producerName_ |Producer name |String |true | +|_sendTimeoutMs_ |Message send timeout in ms. + +If a message is not acknowledged by a server before the `sendTimeout` expires, an error occurs. |long |true |30000 +|_blockIfQueueFull_ |If it is set to `true`, when the outgoing message queue is full, the `Send` and `SendAsync` methods of producer block, rather than failing and throwing errors. + +If it is set to `false`, when the outgoing message queue is full, the `Send` and `SendAsync` methods of producer fail and `ProducerQueueIsFullError` exceptions occur. + + + +The `MaxPendingMessages` parameter determines the size of the outgoing message queue. |boolean |true |false +|_maxPendingMessages_ |The maximum size of a queue holding pending messages. + + + +For example, a message waiting to receive an acknowledgment from a https://pulsar.apache.org/docs/reference-terminology#broker[broker]. + + + +By default, when the queue is full, all calls to the `Send` and `SendAsync` methods fail *unless* you set `BlockIfQueueFull` to `true`. |int |true |0 +|_maxPendingMessagesAcrossPartitions_ |The maximum number of pending messages across partitions. + + + +Use the setting to lower the max pending messages for each partition (`#setMaxPendingMessages(int)`) if the total number exceeds the configured value. |int |true |0 +|_messageRoutingMode_ |Message routing logic for producers on https://pulsar.apache.org/docs/concepts-architecture-overview#partitioned-topics[partitioned topics]. + +Apply the logic only when setting no key on messages. + +Available options are as follows: + +++*++ `pulsar.RoundRobinDistribution`: round robin + +++*++ `pulsar.UseSinglePartition`: publish all messages to a single partition + +++*++ `pulsar.CustomPartition`: a custom partitioning scheme |MessageRoutingMode |true | +|_hashingScheme_ |Hashing function determining the partition where you publish a particular message (partitioned topics only). + +Available options are as follows: + +++*++ `pulsar.JavastringHash`: the equivalent of `string.hashCode()` in Java + +++*++ `pulsar.Murmur3++_++32Hash`: applies the https://en.wikipedia.org/wiki/MurmurHash[Murmur3] hashing function + +++*++ `pulsar.BoostHash`: applies the hashing function from C{plus}{plus}'shttps://www.boost.org/doc/libs/1_62_0/doc/html/hash.html[Boost] library |HashingScheme |true |JavaStringHash +|_cryptoFailureAction_ |Producer should take action when encryption fails. + +++*++ *FAIL*: if encryption fails, unencrypted messages fail to send. + +++*++ *SEND*: if encryption fails, unencrypted messages are sent. |ProducerCryptoFailureAction |true |FAIL +|_customMessageRouter_ | |MessageRouter |false | +|_batchingMaxPublishDelayMicros_ |Batching time period of sending messages. |long |true |1000 +|_batchingPartitionSwitchFrequencyByPublishDelay_ | |int |true |10 +|_batchingMaxMessages_ |The maximum number of messages permitted in a batch. |int |true |1000 +|_batchingMaxBytes_ | |int |true |131072 +|_batchingEnabled_ |Enable batching of messages. |boolean |true |true +|_batcherBuilder_ | |BatcherBuilder |false | +|_chunkingEnabled_ |Enable chunking of messages. |boolean |true |false +|_chunkMaxMessageSize_ | |int |true |-1 +|_cryptoKeyReader_ | |CryptoKeyReader |false | +|_messageCrypto_ | |MessageCrypto |false | +|_encryptionKeys_ | |Set |true |++[]++ +|_compressionType_ |Message data compression type used by a producer. + +Available options: + +++*++ https://github.com/lz4/lz4[LZ4] + +++*++ https://zlib.net/[ZLIB] + +++*++ https://facebook.github.io/zstd/[ZSTD] + +++*++ https://google.github.io/snappy/[SNAPPY] |CompressionType |true |NONE +|_initialSequenceId_ | |Long |true | +|_autoUpdatePartitions_ | |boolean |true |true +|_autoUpdatePartitionsIntervalSeconds_ | |long |true |60 +|_multiSchema_ | |boolean |true |true +|_accessMode_ | |ProducerAccessMode |true |Shared +|_lazyStartPartitionedProducers_ | |boolean |true |false +|_properties_ | |SortedMap |true |++{++} +|_initialSubscriptionName_ |Use this configuration to automatically create an initial subscription when creating a topic. If this field is not set, the initial subscription is not created. |String |true | +|=== diff --git a/content/docs/smallrye-reactive-messaging/main/amqp/amqp.adoc b/content/docs/smallrye-reactive-messaging/main/amqp/amqp.adoc new file mode 100644 index 000000000..d4326ffeb --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/amqp/amqp.adoc @@ -0,0 +1,51 @@ += AMQP 1.0 Connector +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The AMQP Connector adds support for AMQP 1.0 to Reactive Messaging. + +Advanced Message Queuing Protocol 1.0 (AMQP 1.0) is an open standard for +passing business messages between applications or organizations. + +With this connector, your application can: + +* receive messages from an AMQP Broker or Router. +* send `Message` to an AMQP _address_ +* implement xref:amqp/request-reply.adoc[request-reply] messaging patterns +* authenticate using xref:amqp/cbs-authentication.adoc[Claims-Based Security (CBS)] tokens + +The AMQP connector is based on the https://vertx.io/docs/vertx-amqp-client/java/[Vert.x AMQP Client]. + +Using the AMQP connector + +To use the AMQP Connector, add the following dependency to your project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.reactive + smallrye-reactive-messaging-amqp + {project-version} + +---- + +The connector name is: `smallrye-amqp`. + +So, to indicate that a channel is managed by this connector you need: + +[source,properties] +---- +# Inbound +mp.messaging.incoming.[channel-name].connector=smallrye-amqp + +# Outbound +mp.messaging.outgoing.[channel-name].connector=smallrye-amqp +---- + +[IMPORTANT] +.RabbitMQ +==== + +To use RabbitMQ, refer to xref:#amqp-rabbitmq[Using RabbitMQ]. + +==== diff --git a/content/docs/smallrye-reactive-messaging/main/amqp/cbs-authentication.adoc b/content/docs/smallrye-reactive-messaging/main/amqp/cbs-authentication.adoc new file mode 100644 index 000000000..5b5fd6132 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/amqp/cbs-authentication.adoc @@ -0,0 +1,186 @@ += Claims-Based Security (CBS) Authentication +:version-root: ../ +include::{version-root}_attributes.adoc[] + +[WARNING] +.Experimental +==== + +CBS authentication support is an experimental feature. + +==== + +The AMQP connector supports https://docs.oasis-open.org/amqp/amqp-cbs/v1.0/amqp-cbs-v1.0.html[Claims-Based Security (CBS)] for token-based authentication with AMQP 1.0 brokers. +CBS allows the client to authenticate by sending a security token (such as a JWT or SAS token) to the broker over the AMQP connection, rather than relying solely on SASL mechanisms. +This is commonly used with brokers like *Azure Service Bus*. + +== Enabling CBS + +To enable CBS authentication on a channel, set the `cbs.enabled` attribute: + +[source,properties] +---- +mp.messaging.incoming.my-channel.connector=smallrye-amqp +mp.messaging.incoming.my-channel.host=my-broker.servicebus.windows.net +mp.messaging.incoming.my-channel.port=5671 +mp.messaging.incoming.my-channel.use-ssl=true +mp.messaging.incoming.my-channel.cbs.enabled=true +---- + +CBS can be enabled on both incoming and outgoing channels independently. + +== Providing Tokens + +When CBS is enabled, the connector requires a CDI bean implementing +link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-amqp/{javadoc-version}/io/smallrye/reactive/messaging/amqp/cbs/CbsTokenProvider.html[`CbsTokenProvider`]. +This bean is called during initial connection and on each token refresh cycle. + +[source,java] +---- +include::{examples-dir}/amqp/customization/MyCbsTokenProvider.java[tags=provider,indent=0] +---- + +The `getToken` method receives the channel configuration, allowing you to adapt token generation per channel (e.g., using different audiences or scopes). +It returns a `Uni++<++CbsToken++>++` to support non-blocking token retrieval from external identity providers. + +=== The CbsToken Interface + +A `CbsToken` provides: + +[width="100%",cols="16%,84%",options="header",] +|=== +|Method |Description +|`token()` |The token string value (JWT, SAS token, etc.) +|`type()` |The token type (e.g., `"jwt"` or `"servicebus.windows.net:sastoken"`) +|`expiresAt()` |When the token expires +|`refreshAt()` |When the token should be refreshed (defaults to `expiresAt()` if not set) +|=== + +The `DefaultCbsToken` record provides a convenient implementation: + +[source,java] +---- +// With explicit refresh time (refresh before expiration) +new DefaultCbsToken(token, "jwt", expiresAt, refreshAt); + +// Refresh at expiration +new DefaultCbsToken(token, "jwt", expiresAt); + +// Using validity duration in seconds +new DefaultCbsToken(token, "jwt", 3600); +---- + +== Token Refresh + +The connector automatically refreshes tokens before they expire. +After a successful authorization, a timer is scheduled based on the `refreshAt()` value of the token. + +* If `refreshAt()` is set earlier than `expiresAt()`, the token is refreshed proactively. +* If the refresh fails after retries, the connection is closed and re-established. + +Setting `refreshAt()` earlier than `expiresAt()` is recommended to avoid authentication gaps during refresh: + +[source,java] +---- +new DefaultCbsToken( + token, "jwt", + OffsetDateTime.now().plusHours(1), // expires in 1 hour + OffsetDateTime.now().plusMinutes(45)); // refresh after 45 minutes +---- + +== CBS Exchange Protocols + +The connector supports two CBS exchange protocols, configured with `cbs.exchange`: + +=== `set-token` (default) + +The `set-token` protocol sends the token as an AMQP message to the CBS node with: + +* Subject: `set-token` +* Application property `token-type`: the token type +* Body: the token string + +The broker acknowledges the message to confirm the token was accepted. + +[source,properties] +---- +mp.messaging.incoming.my-channel.cbs.exchange=set-token +---- + +=== `put-token` + +The `put-token` protocol uses a request-reply exchange, compatible with *Azure Service Bus*. +It sends the token as a request and validates the response status code. + +[source,properties] +---- +mp.messaging.incoming.my-channel.cbs.exchange=put-token +mp.messaging.incoming.my-channel.cbs.audience=my-namespace.servicebus.windows.net/my-queue +---- + +When using `put-token`, the `cbs.audience` attribute sets the token audience. +If not configured, the audience is derived from the broker host and target address in the format `amqp://HOST/ADDRESS`. + +== Connection Sharing with CBS + +When multiple channels share the same connection using `container-id`, CBS authentication is performed once for the shared connection: + +[source,properties] +---- +mp.messaging.incoming.my-channel-in.connector=smallrye-amqp +mp.messaging.incoming.my-channel-in.container-id=shared +mp.messaging.incoming.my-channel-in.cbs.enabled=true + +mp.messaging.outgoing.my-channel-out.connector=smallrye-amqp +mp.messaging.outgoing.my-channel-out.container-id=shared +mp.messaging.outgoing.my-channel-out.cbs.enabled=true +---- + +Both channels share a single connection and a single CBS token authorization. + +== Azure Service Bus Example + +To connect to Azure Service Bus using CBS with SAS tokens: + +[source,java] +---- +include::{examples-dir}/amqp/customization/MySasTokenProvider.java[tags=sas-provider,indent=0] +---- + +[source,properties] +---- +mp.messaging.incoming.my-channel.connector=smallrye-amqp +mp.messaging.incoming.my-channel.host=my-namespace.servicebus.windows.net +mp.messaging.incoming.my-channel.port=5671 +mp.messaging.incoming.my-channel.use-ssl=true +mp.messaging.incoming.my-channel.cbs.enabled=true +mp.messaging.incoming.my-channel.cbs.exchange=put-token +mp.messaging.incoming.my-channel.cbs.audience=my-namespace.servicebus.windows.net/my-queue +---- + +== Retry Configuration + +If token authorization fails, the connector retries with exponential backoff. +The retry behavior is controlled by the standard connector retry attributes: + +[source,properties] +---- +mp.messaging.incoming.my-channel.retry-on-fail-attempts=6 # max retries (default: 6) +mp.messaging.incoming.my-channel.retry-on-fail-interval=5 # retry delay in seconds (default: 5) +---- + +If all retries are exhausted, the connection is closed and re-established using the connector's reconnection mechanism. + +== Configuration Reference + +[width="100%",cols="17%,7%,24%,52%",options="header",] +|=== +|Attribute |Type |Default |Description +|`cbs.enabled` |boolean |`false` |Enable CBS authorization +|`cbs.exchange` |string |`set-token` |CBS exchange protocol: `set-token` or `put-token` +|`cbs.audience` |string | |Token audience (auto-derived from address if not set) +|`cbs.scopes` |string | |Token scopes for authorization +|`cbs.token-manager` |string |`default-cbs-token-manager` |CDI bean identifier of a custom `CbsTokenManager.Factory` +|=== + +All attributes are available for both incoming and outgoing channels. diff --git a/content/docs/smallrye-reactive-messaging/main/amqp/client-customization.adoc b/content/docs/smallrye-reactive-messaging/main/amqp/client-customization.adoc new file mode 100644 index 000000000..bc214acb8 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/amqp/client-customization.adoc @@ -0,0 +1,50 @@ += Customizing the underlying AMQP client +:version-root: ../ +include::{version-root}_attributes.adoc[] + +You can customize the underlying AMQP Client configuration by +_producing_ an instance of +https://vertx.io/docs/apidocs/io/vertx/amqp/AmqpClientOptions.html[`AmqpClientOptions`]: + +[source,java] +---- +include::{examples-dir}/amqp/customization/ClientProducers.java[tags=named,indent=0] +---- + +This instance is retrieved and used to configure the client used by the +connector. You need to indicate the name of the client using the +`client-options-name` attribute: + +[source,properties] +---- +mp.messaging.incoming.prices.client-options-name=my-named-options +---- + +If you experience frequent disconnections from the broker, the AmqpClientOptions can also be used to set a heartbeat if you need to keep the AMQP connection permanently. +Some brokers might terminate the AMQP connection after a certain idle timeout. +You can provide a heartbeat value which will be used by the Vert.x proton client to advertise the idle timeout when opening transport to a remote peer. + +[source,java] +---- +@Produces +@Identifier("my-named-options") +public AmqpClientOptions getNamedOptions() { + // set a heartbeat of 30s (in milliseconds) + return new AmqpClientOptions() + .setHeartbeat(30000); +} +---- + +== Client capabilities + +Both incoming and outgoing AMQP channels can be configured with a list +of capabilities to declare during sender and receiver connections with +the AMQP broker. Note that supported capability names are broker +specific. + +[source,properties] +---- +mp.messaging.incoming.sink.capabilities=temporary-topic +... +mp.messaging.outgoing.source.capabilities=shared +---- diff --git a/content/docs/smallrye-reactive-messaging/main/amqp/health.adoc b/content/docs/smallrye-reactive-messaging/main/amqp/health.adoc new file mode 100644 index 000000000..79ec6eb9f --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/amqp/health.adoc @@ -0,0 +1,34 @@ += Health reporting +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The AMQP connector reports the startup, liveness, and readiness of each +inbound (Receiving messages) and outbound (sending messages) channel +managed by the connector: + +* Startup :: For both inbound and outbound, the startup probe reports _OK_ when the +connection with the broker is established, and the AMQP senders and +receivers are opened (the links are attached to the broker). +* Liveness :: For both inbound and outbound, the liveness check verifies that the +connection is established. The check still returns _OK_ if the +connection got cut, but we are attempting a reconnection. +* Readiness :: For the inbound, it checks that the connection is established and the +receiver is opened. Unlike the liveness check, this probe reports _KO_ +until the connection is re-established. For the outbound, it checks that +the connection is established and the sender is opened. Unlike the +liveness check, this probe reports _KO_ until the connection is +re-established. + +[NOTE] +==== + +To disable health reporting, set the `health-enabled` attribute for the +channel to `false`. + +==== + +Note that a message processing failures _nacks_ the message, which is +then handled by the failure-strategy. It is the responsibility of the +`failure-strategy` to report the failure and influence the outcome of +the checks. The `fail` failure strategy reports the failure, and so the +check will report the fault. diff --git a/content/docs/smallrye-reactive-messaging/main/amqp/rabbitmq.adoc b/content/docs/smallrye-reactive-messaging/main/amqp/rabbitmq.adoc new file mode 100644 index 000000000..06631ded9 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/amqp/rabbitmq.adoc @@ -0,0 +1,61 @@ += Using RabbitMQ +:version-root: ../ +include::{version-root}_attributes.adoc[] + +RabbitMQ 4.0{plus} provides native AMQP 1.0 support, making it fully compatible with this connector. +For older versions (3.x), RabbitMQ implements AMQP 0.9.1 and requires the +https://github.com/rabbitmq/rabbitmq-amqp1.0/blob/v3.8.x/README.md[AMQP 1.0 plugin] to be enabled. + +== Address format + +RabbitMQ 4.0{plus} uses the v2 address format with `/queues/` and `/exchanges/` prefixes. +When using this connector with RabbitMQ, prefix your addresses accordingly: + +[source,properties] +---- +mp.messaging.incoming.prices.connector=smallrye-amqp +mp.messaging.incoming.prices.address=/queues/prices + +mp.messaging.outgoing.generated-price.connector=smallrye-amqp +mp.messaging.outgoing.generated-price.address=/queues/prices +mp.messaging.outgoing.generated-price.use-anonymous-sender=false +---- + +== Recommendations + +RabbitMQ support for AMQP 1.0 may differ from other AMQP 1.0 supporting brokers. +We recommend the following configurations. + +To receive messages from RabbitMQ: + +* Set `durable` to `false` + +[source,properties] +---- +mp.messaging.incoming.prices.connector=smallrye-amqp +mp.messaging.incoming.prices.durable=false +---- + +To send messages to RabbitMQ: + +* set the destination `address` (anonymous senders are not supported) +* set `use-anonymous-sender` to `false` + +[source,properties] +---- +mp.messaging.outgoing.generated-price.connector=smallrye-amqp +mp.messaging.outgoing.generated-price.address=prices +mp.messaging.outgoing.generated-price.use-anonymous-sender=false +---- + +It’s not possible to change the destination dynamically (using message +metadata) when using RabbitMQ. The connector automatically detects that +the broker does not support anonymous sender (See +http://docs.oasis-open.org/amqp/anonterm/v1.0/anonterm-v1.0.html). + +== Request/Reply with RabbitMQ + +This connector supports the request-reply pattern with RabbitMQ. +See xref:amqp/request-reply.adoc#using-with-rabbitmq[AMQP Request/Reply] for details. + +Alternatively, you can use the xref:rabbitmq/rabbitmq.adoc[RabbitMQ connector]. diff --git a/content/docs/smallrye-reactive-messaging/main/amqp/receiving-amqp-messages.adoc b/content/docs/smallrye-reactive-messaging/main/amqp/receiving-amqp-messages.adoc new file mode 100644 index 000000000..751056c73 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/amqp/receiving-amqp-messages.adoc @@ -0,0 +1,257 @@ += Receiving messages from AMQP +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The AMQP connector lets you retrieve messages from an https://www.amqp.org/product/architecture[AMQP broker or +router]. The AMQP connector +retrieves _AMQP Messages_ and maps each of them into Reactive Messaging +`Messages`. + +== Example + +Let’s imagine you have an AMQP broker (such as https://activemq.apache.org/components/artemis/[Apache ActiveMQ +Artemis]) running, and +accessible using the `amqp:5672` address (by default it would use +`localhost:5672`). Configure your application to receive AMQP Messages +on the `prices` channel as follows: + +[source,properties] +---- +amqp-host=amqp # <1> +amqp-port=5672 # <2> +amqp-username=my-username # <3> +amqp-password=my-password # <4> + +mp.messaging.incoming.prices.connector=smallrye-amqp # <5> +---- + +[arabic] +. Configures the broker/router host name. You can do it per channel +(using the `host` attribute) or globally using `amqp-host` +. Configures the broker/router port. You can do it per channel (using +the `port` attribute) or globally using `amqp-port`. The default is +`5672`. +. Configures the broker/router username if required. You can do it per +channel (using the `username` attribute) or globally using +`amqp-username`. +. Configures the broker/router password if required. You can do it per +channel (using the `password` attribute) or globally using +`amqp-password`. +. Instructs the `prices` channel to be managed by the AMQP connector + +[NOTE] +==== + +You don’t need to set the AMQP _address_. By default, it uses the +channel name (`prices`). You can configure the `address` attribute to +override it. + +==== + +Then, your application receives `Message++<++Double++>++`. You can consume the +payload directly: + +[source,java] +---- +include::{examples-dir}/amqp/inbound/AmqpPriceConsumer.java[indent=0] +---- + +Or, you can retrieve the `Message++<++Double++>++`: + +[source,java] +---- +include::{examples-dir}/amqp/inbound/AmqpPriceMessageConsumer.java[indent=0] +---- + +== Deserialization + +The connector converts incoming AMQP Messages into Reactive Messaging +`Message++<++T++>++` instances. `T` depends on the _body_ of the received AMQP +Message. + +The http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-types-v1.0-os.html[AMQP Type +System] +defines the supported types. + +[width="100%",cols="66%,34%",options="header",] +|=== +|AMQP Body Type |`++<++T++>++` +|AMQP Value containing a http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-types-v1.0-os.html#section-primitive-type-definitions[AMQP Primitive Type] |the corresponding Java type +|AMQP Value using the `Binary` type |`byte++[]++` +|AMQP Sequence |`List` +|AMQP Data (with binary content) and the `content-type` is set to `application/json` |https://vertx.io/docs/apidocs/io/vertx/core/json/JsonObject.html[`JsonObject`] +|AMQP Data with a different `content-type` |`byte++[]++` +|=== + +If you send objects with this AMQP connector (outbound connector), it +gets encoded as JSON and sent as binary. The `content-type` is set to +`application/json`. You can receive this payload using (Vert.x) JSON +Objects, and then map it to the object class you want: + +[source,java] +---- +@ApplicationScoped +public static class Generator { + + @Outgoing("to-amqp") + public Multi prices() { // <1> + AtomicInteger count = new AtomicInteger(); + return Multi.createFrom().ticks().every(Duration.ofMillis(1000)) + .map(l -> new Price().setPrice(count.incrementAndGet())) + .onOverflow().drop(); + } + +} + +@ApplicationScoped +public static class Consumer { + + List prices = new CopyOnWriteArrayList<>(); + + @Incoming("from-amqp") + public void consume(JsonObject p) { // <2> + Price price = p.mapTo(Price.class); // <3> + prices.add(price); + } + + public List list() { + return prices; + } +} +---- + +[arabic] +. The `Price` instances are automatically encoded to JSON by the +connector +. You can receive it using a `JsonObject` +. Then, you can reconstruct the instance using the `mapTo` method + +== Inbound Metadata + +Messages coming from AMQP contains an instance of link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-amqp/{javadoc-version}/io/smallrye/reactive/messaging/amqp/IncomingAmqpMetadata.html[`IncomingAmqpMetadata`] + +[source,java] +---- +include::{examples-dir}/amqp/inbound/AmqpMetadataExample.java[tags=code,indent=0] +---- + +== Acknowledgement + +When a Reactive Messaging `Message` associated with an AMQP Message is +acknowledged, it informs the broker that the message has been +_accepted_. + +== Failure Management + +If a message produced from an AMQP message is _nacked_, a failure +strategy is applied. The AMQP connector supports six strategies: + +* `fail` - fail the application; no more AMQP messages will be +processed (default). The AMQP message is marked as rejected. +* `accept` - this strategy marks the AMQP message as _accepted_. The +processing continues ignoring the failure. Refer to the http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-messaging-v1.0-os.html#type-accepted[accepted +delivery state +documentation]. +* `release` - this strategy marks the AMQP message as _released_. The +processing continues with the next message. The broker can redeliver +the message. Refer to the http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-messaging-v1.0-os.html#type-released[released delivery state +documentation]. +* `reject` - this strategy marks the AMQP message as rejected. The +processing continues with the next message. Refer to the http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-messaging-v1.0-os.html#type-rejected[rejected +delivery state +documentation]. +* `modified-failed` - this strategy marks the AMQP message as +_modified_ and indicates that it failed (with the `delivery-failed` +attribute). The processing continues with the next message, but the +broker may attempt to redeliver the message. Refer to the http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-messaging-v1.0-os.html#type-modified[modified +delivery state +documentation] +* `modified-failed-undeliverable-here` - this strategy marks the AMQP +message as _modified_ and indicates that it failed (with the +`delivery-failed` attribute). It also indicates that the application +cannot process the message, meaning that the broker will not attempt +to redeliver the message to this node. The processing continues with +the next message. Refer to the http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-messaging-v1.0-os.html#type-modified[modified delivery state +documentation] + +== Configuration Reference + +include::{generated-dir}/smallrye-amqp-incoming.adoc[indent=0] + +You can also pass any property supported by the https://vertx.io/docs/vertx-amqp-client/java/[Vert.x AMQP +client] as attribute. + +To use an existing _address_ or _queue_, you need to configure the +`address`, `container-id` and, optionally, the `link-name` attributes. +For example, if you have an Apache Artemis broker configured with: + +[source,xml] +---- + + +
people
+ true + artemis +
+
+---- + +You need the following configuration: + +[source,properties] +---- +mp.messaging.incoming.people.connector=smallrye-amqp +mp.messaging.incoming.people.durable=true +mp.messaging.incoming.people.address=people +mp.messaging.incoming.people.container-id=people +---- + +You may need to configure the `link-name` attribute, if the queue name +is not the channel name: + +[source,properties] +---- +mp.messaging.incoming.people-in.connector=smallrye-amqp +mp.messaging.incoming.people-in.durable=true +mp.messaging.incoming.people-in.address=people +mp.messaging.incoming.people-in.container-id=people +mp.messaging.incoming.people-in.link-name=people +---- + +== Receiving Cloud Events + +The AMQP connector supports https://cloudevents.io/[Cloud Events]. +When the connector detects a _structured_ or _binary_ Cloud Events, it +adds a link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/io/smallrye/reactive/messaging/ce/IncomingCloudEventMetadata.html[`IncomingCloudEventMetadata`] +into the metadata of the `Message`. `IncomingCloudEventMetadata` +contains accessors to the mandatory and optional Cloud Event attributes. + +If the connector cannot extract the Cloud Event metadata, it sends the +Message without the metadata. + +=== Binary Cloud Events + +For `binary` Cloud Events, *all* mandatory Cloud Event attributes must +be set in the AMQP application properties, prefixed by `cloudEvents:` +(as mandated by the https://github.com/cloudevents/spec/blob/v1.0.1/amqp-protocol-binding.md[protocol +binding]). +The connector considers headers starting with the `cloudEvents:` prefix +but not listed in the specification as extensions. You can access them +using the `getExtension` method from `IncomingCloudEventMetadata`. + +The `datacontenttype` attribute is mapped to the `content-type` header +of the record. + +=== Structured Cloud Events + +For `structured` Cloud Events, the event is encoded in the record’s +value. Only JSON is supported, so your event must be encoded as JSON in +the record’s value. + +Structured Cloud Event must set the `content-type` header of the record +to `application/cloudevents{plus}json; charset=UTF-8`. The message body must +be a valid JSON object containing at least all the mandatory Cloud +Events attributes. + +If the record is a structured Cloud Event, the created Message’s payload +is the Cloud Event `data`. diff --git a/content/docs/smallrye-reactive-messaging/main/amqp/request-reply.adoc b/content/docs/smallrye-reactive-messaging/main/amqp/request-reply.adoc new file mode 100644 index 000000000..4b7bbaad1 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/amqp/request-reply.adoc @@ -0,0 +1,202 @@ += AMQP Request/Reply +:version-root: ../ +include::{version-root}_attributes.adoc[] + +[WARNING] +.Experimental +==== + +AMQP Request Reply Emitter is an experimental feature. + +==== + +The AMQP https://www.enterpriseintegrationpatterns.com/patterns/messaging/RequestReply.html[Request-Reply] pattern allows you to publish a message to an AMQP address and then await for a reply message that responds to the initial request. + +The `AmqpRequestReply` emitter implements the requestor (or the client) of the request-reply pattern for AMQP 1.0 outbound channels: + +[source,java] +---- +include::{examples-dir}/amqp/outbound/AmqpRequestReplyEmitter.java[indent=0] +---- + +The `request` method publishes the request message to the configured target address of the outgoing channel, +and listens on a reply address (by default, the channel name with `-reply` suffix) for a reply message. +When the reply is received the returned `Uni` is completed with the message payload. + +The request send operation generates a correlation id and sets the AMQP `message-id` property, +which it expects to be sent back in the reply message's `correlation-id` property. + +The replier (or the server) can be implemented using a Reactive Messaging processor: + +[source,java] +---- +include::{examples-dir}/amqp/outbound/AmqpReplier.java[indent=0] +---- + +When you need more control over the reply message, such as setting the reply-to address and correlation id explicitly, you can use the `Message` type: + +[source,java] +---- +include::{examples-dir}/amqp/outbound/AmqpReplierWithMetadata.java[indent=0] +---- + +Given the following configuration example: + +[source,properties] +---- +mp.messaging.outgoing.my-request.connector=smallrye-amqp +mp.messaging.outgoing.my-request.address=requests +mp.messaging.outgoing.my-request.reply.address=my-request-reply + +mp.messaging.incoming.request.connector=smallrye-amqp +mp.messaging.incoming.request.address=requests + +mp.messaging.outgoing.reply.connector=smallrye-amqp +mp.messaging.outgoing.reply.address=my-request-reply +mp.messaging.outgoing.reply.use-anonymous-sender=false +---- + +== Requesting with `Message` types + +Like the core Emitter's `send` methods, `request` method also can receive a `Message` type and return a message: + +[source,java] +---- +include::{examples-dir}/amqp/outbound/AmqpRequestReplyMessageEmitter.java[indent=0] +---- + +[NOTE] +==== + +The ingested reply type of the `AmqpRequestReply` is discovered at runtime, +in order to configure a `MessageConverter` to be applied on the incoming message before returning the `Uni` result. + +==== + +== Requesting multiple replies + +You can use the `requestMulti` method to expect any number of replies represented by the `Multi` return type. + +For example this can be used to aggregate multiple replies to a single request. + +[source,java] +---- +include::{examples-dir}/amqp/outbound/AmqpRequestReplyMultiEmitter.java[indent=0] +---- + +Like the other `request` you can also request `Message` types. + +[NOTE] +==== + +The channel attribute `reply.timeout` will be applied between each message, if reached the returned `Multi` will +fail. + +==== + +== Pending replies and reply timeout + +By default, the `Uni` returned from the `request` method is configured to fail with timeout exception if no reply is received after 5 seconds. +This timeout is configurable with the channel attribute `reply.timeout` (in milliseconds). + +[source,properties] +---- +mp.messaging.outgoing.my-request.reply.timeout=10000 +---- + +A snapshot of the list of pending replies is available through the `AmqpRequestReply#getPendingReplies` method. + +== Scaling Request/Reply + +If multiple requestor instances are configured on the same outgoing address, and the same reply address, +each requestor instance will receive replies of all instances. If an observed correlation id doesn't match +the id of any pending replies, the reply is simply discarded. +With the additional network traffic this allows scaling requestors, (and repliers) dynamically. + +== Correlation Ids + +The AMQP Request/Reply allows configuring the correlation id mechanism completely through a `CorrelationIdHandler` implementation. +The default handler is based on randomly generated UUID strings, set as the AMQP `message-id` on the request message. +The reply message is expected to carry the same value in the `correlation-id` property. + +The correlation id handler implementation can be configured using the `reply.correlation-id.handler` attribute. +The default configuration is `uuid`, which uses randomly generated UUID strings as correlation ids. +An alternative `bytes` implementation can be used to generate random binary correlation ids. +The `bytes` id handler generates 12 random bytes, +but the length can be configured with the `smallrye.amqp.request-reply.correlation-id.bytes.length` attribute. + +Custom handlers can be implemented by proposing a CDI-managed bean with `@Identifier` qualifier. + +== Reply Error Handling + +If the reply server produces an error, it can propagate the error back to the requestor, failing the returned `Uni`. + +If configured using the `reply.failure.handler` channel attribute, +the `ReplyFailureHandler` implementations are discovered through CDI, matching the `@Identifier` qualifier. + +A sample reply error handler can lookup application properties and return the error to be thrown by the reply: + +[source,java] +---- +include::{examples-dir}/amqp/outbound/MyAmqpReplyFailureHandler.java[indent=0] +---- + +`null` return value indicates that no error has been found in the reply message, and it can be delivered to the application. + +== Connection Sharing + +Multiple AMQP channels can share the same underlying connection when configured with the same `container-id`. +This reduces resource consumption and is particularly useful for request-reply patterns +where the sender and reply receiver can share a single connection. + +[source,properties] +---- +mp.messaging.outgoing.my-request.connector=smallrye-amqp +mp.messaging.outgoing.my-request.address=requests +mp.messaging.outgoing.my-request.container-id=my-connection +mp.messaging.outgoing.my-request.reply.address=replies + +mp.messaging.incoming.other-channel.connector=smallrye-amqp +mp.messaging.incoming.other-channel.address=other +mp.messaging.incoming.other-channel.container-id=my-connection +---- + +Both channels above will share the same AMQP connection because they use the same `container-id`. + +[NOTE] +==== + +Connection sharing requires all channels with the same `container-id` to have compatible connection settings (host, port, credentials, etc.). +If the settings differ, the connector will detect the conflict and raise an error. + +==== + +== Using with RabbitMQ + +RabbitMQ 4.0{plus} with native AMQP 1.0 support is compatible with the request-reply pattern. +When using RabbitMQ, remember to use `/queues/` prefixed addresses (v2 address format) +and set `use-anonymous-sender=false` as anonymous senders are not supported. + +[source,properties] +---- +mp.messaging.outgoing.my-request.connector=smallrye-amqp +mp.messaging.outgoing.my-request.address=/queues/requests +mp.messaging.outgoing.my-request.reply.address=/queues/replies +mp.messaging.outgoing.my-request.use-anonymous-sender=false + +mp.messaging.incoming.request.connector=smallrye-amqp +mp.messaging.incoming.request.address=/queues/requests + +mp.messaging.outgoing.reply.connector=smallrye-amqp +mp.messaging.outgoing.reply.address=/queues/replies +mp.messaging.outgoing.reply.use-anonymous-sender=false +---- + +See xref:amqp/rabbitmq.adoc[Using RabbitMQ] for more details on RabbitMQ-specific configuration. + +== Limitations + +The following features are not yet supported: + +* *Direct reply-to* (`amq.rabbitmq.reply-to`): RabbitMQ's direct reply-to mechanism requires link pairing, which is not yet implemented. +* *Link pairing*: Pairing sender and receiver links on the same connection for request-reply routing is not yet available. diff --git a/content/docs/smallrye-reactive-messaging/main/amqp/sending-amqp-messages.adoc b/content/docs/smallrye-reactive-messaging/main/amqp/sending-amqp-messages.adoc new file mode 100644 index 000000000..f89a2a0b6 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/amqp/sending-amqp-messages.adoc @@ -0,0 +1,254 @@ += Sending messages to AMQP +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The AMQP connector can write Reactive Messaging `Messages` as AMQP +Messages. + +== Example + +Let’s imagine you have an AMQP broker (such as https://activemq.apache.org/components/artemis/[Apache ActiveMQ +Artemis]) running, and +accessible using the `amqp:5672` address (by default it would use +`localhost:5672`). Configure your application to send the messages from +the `prices` channel as AMQP Message as follows: + +[source,properties] +---- +amqp-host=amqp # <1> +amqp-port=5672 # <2> +amqp-username=my-username # <3> +amqp-password=my-password # <4> + +mp.messaging.outgoing.prices.connector=smallrye-amqp # <5> +---- + +[arabic] +. Configures the broker/router host name. You can do it per channel +(using the `host` attribute) or globally using `amqp-host` +. Configures the broker/router port. You can do it per channel (using +the `port` attribute) or globally using `amqp-port`. The default is +`5672`. +. Configures the broker/router username if required. You can do it per +channel (using the `username` attribute) or globally using +`amqp-username`. +. Configures the broker/router password if required. You can do it per +channel (using the `password` attribute) or globally using +`amqp-password`. +. Instructs the `prices` channel to be managed by the AMQP connector + +[NOTE] +==== + +You don’t need to set the _address_. By default, it uses the channel +name (`prices`). You can configure the `address` attribute to override +it. + +==== + +Then, your application must send `Message++<++Double++>++` to the `prices` +channel. It can use `double` payloads as in the following snippet: + +[source,java] +---- +include::{examples-dir}/amqp/outbound/AmqpPriceProducer.java[indent=0] +---- + +Or, you can send `Message++<++Double++>++`: + +[source,java] +---- +include::{examples-dir}/amqp/outbound/AmqpPriceMessageProducer.java[indent=0] +---- + +== Serialization + +When receiving a `Message++<++T++>++`, the connector convert the message into an +AMQP Message. The payload is converted to the AMQP Message _body_. + +[width="100%",cols="50%,50%",options="header",] +|=== +|`T` |AMQP Message Body +|primitive types or `String` |AMQP Value with the payload +|`Instant` or `UUID` |AMQP Value using the corresponding AMQP Type +|https://vertx.io/docs/apidocs/io/vertx/core/json/JsonObject.html[`JsonObject`] or https://vertx.io/docs/apidocs/io/vertx/core/json/JsonArray.html[`JsonArray`] |AMQP Data using a binary content. The `content-type` is set to `application/json` +|`io.vertx.mutiny.core.buffer.Buffer` |AMQP Data using a binary content. No `content-type` set +|Any other class |The payload is converted to JSON (using a Json Mapper). The result is wrapped into AMQP Data using a *binary* content. The `content-type` is set to `application/json` +|=== + +If the message payload cannot be serialized to JSON, the message is +_nacked_. + +== Outbound Metadata + +When sending `Messages`, you can add an instance of +link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-amqp/{javadoc-version}/io/smallrye/reactive/messaging/amqp/OutgoingAmqpMetadata.html[`OutgoingAmqpMetadata`] +to influence how the message is going to be sent to AMQP. For example, you +can configure the subjects, properties: + +[source,java] +---- +include::{examples-dir}/amqp/outbound/AmqpOutboundMetadataExample.java[tags=code,indent=0] +---- + +== Dynamic address names + +Sometimes it is desirable to select the destination of a message +dynamically. In this case, you should not configure the address inside +your application configuration file, but instead, use the outbound +metadata to set the address. + +For example, you can send to a dynamic address based on the incoming +message: + +[source,java] +---- +include::{examples-dir}/amqp/outbound/AmqpOutboundDynamicAddressExample.java[tags=code,indent=0] +---- + +[NOTE] +==== + +To be able to set the address per message, the connector is using an +_anonymous sender_. + +==== + +== Acknowledgement + +By default, the Reactive Messaging `Message` is acknowledged when the +broker acknowledged the message. When using routers, this +acknowledgement may not be enabled. In this case, configure the +`auto-acknowledgement` attribute to acknowledge the message as soon as +it has been sent to the router. + +If an AMQP message is rejected/released/modified by the broker (or +cannot be sent successfully), the message is nacked. + +== Back Pressure and Credits + +The back-pressure is handled by the `max-inflight-messages` attribute and AMQP _credits_. +The outbound connector requests messages minimum between `max-inflight-messages` and credits allowed by the broker. +When the amount of credits reaches 0, it waits (in a non-blocking fashion) until the broker grants more credits to the AMQP sender. + +When `max-inflight-messages` is set to 0, only AMQP credits apply to limit the requests. + +Note that if an AMQP message send fails, it is retried until `retry-on-fail-attempts` is reached. +If the client reconnects to the broker during the retry, failing messages are sent again but the message order is not preserved. + +To preserve the message order in this case you can set `max-inflight-messages` to + +== Configuration Reference + +include::{generated-dir}/smallrye-amqp-outgoing.adoc[indent=0] + +You can also pass any property supported by the https://vertx.io/docs/vertx-amqp-client/java/[Vert.x AMQP +client] as attribute. + +== Using existing destinations + +To use an existing _address_ or _queue_, you need to configure the +`address`, `container-id` and, optionally, the `link-name` attributes. +For example, if you have an Apache Artemis broker configured with: + +[source,xml] +---- + + +
people
+ true + artemis +
+
+---- + +You need the following configuration: + +[source,properties] +---- +mp.messaging.outgoing.people.connector=smallrye-amqp +mp.messaging.outgoing.people.durable=true +mp.messaging.outgoing.people.address=people +mp.messaging.outgoing.people.container-id=people +---- + +You may need to configure the `link-name` attribute, if the queue name +is not the channel name: + +[source,properties] +---- +mp.messaging.outgoing.people-out.connector=smallrye-amqp +mp.messaging.outgoing.people-out.durable=true +mp.messaging.outgoing.people-out.address=people +mp.messaging.outgoing.people-out.container-id=people +mp.messaging.outgoing.people-out.link-name=people +---- + +To use a `MULTICAST` queue, you need to provide the _FQQN_ +(Fully-qualified queue name) instead of just the name of the queue: + +[source,properties] +---- +mp.messaging.outgoing.people-out.connector=smallrye-amqp +mp.messaging.outgoing.people-out.durable=true +mp.messaging.outgoing.people-out.address=foo +mp.messaging.outgoing.people-out.container-id=foo + +mp.messaging.incoming.people-out.connector=smallrye-amqp +mp.messaging.incoming.people-out.durable=true +mp.messaging.incoming.people-out.address=foo::bar # Note the syntax: address-name::queue-name +mp.messaging.incoming.people-out.container-id=bar +mp.messaging.incoming.people-out.link-name=people +---- + +More details about the AMQP Address model can be found on https://activemq.apache.org/components/artemis/documentation/2.0.0/address-model.html[the Artemis +documentation]. + +== Sending Cloud Events + +The AMQP connector supports https://cloudevents.io/[Cloud Events]. The +connector sends the outbound record as Cloud Events if: + +* the message metadata contains an +`io.smallrye.reactive.messaging.ce.OutgoingCloudEventMetadata` +instance, +* the channel configuration defines the `cloud-events-type` and +`cloud-events-source` attributes. + +You can create +`io.smallrye.reactive.messaging.ce.OutgoingCloudEventMetadata` instances +using: + +[source,java] +---- +include::{examples-dir}/amqp/outbound/AmqpCloudEventProcessor.java[indent=0] +---- + +If the metadata does not contain an id, the connector generates one +(random UUID). The `type` and `source` can be configured per message or +at the channel level using the `cloud-events-type` and +`cloud-events-source` attributes. Other attributes are also +configurable. + +The metadata can be contributed by multiple methods, however, you must +always retrieve the already existing metadata to avoid overriding the +values: + +[source,java] +---- +include::{examples-dir}/amqp/outbound/AmqpCloudEventMultipleProcessors.java[indent=0] +---- + +By default, the connector sends the Cloud Events using the _binary_ +format. You can write _structured_ Cloud Events by setting the +`cloud-events-mode` to `structured`. Only JSON is supported, so the +created records had its `content-type` header set to +`application/cloudevents{plus}json; charset=UTF-8` + +[NOTE] +==== + +you can disable the Cloud Event support by setting the `cloud-events` +attribute to `false` + +==== diff --git a/content/docs/smallrye-reactive-messaging/main/camel/camel-processor.adoc b/content/docs/smallrye-reactive-messaging/main/camel/camel-processor.adoc new file mode 100644 index 000000000..20327120f --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/camel/camel-processor.adoc @@ -0,0 +1,37 @@ += The processor pattern using Camel +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Using the processor pattern, you can consume on a channel using a Camel +component, and produce on a channel using another Camel component. In +that case, the headers present in the incoming metadata will be +forwarded in the outgoing metadata. + +== Example + +Let’s imagine you want to read messages from a Nats subject, process it +and produce a message on a Kafka topic. + +[source,properties] +---- +mp.messaging.incoming.mynatssubject.connector=smallrye-camel # <1> +mp.messaging.incoming.mynatssubject.endpoint-uri=nats:mynatssubject # <2> +mp.messaging.outgoing.mykafkatopic.connector=smallrye-camel # <3> +mp.messaging.outgoing.mykafkatopic.endpoint-uri=kafka:mykafkatopic# <4> + +camel.component.nats.servers=127.0.0.1:5555 # <5> +camel.component.kafka.brokers=127.0.0.1:9092 # <6> +---- + +[arabic] +. Sets the connector for the `mynatssubject` channel +. Configures the `endpoint-uri` for nats subject +. Sets the connector for the `mykafkatopic` channel +. Configures the `endpoint-uri` for the kafka topic +. Sets the URL of the nats server to use +. Sets the URL of a kafka broker to use + +[source,java] +---- +include::{examples-dir}/camel/processor/CamelProcessor.java[indent=0] +---- diff --git a/content/docs/smallrye-reactive-messaging/main/camel/camel.adoc b/content/docs/smallrye-reactive-messaging/main/camel/camel.adoc new file mode 100644 index 000000000..00eb2828e --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/camel/camel.adoc @@ -0,0 +1,75 @@ += Apache Camel Connector +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The Camel connector adds support for Apache Camel to Reactive Messaging. + +https://camel.apache.org/[Camel] is an open source integration +framework let you integrate various systems consuming or producing data. +Camel implements the Enterprise Integration Patterns and provides +several hundred of components used to access databases, message queues, +APIs or basically https://camel.apache.org/components/latest/[anything under the +sun]. + +[IMPORTANT] +==== + +Smallrye Reactive Messaging 4.x moved from `javax` to `jakarta` APIs therefore only supports Camel 4 releases. +Similarly, this connector drops the Java 11 support as this is the case for Camel 4. + +==== + +== Introduction + +Camel is not a messaging broker. But, it allows your Reactive Messaging +application to retrieve data from almost anything and send data to +almost anything. + +So if you want to send Reactive Messaging `Message` to Telegram or +retrieve data from Salesforce or SAP, this is the connector you need. + +One of the Camel cornerstone is the `endpoint` and its `uri` encoding +the connection to an external system. For example, +`file:orders/?delete=true&charset=utf-8` instructs Camel to read the +files from the `orders` directory. URI format and parameters are listed +on the component documentation, such as the https://camel.apache.org/components/latest/file-component.html[File +component]. + +== Using the camel connector + +To you the camel Connector, add the following dependency to your +project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.reactive + smallrye-reactive-messaging-camel + {project-version} + +---- + +You will also need the dependency of the Camel component you are using. +For example, if you want to process files, you would need to add the +Camel File Component artifact: + +[source,xml,subs="attributes+"] +---- + + org.apache.camel + camel-file + {camel-version} + +---- + +The connector name is: `smallrye-camel`. + +So, to indicate that a channel is managed by this connector you need: + +.... +# Inbound +mp.messaging.incoming.[channel-name].connector=smallrye-camel + +# Outbound +mp.messaging.outgoing.[channel-name].connector=smallrye-camel +.... diff --git a/content/docs/smallrye-reactive-messaging/main/camel/receiving-messages-from-camel.adoc b/content/docs/smallrye-reactive-messaging/main/camel/receiving-messages-from-camel.adoc new file mode 100644 index 000000000..65338aedc --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/camel/receiving-messages-from-camel.adoc @@ -0,0 +1,89 @@ += Retrieving data using Camel +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Camel provides many components. To keep this documentation focused on +the integration with Camel, we use the https://camel.apache.org/components/latest/file-component.html[File +component]. +This component let use read files from a directory. So the connector +configured with this component creates a `Message` for each file located +in the directory. As soon as a file is dropped in the directory, a new +`Message` is created. + +== Example + +Let’s imagine you want to read the files from the `orders` directory and +send them to the `files` channel. Configuring the Camel connector to +gets the _file_ from this directory only requires 2 properties: + +[source,properties] +---- +mp.messaging.incoming.files.connector=smallrye-camel # <1> +mp.messaging.incoming.files.endpoint-uri=file:orders/?delete=true&charset=utf-8 # <2> +---- + +[arabic] +. Sets the connector for the `files` channel +. Configures the `endpoint-uri` + +Then, your application receives `Message++<++GenericFile++<++File++>>++`. + +[NOTE] +==== + +The Camel File component produces +`org.apache.camel.component.file.GenericFile` instances. You can +retrieve the actual `File` using `getFile()`. + +==== + +You can consume the payload directly: + +[source,java] +---- +include::{examples-dir}/camel/inbound/CamelFileConsumer.java[indent=0] +---- + +You can also retrieve the `Message++<++GenericFile++<++File++>>++`: + +[source,java] +---- +include::{examples-dir}/camel/inbound/CamelFileMessageConsumer.java[indent=0] +---- + +== Deserialization + +Each Camel component is producing specific objects. As we have seen, the +File component produces `GenericFile`. + +Refer to the component documentation to check which type is produced. + +== Inbound Metadata + +Messages coming from Camel contains an instance of +link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-camel/{javadoc-version}/io/smallrye/reactive/messaging/camel/IncomingExchangeMetadata.html[`IncomingExchangeMetadata`] +in the metadata. + +[source,java] +---- +include::{examples-dir}/camel/inbound/IncomingCamelMetadataExample.java[indent=0] +---- + +This object lets you retrieve the Camel `Exchange`. + +== Failure Management + +If a message produced from a Camel exchange is _nacked_, a failure +strategy is applied. The Camel connector supports 3 strategies: + +* `fail` - fail the application, no more MQTT messages will be +processed. (default) The offset of the record that has not been +processed correctly is not committed. +* `ignore` - the failure is logged, but the processing continue. + +In both cases, the `exchange` is marked as rollback only and the nack +reason is attached to the exchange. + +== Configuration Reference + +include::{generated-dir}/smallrye-camel-incoming.adoc[indent=0] diff --git a/content/docs/smallrye-reactive-messaging/main/camel/sending-messages-to-camel.adoc b/content/docs/smallrye-reactive-messaging/main/camel/sending-messages-to-camel.adoc new file mode 100644 index 000000000..8bf614150 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/camel/sending-messages-to-camel.adoc @@ -0,0 +1,76 @@ += Sending data with Camel +:version-root: ../ +include::{version-root}_attributes.adoc[] + +You can use the Camel connector to send data to almost any type of +system. + +To keep this document focused on the Camel connector, we use the Camel +File component. However, the connector can be used with any Camel +component. + +== Example + +Let’s imagine you want to write generated prices into files. Configure +your application to write the messages from the `prices` channel into a +files as follows: + +[source,properties] +---- +mp.messaging.outgoing.prices.connector=smallrye-camel # <1> +mp.messaging.outgoing.prices.endpoint-uri=file:prices/?fileName=${date:now:yyyyMMddssSS}.txt&charset=utf-8 # <2> +---- + +[arabic] +. Sets the connector for the `prices` channel +. Configure the `endpoint-uri` to write into files in the `prices` +directory + +[IMPORTANT] +==== + +Depending on your implementation of MicroProfile Reactive Messaging, the +`$` may need to be escaped as follows: `$$++{++...}` + +==== + +Then, your application must send `Message++<++String++>++` to the `prices` +channel. It can use `String` payloads as in the following snippet: + +[source,java] +---- +include::{examples-dir}/camel/outbound/CamelPriceProducer.java[indent=0] +---- + +Or, you can send `Message++<++Double++>++`: + +[source,java] +---- +include::{examples-dir}/camel/outbound/CamelPriceMessageProducer.java[indent=0] +---- + +== Serialization + +The serialization is handled by the Camel component. Refer to the Camel +documentation to check which payload type is supported by the component. + +== Outbound Metadata + +When sending `Messages`, you can add an instance +of link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-camel/{javadoc-version}/io/smallrye/reactive/messaging/camel/OutgoingExchangeMetadata.html[`OutgoingExchangeMetadata`] +to the message metadata. You can then influence how the outbound Camel +`Exchange` is created, for example by adding properties: + +[source,java] +---- +include::{examples-dir}/camel/outbound/CamelOutboundMetadataExample.java[tags=code,indent=0] +---- + +== Acknowledgement + +The incoming messages are acknowledged when the underlying Camel +exchange completes. If the exchange fails, the message is nacked. + +== Configuration Reference + +include::{generated-dir}/smallrye-camel-outgoing.adoc[indent=0] diff --git a/content/docs/smallrye-reactive-messaging/main/camel/using-existing-routes.adoc b/content/docs/smallrye-reactive-messaging/main/camel/using-existing-routes.adoc new file mode 100644 index 000000000..ed8b2cfd7 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/camel/using-existing-routes.adoc @@ -0,0 +1,67 @@ += Using the Camel API +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The Camel connector is based on the https://camel.apache.org/components/latest/reactive-streams-component.html[Reactive Streams +support] +from Camel. If you have an application already using the Camel API +(routes, `from`...), you can integrate it with Reactive Messaging. + +== Getting the CamelReactiveStreamsService + +Once you add the Camel connector to your application, you can retrieve +the +`org.apache.camel.component.reactive.streams.api.CamelReactiveStreamsService` +object: + +[source,java] +---- +include::{examples-dir}/camel/api/CamelApi.java[tags=reactive,indent=0] +---- + +This `CamelReactiveStreamsService` lets you create `Publisher` and +`Subscriber` instances from existing routes. + +== Using Camel Route with @Outgoing + +If you have an existing Camel route, you can transform it as a +`Publisher` using the `CamelReactiveStreamsService`. Then, you can +return this `Publisher` from a method annotated with `@Outgoing`: + +[source,java] +---- +include::{examples-dir}/camel/api/CamelApi.java[tags=source,indent=0] +---- + +You can also expose a `RouteBuilder` bean, making sure to use the `Singleton` scope, +as `RouteBuilder` is no longer proxyable: + +[source,java] +---- +include::{examples-dir}/camel/api/CamelApi.java[tags=source-route-builder,indent=0] +---- + +Alternatively you can use the `LambdaRouteBuilder`: + +[source,java] +---- +include::{examples-dir}/camel/api/CamelApi.java[tags=source-lambda-route-builder,indent=0] +---- + +Using Camel Route with @Incoming + +If you have an existing Camel route, you can transform it as a +`Subscriber` using the `CamelReactiveStreamsService`. Then, you can +return this `Subscriber` from a method annotated with `@Incoming`: + +[source,java] +---- +include::{examples-dir}/camel/api/CamelApi.java[tags=sink,indent=0] +---- + +You can also use a producer: + +[source,java] +---- +include::{examples-dir}/camel/api/CamelApi.java[tags=producer,indent=0] +---- diff --git a/content/docs/smallrye-reactive-messaging/main/concepts/acknowledgement.adoc b/content/docs/smallrye-reactive-messaging/main/concepts/acknowledgement.adoc new file mode 100644 index 000000000..782ec0e81 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/concepts/acknowledgement.adoc @@ -0,0 +1,183 @@ += Acknowledgement +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Acknowledgment is an essential concept in messaging. A message is +acknowledged when its processing or reception has been successful. It +allows the broker to move to the next message. + +How acknowledgment is used, and the exact behavior in terms of retry and +resilience depends on the broker. For example, for Kafka, it would +commit the offset. For AMQP, it would inform the broker that the message +has been _accepted_. + +Reactive Messaging supports acknowledgement. The default acknowledgement +depends on the method signature. Also, the acknowledgement policy can be +configured using the `@Acknowledgement` annotation. + +== Chain of acknowledgment + +If we reuse this example: + +[source,java] +---- +include::{examples-dir}/beans/Chain.java[tags=chain,indent=0] +---- + +The framework automatically acknowledges the message received from the +`sink` channel when the `consume` method returns. As a consequence, the +message received by the `process` method is acknowledged, and so on. In +other words, it creates a chain of acknowledgement - from the outbound +channel to the inbound channel. + +When using connectors to receive and consume messages, the outbound +connector acknowledges the messages when they are dispatched +successfully to the broker. The acknowledgment chain would, as a result, +acknowledges the inbound connector, which would be able to send an +acknowledgment to the broker. + +This chain of acknowledgment is automatically implemented when +processing payloads. + +== Acknowledgment when using Messages + +When using `Messages`, the user controls the acknowledgment, and so the +chain is not formed automatically. It gives you more flexibility about +when and how the incoming messages are acknowledged. + +If you create a `Message` using the `with` method, is copy the +acknowledgment function from the incoming message: + +[source,java] +---- +include::{examples-dir}/ack/MessageAckExamples.java[tags=with-payload,indent=0] +---- + +To have more control over the acknowledgment, you can create a brand new +`Message` and pass the acknowledgment function: + +[source,java] +---- +include::{examples-dir}/ack/MessageAckExamples.java[tags=message-creation,indent=0] +---- + +However, you may need to create the acknowledgment chain, to acknowledge +the incoming message: + +[source,java] +---- +include::{examples-dir}/ack/MessageAckExamples.java[tags=process,indent=0] +---- + +To trigger the acknowledgment of the incoming message, use the `ack()` +method. It returns a `CompletionStage`, receiving `null` as value when +the acknowledgment has completed. + +== Acknowledgment when using streams + +When transforming streams of `Message`, the acknowledgment is delegated +to the user. It means that it’s up to the user to acknowledge the +incoming messages: + +[source,java] +---- +include::{examples-dir}/ack/StreamAckExamples.java[tags=message,indent=0] +---- + +In the previous example, we only generate a single message per incoming +message so that we can use the `with` method. It becomes more +sophisticated when grouping incoming messages or when each incoming +message produces multiple messages. + +In the case of a stream of payloads, the default strategy acknowledges +the incoming messages before being processed by the method (regardless +of the outcome). + +[source,java] +---- +include::{examples-dir}/ack/StreamAckExamples.java[tags=payload,indent=0] +---- + +For method receiving a single payload and producing a stream of payloads, it defaults to pre-processing acknowledgement. +However, in this case, post-processing is supported. +It waits for all the produced messages to be acknowledged before acknowledging the received one. +If one of the produced message is nacked, the received one is nacked immediately. + +[source,java] +---- +include::{examples-dir}/ack/StreamAckExamples.java[tags=payload-to-multi,indent=0] +---- + +== Controlling acknowledgement + +The link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/org/eclipse/microprofile/reactive/messaging/Acknowledgment.html[`Acknowledgment`] +annotation lets you customize the default strategy presented in the +previous sections. The `@Acknowledgement` annotation takes a _strategy_ +as parameter. Reactive Messaging proposed 4 strategies: + +* `POST++_++PROCESSING` - the acknowledgement of the incoming message is +executed once the produced message is acknowledged. +* `PRE++_++PROCESSING` - the acknowledgement of the incoming message is +executed before the message is processed by the method. +* `MANUAL` - the acknowledgement is done by the user. +* `NONE` - No acknowledgment is performed, neither manually or +automatically. + +It is recommended to use `POST++_++PROCESSING` as it guarantees that the +full processing has completed before acknowledging the incoming message. +However, sometimes it’s not possible, and this strategy is not available +if you manipulate streams of `Messages` or payloads. + +The `PRE++_++PROCESSING` strategy can be useful to acknowledge a message +early in the process: + +[source,java] +---- +include::{examples-dir}/ack/PreAckExamples.java[tags=pre,indent=0] +---- + +It cuts the acknowledgment chain, meaning that the rest of the +processing is not linked to the incoming message anymore. This strategy +is the default strategy when manipulating streams of payloads. + +Refer to the xref:concepts/signatures.adoc[signature list] to determine +which strategies are available for a specific method signature and +what’s the default strategy. + +== Negative acknowledgement + +Messages can also be _nacked_, which indicates that the message was not +processed correctly. The `Message.nack` method indicates failing +processing (and supply the reason), and, as for successful +acknowledgment, the _nack_ is propagated through the chain of messages. + +If the message has been produced by a connector, this connector +implements specific behavior when receiving a _nack_. It can fail +(default), or ignore the failing, or implement a dead-letter queue +mechanism. Refer to the connector documentation for further details +about the available strategies. + +If the message is sent by an emitter using the `send(P)` method, the +returned `CompletionStage` is completed _exceptionally_ with the _nack_ +reason. + +[source,java] +---- +include::{examples-dir}/ack/NackOnEmitter.java[tags=emitter,indent=0] +---- + +Negative acknowledgment can be manual or automatic. If your method +handles instances of `Message` and the acknowledgment strategy is +`MANUAL`, you can nack a message explicitly. You must indicate the +reason (an exception) when calling the `nack` method. As for successful +acknowledgment, the `nack` returns a `CompletionStage` completed when +the `nack` has been processed. + +If your method uses the `POST++_++PROCESSING` acknowledgment strategy, and +the method fails (either by throwing an exception or by producing a +failure), the message is automatically nacked with the caught exception: + +[source,java] +---- +include::{examples-dir}/ack/AutoNack.java[tags=auto-nack,indent=0] +---- diff --git a/content/docs/smallrye-reactive-messaging/main/concepts/advanced-config.adoc b/content/docs/smallrye-reactive-messaging/main/concepts/advanced-config.adoc new file mode 100644 index 000000000..89aa2cd08 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/concepts/advanced-config.adoc @@ -0,0 +1,43 @@ += Advanced configuration +:version-root: ../ +include::{version-root}_attributes.adoc[] + +== Strict Binding Mode + +By default, SmallRye Reactive Messaging does not enforce whether all +_mediators_ are connected. It just prints a warning message. The strict +mode fails the deployment if some "incomings" are not bound to +"outgoings". To enable this mode, you can pass the +`-Dsmallrye-messaging-strict-binding=true` via the command line, or you +can set the `smallrye-messaging-strict-binding` attribute to `true` in +the configuration: + +[source,text] +---- +smallrye-messaging-strict-binding=true +---- + +== Disabling channels + +You can disable a channel in the configuration by setting the `enabled` +attribute to `false`: + +[source,text] +---- +mp.messaging.outgoing.dummy-sink.connector=dummy +mp.messaging.outgoing.dummy-sink.enabled=false # Disable this channel +---- + +SmallRye Reactive Messaging does not register disabled channels, so make +sure the rest of the application does not rely on them. + +== Publisher metrics + +SmallRye Reactive Messaging integrates MicroProfile Metrics and +Micrometer for registering counter metrics (named +`mp.messaging.message.count`) of published messages per channel. + +Both MicroProfile and Micrometer publisher metrics are enabled by +default if found on the classpath. They can be disabled with +`smallrye.messaging.metrics.mp.enabled` and +`smallrye.messaging.metrics.micrometer.enabled` properties respectively. diff --git a/content/docs/smallrye-reactive-messaging/main/concepts/blocking.adoc b/content/docs/smallrye-reactive-messaging/main/concepts/blocking.adoc new file mode 100644 index 000000000..078af3ac4 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/concepts/blocking.adoc @@ -0,0 +1,105 @@ += @Blocking +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The `io.smallrye.reactive.messaging.annotations.Blocking` annotation can +be used on a method annotated with `@Incoming`, or `@Outgoing` to +indicate that the method should be executed on a worker pool: + +[source,java] +---- +@Outgoing("Y") +@Incoming("X") +@Blocking +public String process(String s) { + return s.toUpperCase(); +} +---- + +If method execution does not need to be ordered, it can be indicated on +the `@Blocking` annotation: + +[source,java] +---- +@Outgoing("Y") +@Incoming("X") +@Blocking(ordered = false) +public String process(String s) { + return s.toUpperCase(); +} +---- + +When unordered, the invocation can happen concurrently. + +By default, use of `@Blocking` results in the method being executed in +the Vert.x worker pool. If it’s desired to execute methods on a custom +worker pool, with specific concurrency needs, it can be defined on +`@Blocking`: + +[source,java] +---- +@Outgoing("Y") +@Incoming("X") +@Blocking("my-custom-pool") +public String process(String s) { + return s.toUpperCase(); +} +---- + +Specifying the concurrency for the above worker pool requires the +following configuration property to be defined: + +.... +smallrye.messaging.worker.my-custom-pool.max-concurrency=3 +.... + +== Custom worker pool shutdown timeout + +When using a custom worker pool, it is possible to define a shutdown timeout in order to wait for the scheduled tasks to complete before shutting down the pool. +This can be done by setting the following configuration property: + +[source,properties] +---- +smallrye.messaging.worker.my-custom-pool.max-concurrency=3 +smallrye.messaging.worker.my-custom-pool.shutdown-timeout=10000 +smallrye.messaging.worker.my-custom-pool.shutdown-check-interval=2000 +---- + +The `shutdown-timeout` property defines the maximum time to wait in milliseconds for the scheduled tasks to complete before shutting down the pool, defaults to 60 seconds. +The `shutdown-check-interval` property defines the interval in milliseconds at which the pool will check if the tasks have completed, defaults to 5 seconds. + +== Supported signatures + +`@Blocking` does not support every signature. The following table lists +the supported ones. + +[width="100%",cols="7%,36%,57%",options="header",] +|=== +|Shape |Signature |Comment +|Publisher |`@Outgoing("in") @Blocking O generator()` |Invokes the generator from a worker thread. If `ordered` is set to `false`, the generator can be called concurrently. +|Publisher |`@Outgoing("in") @Blocking Message++<++O++>++ generator()` |Invokes the generator from a worker thread. If `ordered` is set to `false`, the generator can be called concurrently. +|Processor |`@Incoming("in") @Outgoing("bar") @Blocking O process(I in)` |Invokes the method on a worker thread. If `ordered` is set to `false`, the method can be called concurrently. +|Processor |`@Incoming("in") @Outgoing("bar") @Blocking Message++<++O++>++ process(I in)` |Invokes the method on a worker thread. If `ordered` is set to `false`, the method can be called concurrently. +|Subscriber |`@Incoming("in") @Blocking void consume(I in)` |Invokes the method on a worker thread. If `ordered` is set to `false`, the method can be called concurrently. +|Subscriber |`@Incoming("in") @Blocking Uni++<++Void++>++ consume(I in)` |Invokes the method on a worker thread. If `ordered` is set to `false`, the method can be called concurrently. +|Subscriber |`@Incoming("in") @Blocking Uni++<++Void++>++ consume(Message++<++I++>++ msg)` |Invokes the method on a worker thread. If `ordered` is set to `false`, the method can be called concurrently. +|Subscriber |`@Incoming("in") @Blocking CompletionStage++<++Void++>++ consume(I in)` |Invokes the method on a worker thread. If `ordered` is set to `false`, the method can be called concurrently. +|Subscriber |`@Incoming("in") @Blocking CompletionStage++<++Void++>++ consume(Message++<++I++>++ msg)` |Invokes the method on a worker thread. If `ordered` is set to `false`, the method can be called concurrently. +|=== + +When a method can be called concurrently, the max concurrency depends on +the number of threads from the worker thread pool. + +== Using io.smallrye.common.annotation.Blocking + +`io.smallrye.common.annotation.Blocking` is another annotation with the +same semantic. `io.smallrye.common.annotation.Blocking` is used by +multiple SmallRye projects and Quarkus. + +SmallRye Reactive Messaging also supports +`io.smallrye.common.annotation.Blocking`. However, +`io.smallrye.common.annotation.Blocking` does not allow configuring the +ordering (it defaults to `ordered=true`). + +When both annotations are used, +`io.smallrye.reactive.messaging.annotations.Blocking` is preferred. diff --git a/content/docs/smallrye-reactive-messaging/main/concepts/broadcast.adoc b/content/docs/smallrye-reactive-messaging/main/concepts/broadcast.adoc new file mode 100644 index 000000000..a10dc2c86 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/concepts/broadcast.adoc @@ -0,0 +1,49 @@ += Broadcast +:version-root: ../ +include::{version-root}_attributes.adoc[] + +[WARNING] +.Experimental +==== + +`@Broadcast` is an experimental feature. + +==== + +By default, messages transiting in a channel are only dispatched to a +single consumer. Having multiple consumers is considered as an error, +and is reported at deployment time. + +The link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/io/smallrye/reactive/messaging/annotations/Broadcast.html[`Broadcast`] +annotation changes this behavior and indicates that messages transiting +in the channel are dispatched to all the consumers. `@Broadcast` must be +used with the `@Outgoing` annotation: + +[source,java] +---- +include::{examples-dir}/broadcast/BroadcastExamples.java[tags=chain,indent=0] +---- + +In the previous example, both consumers get the messages. + +You can also control the number of consumers to wait before starting to +dispatch the messages. This allows waiting for the complete graph to be +woven: + +[source,java] +---- +include::{examples-dir}/broadcast/BroadcastWithCountExamples.java[tags=chain,indent=0] +---- + +[NOTE] +==== + +Inbound connectors also support a `broadcast` attribute that allows +broadcasting the messages to multiple downstream subscribers. + +==== + +Use with Emitter + +For details on how to use `@Broadcast` with `Emitter` see the +xref:concepts/emitter.adoc#emitter-and-broadcast[documentation]. diff --git a/content/docs/smallrye-reactive-messaging/main/concepts/client-customizers.adoc b/content/docs/smallrye-reactive-messaging/main/concepts/client-customizers.adoc new file mode 100644 index 000000000..2ca59c240 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/concepts/client-customizers.adoc @@ -0,0 +1,28 @@ += Client Customizers +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Client customizers allow to customize the client instance created by the connector. +Only connectors which create their own client instance support customizers. + +To define a client customizer, you need to provide a CDI bean that implements the `ClientCustomizer++<++T++>++` interface, +parameterized with the config type: + +[source,java] +---- +include::{examples-dir}/customizers/MyClientCustomizer.java[indent=0] +---- + +Connectors which support client customizers will discover all beans and call the `customize` method, +with the _channel name_, the _channel configuration_ and the configuration that'll be used to create the client instance. +If the customizer returns `null` it'll be skipped. + +If you have multiple customizers, customizers can override the `getPriority` method to define the order in which they are called. + +Currently, the following core connectors support client customizers: + +* Kafka: `ClientCustomizer++<++Map++<++String, Object++>>++` +* RabbitMQ: `ClientCustomizer++<++RabbitMQOptions++>++` +* AMQP 1.0: `ClientCustomizer++<++AmqpClientOptions++>++` +* MQTT: `ClientCustomizer++<++MqttClientSessionOptions++>++` +* Pulsar: `ClientCustomizer++<++ClientBuilder++>++`, `ClientCustomizer++<++ConsumerBuilder++<++?++>>++`, `ClientCustomizer++<++ProducerBuilder++<++?++>>++` diff --git a/content/docs/smallrye-reactive-messaging/main/concepts/concepts.adoc b/content/docs/smallrye-reactive-messaging/main/concepts/concepts.adoc new file mode 100644 index 000000000..8b9079d3e --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/concepts/concepts.adoc @@ -0,0 +1,97 @@ += Concepts +:version-root: ../ +include::{version-root}_attributes.adoc[] + +When dealing with event-driven or data streaming applications, there are +a few concepts and vocabulary to introduce. + +== Messages, Payload, Metadata + +A `Message` is an _envelope_ around a `payload`. Your application is +going to receive, process, and send `Messages`. + +Your application’s logic can generate these `Messages` or receive them +from a message broker. They can also be consumed by your application or +sent to a message broker. + +.An application can receive a message, process it and send a resulting message +image::images/messages.png[An application can receive a message, process it and send a resulting message] + +In Reactive Messaging, `Message` are represented by the link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/io/eclipse/microprofile/reactive/messaging/Message.html[`Message`] interface. +Each `Message++<++T++>++` contains a _payload_ of type `++<++T++>++`. This payload can be retrieved using `message.getPayload()`: + +[source,java] +---- +include::{examples-dir}/messages/MessageExamples.java[tags=message,indent=0] +---- + +As you can see in the previous snippet, `Messages` can also have +_metadata_. _Metadata_ is a way to extend messages with additional data. +It can be metadata related to the message broker +(like link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/io/smallrye/reactive/messaging/kafka/api/KafkaMessageMetadata.html[`KafkaMessageMetadata`]), or contain operational data (such as tracing metadata), or business-related data. + +[NOTE] +==== + +When retrieving metadata, you get an `Optional` as it may not be present. + +==== + +[TIP] +==== + +Metadata is also used to influence the outbound dispatching (how the message will be sent to the broker). + +==== + +== Channels and Streams + +Inside your application, `Messages` transit on _channel_. A _channel_ is +a virtual destination identified by a name. + +.The application is a set of channels +image::images/channels.png[The application is a set of channels] + +SmallRye Reactive Messaging connects the component to the channel they read and to the channel they populate. +The resulting structure is a stream: Messages flow between components through channels. + +[NOTE] +.What about Reactive Streams? +==== + +You may wonder why _Reactive Messaging_ has _Reactive_ in the name. The +Messaging part is kind of obvious. The Reactive part comes from the +streams that are created by binding components. These streams are +https://www.reactive-streams.org/[Reactive Streams]. They follow the +subscription and request protocol and implement back-pressure. It also +means that xref:#connectors[Connectors] are intended to use non-blocking IO +to interact with the various message brokers. + +==== + +== Connectors + +Your application is interacting with messaging brokers or event backbone using _connectors_. +A _connector_ is a piece of code that connects to a broker and: + +[arabic] +. subscribe/poll/receive messages from the broker and propagate them to the application +. send/write/dispatch messages provided by the application to the broker + +Connectors are configured to map incoming messages to a specific +_channel_ (consumed by the application) and collect outgoing messages +sent to a specific channel. These collected messages are sent to the +external broker. + +.Connectors manages the communication between the application and the brokers +image::images/connectors.png[Connectors manages the communication between the application and the brokers] + +Each connector is dedicated to a specific technology. +For example, a Kafka Connector only deals with Kafka. + +You don’t necessarily need a connector. +When your application does not use connectors, everything happens _in-memory_, and the streams are created by chaining methods altogether. +Each chain is still a reactive stream and enforces the back-pressure protocol. +When you don’t use connectors, you need to make sure the chain is complete, meaning it starts with a message source, and it ends with a sink. +In other words, you need to generate messages from within the application (using a method with only `@Outgoing`, or an `Emitter`) and consume the messages +from within the application (using a method with only `@Incoming` or using an unmanaged stream). diff --git a/content/docs/smallrye-reactive-messaging/main/concepts/connectors.adoc b/content/docs/smallrye-reactive-messaging/main/concepts/connectors.adoc new file mode 100644 index 000000000..5a10eafca --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/concepts/connectors.adoc @@ -0,0 +1,199 @@ += Connectors +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Reactive Messaging can handle messages generated from within the +application but also interact with remote _brokers_. Reactive Messaging +Connectors interacts with these remote _brokers_ to retrieve messages +and send messages using various protocols and technology. + +Each connector handles to a specific technology. For example, a Kafka +Connector is responsible for interacting with Kafka, while an MQTT +Connector is responsible for MQTT interactions. + +== Connector name + +Each connector has a name. This name is referenced by the application to +indicate that this connector manages a specific channel. + +For example, the SmallRye Kafka Connector is named: `smallrye-kafka`. + +== Inbound and Outbound connectors + +Connector can: + +[arabic] +. retrieve messages from a remote broker (inbound) +. send messages to a remote broker (outbound) + +A connector can, of course, implement both directions. + +Inbound connectors are responsible for: + +[arabic] +. Getting messages from the remote broker, +. Creating a Reactive Messaging `Message` associated with the +retrieved message. +. Potentially associating technical metadata with the message. It +includes unmarshalling the payload. +. Associating an acknowledgment callback to acknowledge the incoming +message when the Reactive Messaging message is +processed/acknowledged. + +[IMPORTANT] +.Reactive matters +==== + +The first step should follow the reactive streams principle: uses +non-blocking technology, respects downstream requests. + +==== + +Outbound connectors are responsible for: + +[arabic] +. Receiving Reactive Messaging `Message` and transform it into a +structure understood by the remote broker. It includes marshaling +the payload. +. If the `Message` contains outbound metadata (metadata set during the +processing to influence the outbound structure and routing), taking +them into account. +. Sending the message to the remote broker. +. Acknowledging the Reactive Messaging `Message` when the broker has +accepted/acknowledged the message. + +== Configuring connectors + +Applications need to configure the connector used by expressing which +channel is managed by which connector. Non-mapped channels are local / +in-memory. + +To configure connectors, you need to have an implementation of +MicroProfile Config. If you don’t have one, add an implementation of +MicroProfile Config in your _classpath_, such as: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.config + smallrye-config + {smallrye-config-version} + +---- + +Then edit the application configuration, generally +`src/main/resources/META-INF/microprofile-config.properties`. + +The application configures the connector with a set of properties +structured as follows: + +.... +mp.messaging.[incoming|outgoing].[channel-name].[attribute]=[value] +.... + +For example: + +.... +mp.messaging.incoming.dummy-incoming-channel.connector=dummy +mp.messaging.incoming.dummy-incoming-channel.attribute=value +mp.messaging.outgoing.dummy-outgoing-channel.connector=dummy +mp.messaging.outgoing.dummy-outgoing-channel.attribute=value +.... + +You configure each channel (both incoming and outgoing) individually. + +The `++[++incoming++|++outgoing++]++` segment indicates the direction. + +* an `incoming` channel consumes data from a message broker or +something producing data. It’s an inbound interaction. It relates to +methods annotated with an `@Incoming` using the same channel name. +* an `outgoing` consumes data from the application and forwards it to +a message broker or something consuming data. It’s an outbound +interaction. It relates to methods annotated with an `@Outgoing` +using the same channel name. + +The `++[++channel-name++]++` is the name of the channel. If the channel name +contains a `.` (dot), you would need to use `"` (double-quote) around +it. For example, to configure the `dummy.incoming.channel` channel, you +would need: + +.... +mp.messaging.incoming."dummy.incoming.channel".connector=dummy +mp.messaging.incoming."dummy.incoming.channel".attribute=value +.... + +The `++[++attribute++]++=++[++value++]++` sets a specific connector attribute to the +given value. Attributes depend on the used connector. So, refer to the +connector documentation to check the supported attributes. + +The `connector` attribute must be set for each mapped channel and +indicates the name of the connector responsible for the channel. + +Here is an example of a channel using an MQTT connector, consuming data +from a MQTT broker, and a channel using a Kafka connector (writing data +to Kafka): + +[source,properties] +---- +# Configure the incoming health channel +mp.messaging.incoming.health.topic=neo +mp.messaging.incoming.health.connector=smallrye-mqtt +mp.messaging.incoming.health.host=localhost +mp.messaging.incoming.health.broadcast=true + +# Configure outgoing data channel +mp.messaging.outgoing.data.connector=smallrye-kafka +mp.messaging.outgoing.data.bootstrap.servers=localhost:9092 +mp.messaging.outgoing.data.key.serializer=org.apache.kafka.common.serialization.StringSerializer +mp.messaging.outgoing.data.value.serializer=io.vertx.kafka.client.serialization.JsonObjectSerializer +mp.messaging.outgoing.data.acks=1 +---- + +[IMPORTANT] +==== + +To use a connector, you need to add it to your _CLASSPATH_. Generally, +adding the dependency to your project is enough. Then, you need to know +the connector’s name and set the `connector` attribute for each channel +managed by this connector. + +==== + +[TIP] +.Connector names +==== + +Connector managed by the SmallRye Reactive Messaging project are typically +prefixed with `smallrye-`. For example, the SmallRye Kafka Connector is +named `smallrye-kafka`. When using Smallrye connectors, you can omit the +prefix and just use `kafka` as the connector name: + +[source,properties] +---- +mp.messaging.incoming.my-channel.connector=kafka +mp.messaging.connector.kafka.bootstrap.servers=localhost:9092 +---- + +==== + +== Connector attribute table + +In the connector documentation, you will find a table listing the +attribute supported by the connector. Be aware that attributes for +inbound and outbound interactions may be different. + +These tables contain the following entries: + +[arabic] +. The name of the attribute, and potentially an _alias_. The name of +the attribute is used with the +`mp.messaging.++[++incoming++|++outgoing++]++.++[++channel-name++]++.++[++attribute++]++=++[++value++]++` +syntax (the `attribute` segment). The _alias_ (if set) is the name +of a global MicroProfile Config property that avoids having to +configure the attribute for each managed channel. For example, to +set the location of your Kafka broker globally, you can use the +`kafka.bootstrap.servers` alias. +. The description of the attribute, including the type. +. Whether that attribute is mandatory. If so, it fails the +deployment if not set +. The default value, if any. diff --git a/content/docs/smallrye-reactive-messaging/main/concepts/contributing-connectors.adoc b/content/docs/smallrye-reactive-messaging/main/concepts/contributing-connectors.adoc new file mode 100644 index 000000000..2af522fc2 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/concepts/contributing-connectors.adoc @@ -0,0 +1,457 @@ += Connector Contribution Guide +:version-root: ../ +include::{version-root}_attributes.adoc[] + +A connector implementation is a CDI-managed bean, typically an `@ApplicationScoped` bean, +which is identified by the `@Connector` identifier. +In order to provide inbound and outbound channels, the connector implements two interfaces `InboundConnector` and `OutboundConnector` respectively. +In addition to that, the connector bean is annotated with `@ConnectorAttribute`, which describes attributes to configure channels. + +[NOTE] +.Maven Archetype +==== + +Smallrye Reactive Messaging provides a Maven archetype to bootstrap a new connector. +You can generate a new connector project with the code described in this guide using: + +[source,shell,subs="attributes+"] +---- +mvn -N archetype:generate \ +-DarchetypeGroupId=io.smallrye.reactive \ +-DarchetypeArtifactId=smallrye-reactive-messaging-connector-archetype \ +-DarchetypeVersion={project-version} \ +-DgroupId=io.smallrye.reactive \ +-Dpackage=io.smallrye.reactive.messaging.my \ +-Dversion={project-version} \ +-DartifactId=smallrye-reactive-messaging-my \ +-DconnectorName=my +---- + +==== + +The following is an example of a connector skeleton : + +[source,java] +---- +include::{examples-dir}/connectors/MyConnector.java[indent=0] +---- + +Note that the `getPublisher` and `getSubscriber` methods receive MicroProfile Config `Config` instance and wrap it with +`MyConnectorIncomingConfiguration` and `MyConnectorOutgoingConfiguration` objects. + +These custom channel configuration types ease getting channel configuration, including the optional or default values. +They are generated by the `smallrye-connector-attribute-processor` annotation processor, and can be configured in project pom like the following: + +[source,xml] +---- + + + ${project.groupId} + smallrye-reactive-messaging-provider + ${project.version} + + + io.smallrye.reactive + smallrye-connector-attribute-processor + ${project.version} + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + ${project.build.directory}/generated-sources/ + + + io.smallrye.reactive.messaging.connector.ConnectorAttributeProcessor + + + org.jboss.logging.processor.apt.LoggingToolsProcessor + + + + + + +---- + +The `smallrye-reactive-messaging-provider` is the minimum required dependency for a connector implementation. +You'll also note that the `LoggingToolsProcessor` annotation processor is also configured. +This enables generating internationalized log statements and exceptions. +Typically, you would create following interfaces in `i18n` sub-package: `++[++Connector++]++Exceptions`, `++[++Connector++]++Logging` and `++[++Connector++]++Messages`. +More information can be found in https://jboss-logging.github.io/jboss-logging-tools/[JBoss Logging Tools documentation]. + +== Implementing Inbound Channels + +The `InboundConnector` implementation returns, for a given channel configuration, a reactive stream of `Message`s. +The returned reactive stream is an instance of `Flow.Publisher` and typically can be implemented using Mutiny `Multi` type. + +[NOTE] +.`IncomingConnectorFactory` +==== + +The inbound channels can also implement the `IncomingConnectorFactory` from the MicroProfile Reactive Messaging specification. +However, the `PublisherBuilder` type can be more challenging to work with and +Smallrye Reactive Messaging converts the provided stream to Mutiny types to do the wiring anyway. + +==== + +The returned `Flow.Publisher` would allow controlling the flow of ingestion using backpressure. +It would be preferable to use pull-based APIs of the underlying messaging library to receive messages from the message broker. +You can refer to the https://smallrye.io/smallrye-mutiny/latest/guides/polling/[`Mutiny` "How to use polling?" guide] +to construct a `Multi` using Mutiny APIs, or implement the `Flow.Subscription` from scratch and wrap it in an `AbstractMulti`. + +Here is an example channel implementation which constructs the reactive stream using the polling API: + +[source,java] +---- +include::{examples-dir}/connectors/MyIncomingChannel.java[indent=0] +---- + +=== Connector Threading and Vert.x + +Whether the external API call is blocking or non-blocking, managing the thread on which the message processing will be dispatched can be challenging. +Smallrye Reactive Messaging depends on https://vertx.io/[Eclipse Vert.x] to consistently dispatch messages in event-loop or worker threads, +propagating the message context along different processing stages. +You can read more on xref:concepts/message-context.adoc[Message Context] and https://vertx.io/blog/an-introduction-to-the-vert-x-context-object/[Vert.x Context]. + +Some connectors already use Vert.x clients, such as RabbitMQ, AMQP 1.0 or MQTT. +Other connectors such as Kafka or Pulsar directly use the client library of the messaging technology, +therefore they create a Vert.x Context per channel to dispatch messages on that context. +Connectors can access the `Vertx` instance by injecting the `ExecutionHolder` bean. +https://smallrye.io/smallrye-mutiny/latest/guides/emit-on-vs-run-subscription-on/[Mutiny operators `runSubscribtionOn` and `emitOn`] can be used to switch threads the events are dispatched on. + +=== Custom Message and Metadata implementations + +Reactive messaging link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/org/eclipse/microprofile/reactive/messaging/Message.html[`Message`] +type is a thin wrapper around a payload and some metadata, which lets implementing acknowledgment and negative acknowledgment of that message. + +Messages received from the underlying library very often return with a custom type, +wrapping the payload and some properties such as key, timestamp, schema information, or any other metadata. + +[TIP] +==== + +The client may propose different strategies for consuming messages individually or in batches. +If a batch consuming is available the incoming channel may receive wrap and dispatch message as a batch or individually. + +==== + +While it is possible to use `Message.of` builder methods to wrap the incoming message type, +a custom type implementing `Message` interface helps to deal with different aspects we'll cover later, +such as deserialization, message acknowledgment or tracing. + +An example message implementation would be like the following: + +[source,java] +---- +include::{examples-dir}/connectors/MyMessage.java[indent=0] +---- + +Note that `MyMessage` implements the `ContextAwareMessage`. +In the constructor `captureContextMetadata` helper method is used to capture the Vert.x context which created the object and capturing it into the `LocalContextMetadata`. +This metadata allows running xref:concepts/message-context.adoc[each message in its own Vert.x context, supporting context propagation]. + +The `MyMessage` type implements the accessors for the `metadata` and `payload` from the `Message` interface. +If the messaging technology doesn't have a built-in unmarshalling mechanism, the message can deserialize the raw payload to a primitive or a complex object. + +[WARNING] +==== + +The custom message implementation is usually not the type consumed by the application injecting channels. +Applications usually inject in the payload, the raw consumed type (in the above example the `ConsumedMessage`), +or some other type provided by the xref:concepts/converters.adoc[`MessageConverter`s]. +Handling of `Message` types by the application is restricted only to advanced use cases, because handling of message acknowledgment is manual +Even then the message may be intercepted before and changed, conserving the `metadata`, `ack` and `nack` handlers but not the original type created by the connector. + +==== + +The `MyIncomingMetadata` gives access to the underlying consumed message attributes, and applications can inject this object for accessing message details: + +[source,java] +---- +include::{examples-dir}/connectors/MyIncomingMetadata.java[indent=0] +---- + +Also note that `ack` and `nack` method implementations are delegated to handler objects. +This allows configuring different strategies at channel level. + +=== Acknowledgment strategies + +The xref:concepts/acknowledgement.adoc[acknowledgement] is the way for message consumers to inform the broker that the message has been successfully received and processed. +Depending on the messaging technology the broker then can decide to remove the message from the queue, flag as consumed or purge it completely. +In Reactive Messaging there are different policies to trigger the acknowledgement but the canonical one is to acknowledge a message when the processing (potentially asynchronous) has completed (`POST++_++PROCESSING`). + +The Reactive Messaging defines `Message#ack` method as non-blocking asynchronous, returning a `CompletionStage++<++Void++>++`, +because potentially the acknowledgement action sends a network call to the broker. + +The following example simply calls the client `ack` method using the Mutiny `Uni` and switch the emitter to the Message context. +Returning back to the message context is essential for chaining asynchronous actions without losing the context and for keeping the consistency on message consumption flow. + +[source,java] +---- +include::{examples-dir}/connectors/MyAckHandler.java[indent=0] +---- + +While this ack handler strategy acknowledges each message to the broker, +the messaging technology can allow employing different strategies for acknowledging messages. +For example an ack strategy can track processed messages and acknowledge them altogether or call a different client side endpoint to acknowledge the message batch. + +=== Failure handling strategies + +The failure handling, or the xref:concepts/acknowledgement.adoc#negative-acknowledgement[negative acknowledgment] allows indicating that a message was not processed correctly. +Similar to the acknowledgment the Reactive Messaging defines `Message#nack(Throwable reason, Metadata metadata)` method as non-blocking asynchronous, returning a `CompletionStage++<++Void++>++`. + +[source,java] +---- +include::{examples-dir}/connectors/MyFailureHandler.java[indent=0] +---- + +Different failure handling strategies can, for example, +- Ignore the failure, log and call the `ack` instead +- Send the message to a dead letter queue and call the `ack` +- Employ a different strategy depending on the `Metadata` associated with the `nack` method call. + +=== Message Converters + +The connector can propose default xref:concepts/converters.adoc[`MessageConverter`] implementations for converting the payload to a custom type. +As an example the following converter extracts the `CustomMessage` and puts it in the payload: + +[source,java] +---- +include::{examples-dir}/connectors/MyMessageConverter.java[indent=0] +---- + +== Implementing Outbound Channels + +The `OutboundConnector` implementation returns, for a given channel configuration, a `Flow.Subscriber` of messages. +This is typically implemented by a custom `Flow.Processor` and using the `MultiUtils.via` helper methods to apply message transformations. + +[NOTE] +.`OutgoingConnectorFactory` +==== + +The outbound channels can also implement the `OutgoingConnectorFactory` from the MicroProfile Reactive Messaging specification. +However, it is usually more friendly to work with the `MultiUtils.via` methods to construct and transform outgoing messages. + +==== + +Here is an example outgoing channel implementation: + +[source,java] +---- +include::{examples-dir}/connectors/MyOutgoingChannel.java[indent=0] +---- + +The `MultiUtils.via` helper method allows using the `Multi` chaining methods and in the same time provides a `Flow.Subscriber`. +However, this implementation allows sending messages one at a time: +one only after the previous outgoing message send is completed. + +Some messaging technologies provide publish receipt, +a message back from the broker to the sender that asynchronously acknowledges the sent operation. +In this case the connector can only be sure of the send operation when it receives the publish receipt of that message. +Some technologies may provide blocking sending calls, +in that case the connector needs to delegate the sending call to a worker thread. + +Depending on whether the client supports multiple in-flight outgoing messages, you can also use a `SenderProcessor`, +which allows receiving configuration for the maximum number of in-flight messages and whether it waits for completion (publish receipt from the broker): + +[source,java] +---- +include::{examples-dir}/connectors/MyOutgoingChannelWithPartials.java[tags=sender-processor,indent=0] +---- + +Other more advanced scenarios can be implemented to retry the transmission in case of a retryable failure, +or batch multiple outgoing messages to a single send operation. + +=== Outgoing `Message` Builder + +In order to convey all attributes of the outgoing message to the client library +connectors provide outgoing `Message` implementation and a corresponding outgoing message metadata. +These allow the application developer to build the message attributes that will be sent to the broker. + +[source,java] +---- +include::{examples-dir}/connectors/MyOutgoingMessage.java[indent=0] +---- + +[source,java] +---- +include::{examples-dir}/connectors/MyOutgoingMetadata.java[indent=0] +---- + +The outgoing channel implementation then will construct the client library object that represents the outbound message, `SendMessage` in this example: + +[source,java] +---- +include::{examples-dir}/connectors/MyOutgoingChannelWithPartials.java[tags=send-message,indent=0] +---- + +It is a best practice to also allow the application to return a payload of the client outbound library object (`SendMessage`). + +=== Outgoing message acknowledgement + +Because the Reactive Messaging xref:concepts/acknowledgement.adoc#chain-of-acknowledgment[chains acknowledgements] from incoming message until the outgoing message, +it is crucial for the outgoing channel to correctly ack and nack the message. + +== Smallrye Health Integration + +Smallrye Reactive Messaging allows connectors to integrate with Smallrye Health to contribute channel state to the health reports. +Connectors need to implement the `HealthReporter` interface and implement some or all of the `getReadiness`, `getLiveness` and `getStartup` methods: + +[source,java] +---- +include::{examples-dir}/connectors/MyConnectorWithPartials.java[tags=health-report,indent=0] +---- + +Implementing health reports per channel depends on what information is available to the connector. +For more information on different health check probes you can check out https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/[Configure Liveness, Readiness and Startup Probes] + +You may want to add a connector attribute to enable/disable the health reporting per channel: + +[source,java] +---- +import io.smallrye.reactive.messaging.annotations.ConnectorAttribute; + +@ConnectorAttribute(name = "health-enabled", type = "boolean", direction = ConnectorAttribute.Direction.INCOMING_AND_OUTGOING, description = "Whether health reporting is enabled (default) or disabled", defaultValue = "true") +---- + +== OpenTelemetry Tracing Integration + +Smallrye Reactive Messaging allows connectors to easily integrate with OpenTelemetry tracing. +It propagates the tracing context from inbound messages and to outbound messages. +The `smallrye-reactive-messaging-otel` module provides necessary dependencies to the OpenTelemetry artifacts and also provides `TracingUtils` helper class for setting up the tracing. + +[source,xml] +---- + + io.smallrye.reactive.messaging + smallrye-reactive-messaging-otel + +---- + +For integrating tracing you'd need to create a couple of classes: + +* Holder class for tracing information +* Implementation of `io.opentelemetry.context.propagation.TextMapGetter`, which retrieves for a given key the value of a tracing attributed from the holder object +* Implementation of `io.opentelemetry.context.propagation.TextMapSetter`, which sets on the holder object the key and value of tracing attributes +* Implementations of `io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor` and `io.opentelemetry.instrumentation.api.instrumenter.messaging.MessagingAttributesGetter` + +Then you'd need to configure instrumenters per incoming and outgoing channel: + +[source,java] +---- +include::{examples-dir}/connectors/tracing/MyOpenTelemetryInstrumenter.java[tags=create-instrumenter,indent=0] +---- + +Finally, you'd need to configure instrumenters per incoming and outgoing channels and wire the call to instrumenter using `TracingUtils`. + +For an incoming channel, you'd need to call the instrumenter on an inbound message: + +[source,java] +---- +include::{examples-dir}/connectors/MyIncomingChannelWithPartials.java[tags=incoming-tracing,indent=0] +---- + +For an outgoing channel, you'd need to call the instrumenter on constructing the outbound message: + +[source,java] +---- +include::{examples-dir}/connectors/MyOutgoingChannelWithPartials.java[tags=outgoing-tracing,indent=0] +---- + +You may want to add a connector attribute to enable/disable the tracing per channel: + +[source,java] +---- +import io.smallrye.reactive.messaging.annotations.ConnectorAttribute; + +@ConnectorAttribute(name = "tracing-enabled", type = "boolean", direction = ConnectorAttribute.Direction.INCOMING_AND_OUTGOING, description = "Whether tracing is enabled (default) or disabled", defaultValue = "true") +---- + +== Testing the connector + +While unit tests are highly encouraged for validating ad-hoc logic in connector code, +by nature connector tests are mostly integration tests validating the correct configuration and functioning of channels. +Most of the time tests need to run against a broker instance. +This instance can be mocked or embedded in the test JVM, +or provisioned in a container runtime using https://testcontainers.org[Testcontainers]. +The testcontainers approach is encouraged as it'll provide a testing environment closest to reality. + +It may take too much time and resources to start a broker per test method or per test class, +so may want to share the same broker instance between all test classes. +In that case you can checkout how to write a https://junit.org/junit5/docs/current/user-guide/#extensions[JUnit 5 Extension] +and start only one container instance in the beginning of tests and stop it at the end of all the tests. + +There are essentially two ways of creating the connector behavior to test against: + +[arabic] +. Instantiating channels directly by passing the custom configuration. +With this you can get the Reactive stream directly from the channel implementation and send/receive messages. +You can use https://smallrye.io/smallrye-mutiny/2.5.1/guides/testing/[AssertSubscriber from Mutiny] to regulate demand and write assertions. +. CDI-based tests which write configuration and instantiate application beans. +You can use https://weld.cdi-spec.org/[Weld, the reference implementation of CDI specification] with configured set of beans and extensions: + +[source,java] +---- +include::{examples-dir}/connectors/test/WeldTestBase.java[indent=0] +---- + +You would need following test dependencies for enabling Weld in tests: + +[source,xml] +---- + + io.smallrye.reactive + test-common + test + + + org.jboss.weld.se + weld-se-shaded + ${weld.version} + test + + + org.jboss.weld + weld-core-impl + ${weld.version} + test + + + org.awaitility + awaitility + test + +---- + +Your test classes can therefore extend the `WeldTestBase` and provide configuration and application beans: + +[source,java] +---- +include::{examples-dir}/connectors/test/MyConnectorTest.java[indent=0] +---- + +[TIP] +.Awaitility +==== + +Because connector tests are usually asynchronous, https://github.com/awaitility/awaitility[awaitility] +provides a DSL to await on expressed assertions. + +==== + +=== Common tests for validating the connector + +* Message consumption through incoming channels +* Message producing through outgoing channels +* Ack and failure handler strategies test +* Message Context propagation test `LocalPropagationTest` +* `HealthCheckTest` +* `MessageConverterTest` +* `TracingPropagationTest` +* Configuration test +* Authentication test +* Tests for diff --git a/content/docs/smallrye-reactive-messaging/main/concepts/converters.adoc b/content/docs/smallrye-reactive-messaging/main/concepts/converters.adoc new file mode 100644 index 000000000..ff3f6148e --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/concepts/converters.adoc @@ -0,0 +1,43 @@ += Message Converters +:version-root: ../ +include::{version-root}_attributes.adoc[] + +SmallRye Reactive Messaging supports _message converters_, allowing to +transform an incoming message into a version accepted by the method. If +the incoming messages or payload does not match the invoked method’s +expectation, SmallRye Reactive Messaging looks for a suitable converter. +If found, it converts the incoming message with this converter. + +Converters can have multiple purposes, but the main use case is about +transforming the message’s payload: + +[source,java] +---- +include::{examples-dir}/converters/MyConverter.java[tags=code,indent=0] +---- + +To provide a converter, implement a bean exposing the `MessageConverter` +interface. The `canConvert` method is called during the lookup and +verifies if it can handle the conversion. The `target` type is the +expected payload type. If the converter returns `true` to `canConvert`, +SmallRye Reactive Messaging calls the `convert` method to proceed to the +conversion. + +The previous converter can be used in application like the following, to +convert `Message++<++String++>++` to `Message++<++Person++>++`: + +[source,java] +---- +include::{examples-dir}/converters/ConverterExample.java[tags=code,indent=0] +---- + +Converters work for all supported method signatures. However, the +signature must be well-formed to allow the extraction of the expected +payload type. Wildcards and raw types do not support conversion. If the +expected payload type cannot be extracted, or no converter fits, the +message is passed as received. + +If multiple suitable converters are present, implementations should +override the `getPriority` method returning the priority. The default +priority is `100`. The converter lookup invokes converters with higher +priority (from the least value to the greatest) first. diff --git a/content/docs/smallrye-reactive-messaging/main/concepts/decorators.adoc b/content/docs/smallrye-reactive-messaging/main/concepts/decorators.adoc new file mode 100644 index 000000000..0cdb8b7a9 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/concepts/decorators.adoc @@ -0,0 +1,96 @@ += Channel Decorators +:version-root: ../ +include::{version-root}_attributes.adoc[] + +SmallRye Reactive Messaging supports decorating reactive streams +of incoming and outgoing channels for implementing cross-cutting +concerns such as monitoring, tracing or message interception. + +Two symmetrical APIs are proposed for decorating publisher and subscriber channels, +link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/io/smallrye/reactive/messaging/PublisherDecorator.html[`PublisherDecorator`] +and link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/io/smallrye/reactive/messaging/SubscriberDecorator.html[`SubscriberDecorator`] respectively. + +[IMPORTANT] +==== + +`@Incoming` channels and channels bound to an outbound connector are both `Subscriber`s. +Conversely `@Outgoing` channels and channels bound to an inbound connector are `Publisher`s. + +==== + +For example, to provide a decorator which counts consumed messages from incoming connector, +implement a bean exposing the interface `PublisherDecorator`: + +[source,java] +---- +include::{examples-dir}/decorators/ConsumedMessageDecorator.java[tags=code,indent=0] +---- + +Decorators' `decorate` method is called only once per channel at application deployment when graph wiring is taking place. +Decorators are very powerful because they receive the stream of messages (Mutiny `Multi++<++Message++<++?++>>++`) +and potentially return a new stream of messages. + +Note that if a decorator is available it will be called on every channel. +The `decorate` method receives the channel name and whether the channel is a connector or not as parameters. +Decorators are called ordered from highest to lowest priority (from the least value to the greatest), +obtained using the `jakarta.enterprise.inject.spi.Prioritized#getPriority` method. + +[NOTE] +==== + +The `SubscriberDecorator` receive a list of channel names because `@Incoming` annotation is repeatable +and consuming methods can be linked to multiple channels. + +==== + +== Intercepting Incoming and Outgoing Messages + +Decorators (`PublisherDecorator` and `SubscriberDecorator`) can be used to intercept and alter messages, both on incoming and outgoing channels. + +Smallrye Reactive Messaging allows defining intercepting incoming and outgoing messages for a specific channel, using +`IncomingInterceptor` and `OutgoingInterceptor` respectively. + +Only one interceptor is allowed to be bound for interception per outgoing channel. +If no interceptors are found with a `@Identifier` but a `@Default` one is available, it is used. +When multiple interceptors are available, the bean with the highest priority is used. + +=== `IncomingInterceptor` + +To provide an incoming interceptor implement a bean exposing the interface link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/io/smallrye/reactive/messaging/IncomingInterceptor.html[`IncomingInterceptor`], qualified with a `@Identifier` with the channel name to intercept. + +[source,java] +---- +include::{examples-dir}/interceptors/MyIncomingInterceptor.java[indent=0] +---- + +An `IncomingInterceptor` can implement these three methods: + +* `Message++<++?++>++ afterMessageReceive(Message++<++?++>++ message)` : Called after receiving the message from an incoming connector. +The message can be altered by returning a new message from this method. The modified message will be consumed in incoming channels. +* `void onMessageAck(Message++<++?++>++ message)` : Called after message acknowledgment. +* `void onMessageNack(Message++<++?++>++ message, Throwable failure)` : Called after message negative-acknowledgment. + +[NOTE] +==== + +If you are willing to adapt an incoming message payload to fit a consuming method receiving type, +you can use xref:concepts/converters.adoc[`MessageConverter`]s. + +==== + +=== `OutgoingInterceptor` + +To provide an outgoing interceptor implement a bean exposing the interface link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/io/smallrye/reactive/messaging/OutgoingInterceptor.html[`OutgoingInterceptor`], qualified with a `@Identifier` with the channel name to intercept. + +[source,java] +---- +include::{examples-dir}/interceptors/MyOutgoingInterceptor.java[indent=0] +---- + +An `OutgoingInterceptor` can implement these three methods: + +* `Message++<++?++>++ beforeMessageSend(Message++<++?++>++ message)` : Called before passing the message to the outgoing connector for transmission. +The message can be altered by returning a new message from this method. +* `void onMessageAck(Message++<++?++>++ message)` : Called after message acknowledgment. +This callback can access `OutgoingMessageMetadata` which will hold the result of the message transmission to the broker, if supported by the connector. This is only supported by MQTT and Kafka connectors. +* `void onMessageNack(Message++<++?++>++ message, Throwable failure)` : Called after message negative-acknowledgment. diff --git a/content/docs/smallrye-reactive-messaging/main/concepts/emitter.adoc b/content/docs/smallrye-reactive-messaging/main/concepts/emitter.adoc new file mode 100644 index 000000000..a59761323 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/concepts/emitter.adoc @@ -0,0 +1,270 @@ += Emitter and Channels +:version-root: ../ +include::{version-root}_attributes.adoc[] + +It is not rare to combine in a single application imperative parts +(Jax-RS, regular CDI _beans_) and reactive parts (_beans_ with +`@Incoming` and `@Outgoing` annotations). In these case, it’s often +required to send _messages_ from the imperative part to the reactive +part. In other words, send messages to channels handled by reactive +messaging and how can you retrieve messages. + +== Emitter and @Channel + +To send _things_ (payload or `Message`) from imperative code to a +specific channel you need to use: + +[arabic] +. the link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/org/eclipse/microprofile/reactive/messaging/Channel.html[`org.eclipse.microprofile.reactive.messaging.Channel`] annotations +. the link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/org/eclipse/microprofile/reactive/messaging/Emitter.html[`org.eclipse.microprofile.reactive.messaging.Emitter`] type + +The `@Channel` lets you indicate to which channel you are going to send +your payloads or messages. The `Emitter` is the object to use to send +these payloads or messages. + +[source,java] +---- +include::{examples-dir}/emitter/MyImperativeBean.java[tags=intro,indent=0] +---- + +The `Emitter` class takes a type parameter. It’s the type of payload. +Even if you want to send `Messages`, the type is the payload type. + +[IMPORTANT] +==== + +You must have a `@Incoming("prices")` somewhere in your application +(meaning a method consuming messages transiting on the channel +`prices`), or an outbound connector configured to manage the `prices` +channel (`mp.messaging.outgoing.prices...`) + +==== + +== Sending payloads + +Sending payloads is done as follows: + +[source,java] +---- +include::{examples-dir}/emitter/EmitterExamples.java[tags=payload,indent=0] +---- + +When sending a payload, the emitter returns a `CompletionStage`. This +`CompletionStage` gets completed once the message created from the +payload is acknowledged: + +[source,java] +---- +include::{examples-dir}/emitter/EmitterExamples.java[tags=cs,indent=0] +---- + +If the processing fails, the `CompletionStage` gets completed +exceptionally (with the reason of the nack). + +== Sending messages + +You can also send `Messages`: + +[source,java] +---- +include::{examples-dir}/emitter/EmitterExamples.java[tags=message,indent=0] +---- + +When sending a `Message`, the emitter does not return a +`CompletionStage`, but you can pass the ack/nack callback, and be called +when the message is acked/nacked. + +[source,java] +---- +include::{examples-dir}/emitter/EmitterExamples.java[tags=message-ack,indent=0] +---- + +Sending messages also let you pass metadata. + +[source,java] +---- +include::{examples-dir}/emitter/EmitterExamples.java[tags=message-meta,indent=0] +---- + +Metadata can be used to propagate some context objects with the message. + +== Overflow management + +When sending messages from imperative code to reactive code, you must be +aware of back-pressure. Indeed, messages sent using the emitter and +stored in a _queue_. If the consumer does not process the messages +quickly enough, this queue can become a memory hog and you may even run +out of memory. + +To control what need to happen when the queue becomes out of control, +use the link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/org/eclipse/microprofile/reactive/messaging/OnOverflow.html[`OnOverflow`] annotation. `@OnOverflow` lets you configure: + +* the maximum size of the queue (default is 256) +* what needs to happen when this size is reached (fail, drop...) + +[source,java] +---- +include::{examples-dir}/emitter/EmitterExamples.java[tags=overflow,indent=0] +---- + +The supported strategies are: + +* `OnOverflow.Strategy.BUFFER` - use a buffer to store the elements +until they are consumed. If the buffer is full, a failure is +propagated (and the thread using the emitted gets an exception) +* `OnOverflow.Strategy.UNBOUNDED++_++BUFFER` - use an unbounded buffer to +store the elements +* `OnOverflow.Strategy.DROP` - drops the most recent value if the +downstream can’t keep up. It means that new value emitted by the +emitter are ignored. +* `OnOverflow.Strategy.FAIL` - propagates a failure in case the +downstream can’t keep up. +* `OnOverflow.Strategy.LATEST` - keeps only the latest value, dropping +any previous value if the downstream can’t keep up. +* `OnOverflow.Strategy.NONE` - ignore the back-pressure signals +letting the downstream consumer to implement a strategy. + +=== Defensive emission + +Having an emitter injected into your code does not guarantee that +someone is ready to consume the message. For example, a subscriber may +be connecting to a remote broker. If there are no subscribers, using the +`send` method will throw an exception. + +The `emitter.hasRequests()` method indicates that a subscriber +subscribes to the channel and requested items. So, you can wrap your +emission with: + +[source,java] +---- +if (emitter.hasRequests()) { + emitter.send("hello"); +} +---- + +If you use the `OnOverflow.Strategy.DROP`, you can use the `send` method +even with no subscribers nor demands. The message will be nacked +immediately. + +== Retrieving channels + +You can use the `@Channel` annotation to inject in your bean the +underlying stream. Note that in this case, you will be responsible for +the subscription: + +[source,java] +---- +include::{examples-dir}/emitter/ChannelExamples.java[tags=channel,indent=0] +---- + +[IMPORTANT] +==== + +You must have a `@Outgoing("my-channel")` somewhere in your application +(meaning a method generating messages transiting on the channel +`my-channel`), or an inbound connector configured to manage the `prices` +channel (`mp.messaging.incoming.prices...`) + +==== + +Injected _channels_ merge all the matching _outgoing_ - so if you have +multiple `@Outgoing("out")`, `@Inject @Channel("out")` gets all the +messages. + +If your injected _channel_ receives _payloads_ (`Multi++<++T++>++`), it +acknowledges the message automatically, and support multiple +subscribers. If you injected _channel_ receives `Message` +(`Multi++<++Message++<++T++>>++`), you will be responsible for the acknowledgement +and broadcasting. + +== Emitter and @Broadcast + +When using an `Emitter`, you can now `@Broadcast` what is emitted to all +subscribers. + +Here is an example of emitting a price with two methods marked +`@Incoming` to receive the broadcast: + +[source,java] +---- +include::{examples-dir}/emitter/EmitterExamples.java[tags=broadcast,indent=0] +---- + +For more details see xref:concepts/broadcast.adoc[@Broadcast] +documentation. + +== Mutiny Emitter + +If you prefer to utilize `Uni` in all your code, there is now a +`MutinyEmitter` that will return `Uni++<++Void++>++` instead of `void`. + +[source,java] +---- +include::{examples-dir}/emitter/MutinyExamples.java[tags=uni,indent=0] +---- + +There’s also the ability to block on sending the event to the emitter. +It will only return from the method when the event is acknowledged, or +nacked, by the receiver: + +[source,java] +---- +include::{examples-dir}/emitter/MutinyExamples.java[tags=uni-await,indent=0] +---- + +And if you don’t need to worry about the success or failure of sending +an event, you can `sendAndForget`: + +[source,java] +---- +include::{examples-dir}/emitter/MutinyExamples.java[tags=uni-forget,indent=0] +---- + +== Custom Emitter Implementations + +[WARNING] +.Experimental +==== + +Custom emitter implementations is an experimental feature. + +==== + +`Emitter` and `MutinyEmitter` are two implementations of the emitter concept, +where imperative code in your application can send messages to Reactive Messaging channels. + +With `EmitterFactory` it is possible to provide custom implementations, and application facing emitter interfaces. + +In the following example, the injectable custom emitter interface is `CustomEmitter`, +and it is implemented by `CustomEmitterImpl`: + +[source,java] +---- +include::{examples-dir}/emitter/EmitterFactoryExamples.java[tags=custom-emitter-declaration,indent=0] +---- + +Note that `CustomEmitter` interface extends `EmitterType`, which is a marker interface for discovering custom emitter types. +Also, `CustomEmitterImpl` implements the `MessagePublisherProvider`, which is used by the framework to transform this emitter to a channel. + +Then we need to provide an implementation of the `EmitterFactory` interface: + +[source,java] +---- +include::{examples-dir}/emitter/EmitterFactoryExamples.java[tags=custom-emitter-factory,indent=0] +---- + +The `CustomEmitterFactory` is a CDI managed bean, which implements the `EmitterFactory`. +It is qualified with `EmitterFactoryFor` annotation which is configured with the emitter interface `CustomEmitter` that this factory provides. + +Smallrye Reactive Messaging discovers the emitter factory during the CDI deployment validation +and verifies that custom emitters used by the application have corresponding emitter factories. +It'll use the emitter factory to create the emitter implementation and will register the implementation into the `ChannelRegistry`. + +Note that the `CustomEmitterFactory` also uses the `ChannelRegistry` and provides the custom emitter with `@Produces`. + +Finally, the application can inject and use the `CustomEmitter` as a normal emitter channel: + +[source,java] +---- +include::{examples-dir}/emitter/EmitterFactoryExamples.java[tags=custom-emitter-usage,indent=0] +---- diff --git a/content/docs/smallrye-reactive-messaging/main/concepts/generic-payloads.adoc b/content/docs/smallrye-reactive-messaging/main/concepts/generic-payloads.adoc new file mode 100644 index 000000000..fd7193fd7 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/concepts/generic-payloads.adoc @@ -0,0 +1,55 @@ += Generic Payloads +:version-root: ../ +include::{version-root}_attributes.adoc[] + +[WARNING] +.Experimental +==== + +Generic payloads are an experimental feature and the API is subject to change. + +==== + +When using reactive messaging, `Message` flow in your system +each message has a payload but can also contain _metadata_, as explained in xref:concepts/concepts.adoc#messages-payload-metadata[Messages, Payload, Metadata]. +The metadata can hold information, for example in an outgoing channel, additional properties of the outgoing message to be sent to the broker. + +It is sometimes preferable to continue using the xref:concepts/model.adoc#messages-vs-payloads[payload signatures], +and also being able to attach metadata. +Using `GenericPayload` allows customizing metadata when handling payloads in SmallRye Reactive Messaging `@Incoming` and `@Outgoing` methods. +`GenericPayload` is a wrapper type, like the `Message`, containing a payload and metadata, +without requiring handling acknowledgments manually. + +[source,java] +---- +include::{examples-dir}/genericpayload/GenericPayloadExample.java[tags=code,indent=0] +---- + +You can combine generic payloads with xref:concepts/incoming-metadata-injection.adoc[metadata injection] : + +[source,java] +---- +include::{examples-dir}/genericpayload/GenericPayloadExample.java[tags=injection,indent=0] +---- + +Note that the metadata provided with the outgoing generic payload is merged with the incoming message metadata. + +`GenericPayload` can also be used with emitters to attach metadata without dealing with `Message` directly: + +[source,java] +---- +include::{examples-dir}/genericpayload/GenericPayloadExample.java[tags=emitter,indent=0] +---- + +The inner payload is extracted and the metadata from the `GenericPayload` is merged into the outgoing message. +Note that existing metadata entries are not overwritten by the `GenericPayload` metadata. +If a metadata entry of the same type already exists, it is preserved unless the metadata class implements `MergeableMetadata`. + +[WARNING] +.Limitations +==== + +While `GenericPayload++<++T++>++` can be used as an incoming payload type, +xref:concepts/converters.adoc[message converters] are not applied to the payload type `T`. + +==== diff --git a/content/docs/smallrye-reactive-messaging/main/concepts/incoming-concurrency.adoc b/content/docs/smallrye-reactive-messaging/main/concepts/incoming-concurrency.adoc new file mode 100644 index 000000000..058eb070f --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/concepts/incoming-concurrency.adoc @@ -0,0 +1,73 @@ += Incoming Channel Concurrency +:version-root: ../ +include::{version-root}_attributes.adoc[] + +[WARNING] +.Experimental +==== + +Incoming channel `concurrency` config is an experimental feature. + +==== + +The `concurrency` attribute for incoming channels provides a mechanism to enable concurrent non-blocking processing of incoming messages. +When applied to a channel, this attribute specifies the number of copies of that channel to be created and wired to the processing method, +allowing multiple messages to be processed concurrently. + +For example, concurrency configuration for a Kafka incoming channel the configuration will look like: + +[source,properties] +---- +mp.messaging.incoming.my-channel.connector=smallrye-kafka +mp.messaging.incoming.my-channel.topic=orders +mp.messaging.incoming.my-channel.value.deserializer=org.apache.kafka.common.serialization.DoubleDeserializer +mp.messaging.incoming.my-channel.concurrency=4 +---- + +In this example, there will be 4 copies of the `my-channel` running concurrently, with distinctive internal channel names, +`my-channel$1`, `my-channel$2`, etc. but all registered with the name `my-channel` to the `ChannelRegistry`. + +[NOTE] +.Kafka connector `partitions` +==== + +This is essentially very similar to the Kafka connector `partitions` configuration, but addresses some its limitations. +Using `partitions` config in Kafka connector, channels are merged into the downstream message processor +(method annotated with `@Incoming` or an injected channel) which is therefore called sequentially. +This prevents concurrently processing messages from multiple partitions. + +The `concurrency` mechanism effectively allows polling Kafka partitions from separate clients +and concurrently processing records while preserving the in-partition order. + +==== + +Copy channels inherit all configuration attributes of the main channel config. +Per-copy channel attributes can be configured separately using the `$` separated channel names: `mp.messaging.incoming.my-channel$1.attribute`. + +For example, the following AMQP 1.0 channel defines 3 channels each with a different selector: + +[source,properties] +---- +mp.messaging.incoming.data.connector=smallrye-amqp +mp.messaging.incoming.data.address=address +mp.messaging.incoming.data.durable=false +mp.messaging.incoming.data.concurrency=3 +mp.messaging.incoming.data$1.selector=x='foo' +mp.messaging.incoming.data$2.selector=x='bar' +mp.messaging.incoming.data$3.selector=x='baz' +---- + +While the `concurrency` attribute is applicable to channels of any connector type, +the channel implementation may need to take this configuration into account and adjust the threading accordingly. +Connectors based on Vert.x event loop create a new event loop context per copy-channel to dispatch messages on distinct contexts. + +[IMPORTANT] +.Non-blocking processing +==== + +Note that while this allows concurrent processing, messages are still dispatched on Vert.x event loop threads, and should not be blocked. + +==== + +Otherwise, connectors treat copy channels as independent channels. +For example, health check reports are registered separately for each copy-channel. diff --git a/content/docs/smallrye-reactive-messaging/main/concepts/incoming-metadata-injection.adoc b/content/docs/smallrye-reactive-messaging/main/concepts/incoming-metadata-injection.adoc new file mode 100644 index 000000000..fb27198b2 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/concepts/incoming-metadata-injection.adoc @@ -0,0 +1,29 @@ += Incoming Metadata Injection +:version-root: ../ +include::{version-root}_attributes.adoc[] + +[WARNING] +.Experimental +==== + +Metadata injection is an experimental feature. + +==== + +When using reactive messaging, `Message` flow in your system. +Each message has a payload but can also contain _metadata_, as explained in xref:concepts/concepts.adoc#messages-payload-metadata[Messages, Payload, Metadata]. + +You can inject the incoming message metadata as a parameter depending on the method signature. +*Only methods receiving the payload as a parameter can also receive the metadata.* +For example, `@Incoming("in") void consume(String s)` can also receive the metadata using the following signature: +`@Incoming("in") void consume(String s, MyMetadata m)`. +Note that the payload must be the first parameter. + +The metadata can be injected using the class of the metadata to inject. +In this case, the method is invoked with `null` if the metadata is missing. +You can also inject the metadata using `Optional++<++MetadataClass++>++,` which would be empty if the incoming message does not contain metadata of type `MetadataClass.` + +[source,java] +---- +include::{examples-dir}/incomings/MetadataInjectionExample.java[tags=code,indent=0] +---- diff --git a/content/docs/smallrye-reactive-messaging/main/concepts/incomings.adoc b/content/docs/smallrye-reactive-messaging/main/concepts/incomings.adoc new file mode 100644 index 000000000..8fc179d3a --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/concepts/incomings.adoc @@ -0,0 +1,20 @@ += Multiple Incoming Channels +:version-root: ../ +include::{version-root}_attributes.adoc[] + +[WARNING] +.Experimental +==== + +Multiple `@Incomings` is an experimental feature. + +==== + +The `@Incoming` annotation is repeatable. It means that the method +receives the messages transiting on every listed channels, in no +specific order: + +[source,java] +---- +include::{examples-dir}/incomings/IncomingsExamples.java[tags=code,indent=0] +---- diff --git a/content/docs/smallrye-reactive-messaging/main/concepts/keyed-multi.adoc b/content/docs/smallrye-reactive-messaging/main/concepts/keyed-multi.adoc new file mode 100644 index 000000000..cc1824499 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/concepts/keyed-multi.adoc @@ -0,0 +1,52 @@ += Using `KeyedMulti` +:version-root: ../ +include::{version-root}_attributes.adoc[] + +[WARNING] +.Experimental +==== + +`KeyedMulti` is an experimental feature. + +==== + +When implementing a data streaming application, it's common to handle messages partitioned using a _key_. +In this case, your stream manipulation often has to 1) group by key and 2) do the manipulation. +Reactive Messaging can do the first step for you and reduce the code complexity. +To do this, it injects `io.smallrye.reactive.messaging.keyed.KeyedMulti` in your method instead of a _bare_ `Multi`. + +For example, imagine the following stream, represented as `key:value`: `"a:1", "b:1", "a:2", "b:2", "b:3"...` +Then, let's consider the following method: + +[source,java] +---- +include::{examples-dir}/keyed/KeyedMultiExample.java[tags=code,indent=0] +---- + +Reactive Messaging automatically extracts the key and value from the incoming stream and invokes the method for each key. +The received `KeyedMulti` represent the stream for each key. +The `key()` method returns the extracted key. + +The key and value can be extracted from the payload but also (and often) from the message's metadata. + +When using Kafka, it automatically extracts the key/value from the Kafka records. +In the other cases, or if you need custom extraction, you can implement your own `io.smallrye.reactive.messaging.keyed.KeyValueExtractor`. +Implementations are exposed `ApplicationScoped` beans, and are used to extract the key and value. +The following implementation extracts the key and value from payloads structured as "key:value": + +[source,java] +---- +include::{examples-dir}/keyed/KeyValueExtractorFromPayload.java[tags=code,indent=0] +---- + +The extractor selection uses the `canExtract` method. +When multiple extractors are available, you can implement the `getPriority()` method to give a lower priority. +Default extractors have the priority 100. +So, if you have a custom extractor with the priority 99, it will be used (if it replies `true` to the `canExtract` call). +In addition, you can use the `io.smallrye.reactive.messaging.keyed.Keyed` annotation to indicate the class of the extractor to use. +The extractor must still be a CDI bean, but the `canExtract` method is not called, and priority does not matter: + +[source,java] +---- +include::{examples-dir}/keyed/KeyedExample.java[tags=code,indent=0] +---- diff --git a/content/docs/smallrye-reactive-messaging/main/concepts/logging.adoc b/content/docs/smallrye-reactive-messaging/main/concepts/logging.adoc new file mode 100644 index 000000000..2a8fc8df3 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/concepts/logging.adoc @@ -0,0 +1,269 @@ += Logging +:version-root: ../ +include::{version-root}_attributes.adoc[] + +SmallRye Reactive Messaging uses https://github.com/jboss-logging[JBoss +Logging] as logging API. This section +explains how to configure the loggers for various logging backends. + +[TIP] +==== + +If you are developing SmallRye Reactive Messaging and wonder about how +the logs are managed, it uses https://jboss-logging.github.io/jboss-logging-tools/#introduction[JBoss Logging +Tools]. + +==== + +== Logging Backends + +SmallRye Reactive Messaging uses the JBoss Logging library to write +messages to a log file. This library is a logging _bridge_ that +integrates different log frameworks. You can decide which of the +following frameworks you want to use for your application: + +* JBoss LogManager (`jboss`) +* Log4j 2 (`log4j2`) +* Log4j 1 (`log4j`) +* Slf4j (`slf4j`) +* JDK logging (`jul`) + +You only need to add the chosen framework to the classpath, and the +JBoss Logging library will pick it up. If there are multiple frameworks +available on the classpath, it picks the first found (in the order from +the list). Alternatively, you can set the `org.jboss.logging.provider` +system property is one of the values given above. + +The concepts and log categories are the same for all frameworks. +However, the format of the configuration file and the names of the log +levels differ. Check the documentation of your logging library to find +out which _dependencies_ are required, the exact name of the log levels, +and where the configuration should be written. + +== Log Categories + +As all applications and frameworks, SmallRye Reactive Messaging writes +log messages in different categories and log levels. The categories +group messages from specific connectors, classes or components. The +following table shows the essential log categories used by SmallRye +Reactive Messaging: + +[width="100%",cols="36%,64%",options="header",] +|=== +|Category |Description +|`io.smallrye.reactive.messaging` |This category contains all the messages written by SmallRye Reactive Messaging. +|`io.smallrye.reactive.messaging.provider` |This category contains all the messages generated by the _core_ (provider). +|`io.smallrye.reactive.messaging.kafka` |This category contains all the messages generated by the Kafka Connector. +|`io.smallrye.reactive.messaging.amqp` |This category contains all the messages generated by the AMQP Connector. +|`io.smallrye.reactive.messaging.jms` |This category contains all the messages generated by the JMS Connector. +|`io.smallrye.reactive.messaging.camel` |This category contains all the messages generated by the Camel Connector. +|`io.smallrye.reactive.messaging.mqtt` |This category contains all the messages generated by the MQTT (Client) Connector. +|`io.smallrye.reactive.messaging.mqtt-server` |This category contains all the messages generated by the MQTT (Server) Connector. +|=== + +The names of the log levels are defined by your logging framework and +determine the amount and granularity of the log messages. You can assign +a log level to each category. If you do not specify a specific +category’s log level, it will inherit the level from its parent +category. Thus, setting the log level of +`io.smallrye.reactive.messaging` influences every loggers from SmallRye +Reactive Messaging. + +== Message Code + +Each message has an identifier code. They are all prefixed with `SRMSG`, +followed with the numeric code. + +In the following output, the code is `SRMSG00229`: + +.... +[2020-06-15 13:35:07] [INFO ] SRMSG00229: Channel manager initializing... +.... + +== Recommended logging configurations + +=== Development + +==== Log4J 1 + +_log4j.properties_ + +[source,properties] +---- +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.Target=System.out +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d{HH:mm:ss,SSS} %-5p [%c] - %m%n + +log4j.rootLogger=info, stdout +log4j.logger.io.smallrye.reactive.messaging=info +log4j.logger.org.jboss.weld=warn +---- + +==== Log4J 2 + +_log4j2.xml_ + +[source,xml] +---- + + + PropertiesConfiguration + + + + + + [%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %c{1} - %msg%n + > + + + + + + + + + + + + + + + +---- + +==== JDK (JUL) + +_logging.properties_ + +[source,properties] +---- +handlers=java.util.logging.ConsoleHandler + +java.util.logging.ConsoleHandler.level=FINEST +java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter +java.util.logging.SimpleFormatter.format=[%1$tF %1$tT] [%4$-7s] %5$s %n + +.level=INFO +io.smallrye.reactive.messaging.level=INFO +org.jboss.weld.level=WARNING +---- + +==== LogBack via SLF4J++*++ + +_logback.xml_ + +[source,xml] +---- + + + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + +---- + +=== Production + +==== Log4J 1 + +_log4j.properties_ + +[source,properties] +---- +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.Target=System.out +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d{HH:mm:ss,SSS} %-5p [%c] - %m%n + +log4j.rootLogger=info, stdout +log4j.logger.io.smallrye.reactive.messaging=warn +log4j.logger.org.jboss.weld=error +---- + +==== Log4J 2 + +_log4j2.xml_ + +[source,xml] +---- + + + PropertiesConfiguration + + + + + + [%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %c{1} - %msg%n + > + + + + + + + + + + + + + + + +---- + +==== JDK (JUL) + +_logging.properties_ + +[source,properties] +---- +handlers=java.util.logging.ConsoleHandler + +java.util.logging.ConsoleHandler.level=INFO +java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter +java.util.logging.SimpleFormatter.format=[%1$tF %1$tT] [%4$-7s] %5$s %n + +.level=INFO +io.smallrye.reactive.messaging.level=WARNING +org.jboss.weld.level=SEVERE +---- + +_logback.xml_ + +[source,xml] +---- + + + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + +---- diff --git a/content/docs/smallrye-reactive-messaging/main/concepts/merge.adoc b/content/docs/smallrye-reactive-messaging/main/concepts/merge.adoc new file mode 100644 index 000000000..d7bd6cbd7 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/concepts/merge.adoc @@ -0,0 +1,47 @@ += Merge channels +:version-root: ../ +include::{version-root}_attributes.adoc[] + +[WARNING] +.Experimental +==== + +`@Merge` is an experimental feature. + +==== + +By default, messages transiting in a channel can arise from a single +producer. Having multiple producers is considered erroneous and is +reported at deployment time. + +The link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/io/smallrye/reactive/messaging/annotations/Merge.html[`Merge`] +annotation changes this behavior and indicates that a channel can have +multiple producers. `@Merge` must be used with the `@Incoming` +annotation: + +[source,java] +---- +include::{examples-dir}/merge/MergeExamples.java[tags=chain,indent=0] +---- + +In the previous example, the consumer gets all the messages (from both +producers). + +The `@Merge` annotation allows configuring how the incoming messages +(from the different producers) are merged into the channel. The `mode` +attribute allows configuring this behavior: + +* `ONE` picks a single producer, discarding the other producer; +* `MERGE` (default) gets all the messages as they come, without any +defined order. Messages from different producers may be interleaved. +* `CONCAT` concatenates the producers. The messages from one producer +are received until the messages from other producers are received. + +[NOTE] +==== + +Outbound connectors also support a `merge` attribute that allows +consuming the messages to multiple upstreams. It will dispatch all the +received messages. + +==== diff --git a/content/docs/smallrye-reactive-messaging/main/concepts/message-context.adoc b/content/docs/smallrye-reactive-messaging/main/concepts/message-context.adoc new file mode 100644 index 000000000..411587683 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/concepts/message-context.adoc @@ -0,0 +1,83 @@ += Message Contexts +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Message context provides a way to propagate data along the processing of a message. +It can be used to propagate message specific objects in an implicit manner and be able to retrieve them later, such as the user, session or transaction. + +[IMPORTANT] +==== + +Message contexts are only support by Kafka, AMQP, RabbitMQ and MQTT connectors. + +==== + +[NOTE] +==== + +Message context support is an experimental and SmallRye only feature. + +==== + +== What's a message context + +A message context is execution context on which a message is processed. +Each stage of the processing is going to use the same execution context. +Thus, it allows storing _data_ which can later be restored. +For example, you can imagine storing some authentication (`User` in the following example) data in one part of your processing and restore it in a later stage. + +[source,java] +---- +@Incoming("data") +@Outgoing("process") +public Message process(Message input) { + // Extract some data from the message and store it in the context + User user = ...; + // Store the extracted data into the message context. + ContextLocals.put("user", user); + record input; +} + +@Incoming("process") +@Outgoing("after-process") +public String handle(String payload) { + // You can retrieve the store data using + User user = ContextLocals.get("user", null); + + // ... + return payload; +} +---- + +The Message context is also available when using blocking or asynchronous stages (stage returning `Uni` or `CompletionStage`) + +== The difference with metadata + +Message metadata can be used to provide a similar feature. +However, it requires using `Messages` which can be inconvenient (need to handle the acknowledgement manually). +Message Contexts provide a simpler API, closer to a _Message CDI scope_: you can save data, and restore it later. +The implicit propagation avoid having to deal with `Messages`. + +== Supported signatures + +Message context works with: + +* methods consuming or producing `Messages`, `Uni++<++Message++<++T++>>++` and `CompletionStage++<++Message++<++T++>>++` +* methods consuming or producing payloads, `Uni++<++Payload++>++` and `CompletionStage++<++Payload++>++`. +* blocking and non-blocking methods + +However, message context are *NOT* enforced when using methods consuming or producing: + +* `Multi`, `Flow.Publisher`, `Publisher` and `PublisherBuilder` +* `Subscriber`, `Flow.Subscriber`, and `SubscriberBuilder` +* `Processor`, `Flow.Processor`, and `ProcessorBuilder` + +== Under the hood + +Under the hood, the message context feature uses Vert.x _duplicated contexts_. +A duplicated context is a view of the "root" (event loop) context, which is restored at each stage of the message processing. + +Each time that a compatible connector receives a message from a broker, it creates a new _duplicated context_ and attaches it to the message. +So the context is stored in the metadata of the message. + +When the message is processed, SmallRye Reactive Messaging makes sure that this processing is executed on the stored duplicated context. diff --git a/content/docs/smallrye-reactive-messaging/main/concepts/model.adoc b/content/docs/smallrye-reactive-messaging/main/concepts/model.adoc new file mode 100644 index 000000000..c7adc4e70 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/concepts/model.adoc @@ -0,0 +1,422 @@ += Development Model +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Reactive Messaging proposes a CDI-based programming model to implement event-driven applications. +Following the CDI principles, _beans_ are forming the main building block of your application. +Reactive Messaging provides a set of annotations and types to implement beans that generate, consume or process messages. + +== @Incoming and @Outgoing + +Reactive Messaging provides two main annotations: + +* link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/org/eclipse/microprofile/reactive/messaging/Incoming.html[`org.eclipse.microprofile.reactive.messaging.Incoming`] - indicates the consumed channel +* link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/org/eclipse/microprofile/reactive/messaging/Outgoing.html[`org.eclipse.microprofile.reactive.messaging.Outgoing`] - indicates the populated channel + +These annotations are used on _methods_: + +[source,java] +---- +include::{examples-dir}/beans/MessageProcessingBean.java[indent=0] +---- + +[NOTE] +==== + +Reactive Messaging beans can either be in the _application_ scope (`@ApplicationScoped`) or dependent scope (`@Dependent`). + +==== + +Manipulating messages can be cumbersome. +When you are only interested in the payload, you can use the following syntax: The following code is equivalent to the snippet from above: + +[source,java] +---- +include::{examples-dir}/beans/PayloadProcessingBean.java[indent=0] +---- + +[IMPORTANT] +==== + +You should not call methods annotated with `@Incoming` and/or +`@Outgoing` directly from your code. They are invoked by the framework. +Having user code invoking them would not have the expected outcome. + +==== + +SmallRye Reactive Messaging automatically binds matching `@Outgoing` to +`@Incoming` to form a chain: + +.A chain of components +image::images/chain.png[A chain of components] + +If we consider the following code: + +[source,java] +---- +include::{examples-dir}/beans/Chain.java[tags=chain,indent=0] +---- + +It would generate the following chain: + +[source,text] +---- +generate --> [ source ] --> process --> [ sink ] --> consume +---- + +Methods annotated with `@Incoming` or `@Outgoing` don’t have to be in the same _bean_ (_class_). +You can distribute them among a set of beans. +Remote interactions are also possible when using connectors. + +Methods annotated with: + +* only `@Outgoing` are used to generate messages or payloads +* only `@Incoming` are used to consume messages or payloads +* both `@Incoming` and `@Outgoing` are used to process messages or payloads; or transform the stream + +== Creating messages + +Messages are envelopes around payload. +They are the vehicle. +While manipulating payload is convenient, messages let you add metadata, handle acknowledgement... + +Creating `Messages` is done using the link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/io/eclipse/microprofile/reactive/messaging/Message.html[`Message`] interface directly: + +[source,java] +---- +include::{examples-dir}/messages/MessageExamples.java[tags=creation,indent=0] +---- + +Messages accept `null` as payload. +Channels connected to outbound connectors interpret messages with `null` payload differently depending on the technology. + +[source,java] +---- +include::{examples-dir}/messages/MessageExamples.java[tags=null-payload,indent=0] +---- + +You can also create new instance of `Message` from an existing one: + +[source,java] +---- +include::{examples-dir}/messages/MessageExamples.java[tags=copy,indent=0] +---- + +[WARNING] +.Acknowledgement? +==== + +Acknowledgement is an important part of messaging systems. This will be covered in the xref:concepts/acknowledgement.adoc[acknowledgement] section. + +==== + +[NOTE] +.Connector Metadata +==== + +Most connectors are providing metadata to let you extract technical +details about the message, but also customize the outbound dispatching. + +==== + +== Messages vs. Payloads + +Reactive messaging offers flexibility when it comes to handling messages and their acknowledgements. +The application developer can choose to finely handle acknowledgements per-message basis, by handling the +`Message`-based signatures. Otherwise, when handling payloads, acknowledgements +(and negative-acknowledgements) are handled by the framework. +The following sections in this documentation detail both development models. + +While being the easier development model, in the past using payload-based signatures did not allow associating connector-specific metadata, +making the `Message`-based signatures the de-facto choice even for the most common scenarios. +This lead to using the connector custom-message implementation types, +such as `IncomingKafkaRecord`, `KafkaRecord` or `IncomingRabbitMQMessage`, +as a convenience for accessing connector-specific metadata. + +Not only this forces to handle acknowledgements manually, +it also doesn't allow for incoming or outgoing `Messages` to be xref:concepts/decorators.adoc#intercepting-incoming-and-outgoing-messages[intercepted] +or xref:concepts/observability.adoc[observed]. + +[WARNING] +.Custom `Message` types & Message interception +==== + +Custom `Message` types such as `IncomingKafkaRecord`, `KafkaRecord` or `IncomingRabbitMQMessage` +are not compatible with features intercepting messages. +Therefore, it is no longer recommended to use custom `Message` implementations in consumptions methods. +Instead, you can either use the generic `Message` type and access specific metadata, +or use the payload with xref:concepts/incoming-metadata-injection.adoc[metadata injection] + +==== + +Since xref:concepts/incoming-metadata-injection.adoc[incoming metadata injection] and xref:concepts/generic-payloads.adoc[generic payloads] +features added to SmallRye Reactive Messaging, +it is easier to use payload signatures and benefit from acknowledgement handling and still access connector-specific metadata. + +== Generating Messages + +To produce messages to a channel, you need to use the `@Outgoing` +annotation. This annotation takes a single parameter: the name of the +populated channel. + +=== Generating messages synchronously + +You can generate messages synchronously. In this case, the method is +called for every _request_ from the downstream: + +[source,java] +---- +include::{examples-dir}/generation/GenerationExamples.java[tags=message-sync,indent=0] +---- + +[NOTE] +.Requests? +==== + +Reactive Messaging connects components to build a reactive stream. +In a reactive stream, the emissions are controlled by the consumer +(downstream) indicating to the publisher (upstream) how many items it +can consume. With this protocol, the consumers are never flooded. + +==== + +=== Generating messages using CompletionStage + +You can also return a `CompletionStage` / `CompletableFuture`. +In this case, Reactive Messaging waits until the `CompletionStage` gets completed before calling it again. + +For instance, this signature is useful to poll messages from a source using an asynchronous client: + +[source,java] +---- +include::{examples-dir}/generation/GenerationExamples.java[tags=message-cs,indent=0] +---- + +=== Generating messages using Uni + +You can also return a https://smallrye.io/smallrye-mutiny/#_uni_and_multi[Uni] instance. +In this case, Reactive Messaging waits until the `Uni` emits its item before calling it again. + +This signature is useful when integrating asynchronous clients providing a Mutiny API. + +[source,java] +---- +include::{examples-dir}/generation/GenerationExamples.java[tags=message-uni,indent=0] +---- + +=== Generating Reactive Streams of messages + +Instead of producing the message one by one, you can return the stream +directly. If you have a data source producing Reactive Streams +`Publisher` (or sub-types, such as `Multi`), this is the signature you are looking for: + +[source,java] +---- +include::{examples-dir}/generation/GenerationExamples.java[tags=message-stream,indent=0] +---- + +In this case, the method is called once to retrieve the `Publisher`. + +== Generating Payloads + +Instead of `Message`, you can produce _payloads_. In this case, Reactive +Messaging produces a simple message from the _payload_ using +`Message.of`. + +=== Generating payload synchronously + +You can produce _payloads_ synchronously. The framework calls the method +upon request and create `Messages` around the produced payloads. + +[source,java] +---- +include::{examples-dir}/generation/GenerationExamples.java[tags=payload-sync,indent=0] +---- + +=== Generating payload using CompletionStage + +You can also return `CompletionStage` or `CompletableFuture`. For +example, if you have an asynchronous client returning `CompletionStage`, +you can use it as follows, to poll the data one by one: + +[source,java] +---- +include::{examples-dir}/generation/GenerationExamples.java[tags=payload-cs,indent=0] +---- + +=== Generating payload by producing Unis + +You can also return a `Uni` if you have a client using Mutiny types: + +[source,java] +---- +include::{examples-dir}/generation/GenerationExamples.java[tags=payload-uni,indent=0] +---- + +=== Generating Reactive Streams of payloads + +Finally, you can return a `Publisher` (or a sub-type such as a `Multi`): + +[source,java] +---- +include::{examples-dir}/generation/GenerationExamples.java[tags=payload-stream,indent=0] +---- + +In this case, Reactive Messaging calls the method only once to retrieve +the `Publisher`. + +== Consuming Messages + +To consume messages from a channel, you need to use the `@Incoming` +annotation. This annotation takes a single parameter: the name of the +consumed channel. + +Because `Messages` must be acknowledged, consuming messages requires +returning _asynchronous results_ that would complete when the incoming +message get acknowledged. + +For example, you can receive the `Message`, process it and return the +acknowledgement as result: + +[source,java] +---- +include::{examples-dir}/consumption/ConsumptionExamples.java[tags=message-cs,indent=0]' +---- + +You can also return a `Uni` if you need to implement more complicated +processing: + +[source,java] +---- +include::{examples-dir}/consumption/ConsumptionExamples.java[tags=message-uni,indent=0] +---- + +== Consuming Payloads + +Unlike consuming messages, consuming payloads support both synchronous +and asynchronous consumption. + +For example, you can consume a payload as follows: + +[source,java] +---- +include::{examples-dir}/consumption/ConsumptionExamples.java[tags=payload-sync,indent=0] +---- + +In this case, you don’t need to deal with the acknowledgement yourself. +The framework acknowledges the incoming message (that wrapped the +payload) once your method returns successfully. + +If you need to achieve asynchronous actions, you can return a +`CompletionStage` or a `Uni`: + +[source,java] +---- +include::{examples-dir}/consumption/ConsumptionExamples.java[tags=payload-cs,indent=0] +---- + +[source,java] +---- +include::{examples-dir}/consumption/ConsumptionExamples.java[tags=payload-uni,indent=0] +---- + +In these 2 cases, the framework acknowledges the incoming message when +the returned construct gets _completed_. + +== Processing Messages + +You can process `Message` both synchronously or asynchronously. +This later case is useful when you need to execute an asynchronous action during your processing such as invoking a remote service. + +Do process `Messages` synchronously uses: + +[source,java] +---- +include::{examples-dir}/processing/ProcessingExamples.java[tags=process-message,indent=0] +---- + +This method transforms the `int` payload to a `String`, and wraps it +into a `Message`. + +'''important "Using `Message.withX` methods" +You may be surprised by the usage of `Message.withX` methods. It allows +metadata propagation as the metadata would be copied from the incoming +message and so dispatched to the next method. + +You can also process `Messages` asynchronously: + +[source,java] +---- +include::{examples-dir}/processing/ProcessingExamples.java[tags=process-message-cs,indent=0] +---- + +Or using Mutiny: + +[source,java] +---- +include::{examples-dir}/processing/ProcessingExamples.java[tags=process-message-uni,indent=0] +---- + +In general, you want to create the new `Message` from the incoming one. +It enables metadata propagation and post-acknowledgement. For this, use +the `withX` method from the `Message` class returning a new `Message` +instance but copy the _content_ (metadata, ack/nack...). + +== Processing payloads + +If you don’t need to manipulate the envelope, you can process payload +directly either synchronously or asynchronously: + +[source,java] +---- +include::{examples-dir}/processing/ProcessingExamples.java[tags=process-payload,indent=0] +---- + +[source,java] +---- +include::{examples-dir}/processing/ProcessingExamples.java[tags=process-payload-cs,indent=0] +---- + +[source,java] +---- +include::{examples-dir}/processing/ProcessingExamples.java[tags=process-payload-uni,indent=0] +---- + +[NOTE] +.What about metadata? +==== + +With these methods, the metadata are automatically propagated. + +==== + +== Processing streams + +The previous processing method were taking single `Message` or payload. +Sometimes you need more advanced manipulation. For this, SmallRye +Reactive Messaging lets you process the stream of `Message` or the +stream of payloads directly: + +[source,java] +---- +include::{examples-dir}/processing/StreamExamples.java[tags=processing-stream-message,indent=0] +---- + +[source,java] +---- +include::{examples-dir}/processing/StreamExamples.java[tags=processing-stream-payload,indent=0] +---- + +You can receive either a (Reactive Streams) `Publisher`, a +`PublisherBuilder` or (Mutiny) `Multi`. You can return any subclass of +`Publisher` or a `Publisher` directly. + +[IMPORTANT] +==== + +These signatures do not support metadata propagation. In the case of a +stream of `Message`, you need to propagate the metadata manually. In the +case of a stream of payload, propagation is not supported, and incoming +metadata are lost. + +==== diff --git a/content/docs/smallrye-reactive-messaging/main/concepts/observability.adoc b/content/docs/smallrye-reactive-messaging/main/concepts/observability.adoc new file mode 100644 index 000000000..aa57e8238 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/concepts/observability.adoc @@ -0,0 +1,38 @@ += Observability API +:version-root: ../ +include::{version-root}_attributes.adoc[] + +[IMPORTANT] +==== + +Observability API is experimental and SmallRye only feature. + +==== + +Smallrye Reactive Messaging proposes an observability API that allows to observe messages received and send through inbound and outbound channels. + +For any observation to happen, you need to provide an implementation of the `MessageObservationCollector`, discovered as a CDI-managed bean. + +At wiring time the discovered `MessageObservationCollector` implementation `initObservation` method is called once per channel to initialize the `ObservationContext`. +The default `initObservation` implementation returns a default `ObservationContext` object, +but the collector implementation can provide a custom per-channel `ObservationContext` object that'll hold information necessary for the observation. +The `ObservationContext#complete` method is called each time a message observation is completed – message being acked or nacked. +The collector implementation can decide at initialization time to disable the observation per channel by returning a `null` observation context. + +For each new message, the collector is on `onNewMessage` method with the channel name, the `Message` and the `ObservationContext` object initialized beforehand. +This method can react to the creation of a new message but also is responsible for instantiating and returning a `MessageObservation`. +While custom implementations can augment the observability capability, SmallRye Reactive Messaging provides a default implementation `DefaultMessageObservation`. + +So a simple observability collector can be implemented as such: + +[source,java] +---- +include::{examples-dir}/observability/SimpleMessageObservationCollector.java[indent=0] +---- + +A collector with a custom `ObservationContext` can be implemented as such : + +[source,java] +---- +include::{examples-dir}/observability/ContextMessageObservationCollector.java[indent=0] +---- diff --git a/content/docs/smallrye-reactive-messaging/main/concepts/outgoings.adoc b/content/docs/smallrye-reactive-messaging/main/concepts/outgoings.adoc new file mode 100644 index 000000000..64adca658 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/concepts/outgoings.adoc @@ -0,0 +1,79 @@ += Multiple Outgoing Channels +:version-root: ../ +include::{version-root}_attributes.adoc[] + +[WARNING] +.Experimental +==== + +Multiple `@Outgoings` is an experimental feature. + +==== + +The `@Outgoing` annotation is repeatable. It means that the method +dispatches outgoing messages to multiple listed channels: + +[source,java] +---- +include::{examples-dir}/outgoings/OutgoingsExample.java[tags=code,indent=0] +---- + +The default behaviour is same as the xref:concepts/broadcast.adoc[@Broadcast annotation], +meaning that outbound messages are dispatched to all listed outgoing channels. + +However, different dispatching mechanism can be employed: + +== Selectively dispatching messages using `Targeted` messages + +You can selectively dispatch messages to multiple outgoings by returning +link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/io/smallrye/reactive/messaging/Targeted.html[`Targeted`] : + +[source,java] +---- +include::{examples-dir}/outgoings/TargetedExample.java[tags=code,indent=0] +---- + +In this example, three outgoing channels are declared on the `process` method +but in some condition channel `out3` does not receive any messages. + +[IMPORTANT] +.Coordinated acknowledgements +==== + +`Targeted` return types coordinate acknowledgements between outgoing messages +and the incoming message, therefore the incoming message will be ack'ed only +when all outgoing messages are ack'ed. + +==== + +In cases where you need to consume `Message` and handle metadata propagation +more finely you can use link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/io/smallrye/reactive/messaging/TargetedMessages.html[`TargetedMessages`] +which is a `Message` type: + +[source,java] +---- +include::{examples-dir}/outgoings/TargetedExample.java[tags=targeted-messages,indent=0] +---- + +Note that in this case coordinated acknowledgements is handled explicitly +using link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/io/smallrye/reactive/messaging/Messages.html[`Messages`] utility. + +== Branching outgoing channels with `MultiSplitter` + +In stream transformer processors it can be useful to branch out an incoming +stream into different sub-streams, based on some conditions. + +When consuming a `Multi`, you can use the `Multi.split` +(see https://smallrye.io/smallrye-mutiny/latest/guides/multi-split/[Mutiny documentation]) +operation to define multiple branches. +The stream transformer method with multiple outgoings must return a +link:https://javadoc.io/doc/io.smallrye.reactive/mutiny/{javadoc-version}/io/smallrye/mutiny/operators/multi/split/MultiSplitter.html[`MultiSplitter`]. + +[source,java] +---- +include::{examples-dir}/outgoings/SplitterMultiExample.java[tags=code,indent=0] +---- + +In this case the number of outgoing channels must match the number of branches given to `split` operation. +Outgoing channels will be tried to be matched to branch identifier enum `toString` ignoring case. +If not all branches are matched, it will fall back to one-by-one matching depending on the order of outgoing channel declarations and enum ordinals. diff --git a/content/docs/smallrye-reactive-messaging/main/concepts/pausable-channels.adoc b/content/docs/smallrye-reactive-messaging/main/concepts/pausable-channels.adoc new file mode 100644 index 000000000..2aeae29c4 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/concepts/pausable-channels.adoc @@ -0,0 +1,64 @@ += Pausable Channels +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Based on reactive streams, Smallrye Reactive Messaging ensures that channels are back-pressured. +This means that the flow of messages is controlled by the downstream consumer, +whether it is a processing method or outgoing channel. +Sometimes you may want to pause the flow of messages, for example, when the consumer is not ready to process them. + +Injected xref:concepts/emitter.adoc#retrieving-channels[`@Channel`] streams are not subscribed to by default, so the flow of messages is controlled by the application. +But for xref:concepts/model.adoc#incoming-and-outgoing[`@Incoming`] methods, the flow of messages is controlled by the runtime. + +Pausable channels are useful when you want to control the flow of messages within your application. + +== Creating a Pausable Channel + +To use pausable channels, you need to activate it with the configuration property `pausable` set to `true`. + +[source,properties] +---- +mp.messaging.incoming.my-channel.pausable=true +---- + +=== Configuration Options + +Pausable channels support the following configuration options: + +* `pausable.initially-paused` - Whether the channel starts in a paused state (default: `false`) +* `pausable.late-subscription` - Whether to subscribe to the upstream after pausing (default: `false`) +* `pausable.buffer-size` - Maximum buffer size for items received while the channel is paused (optional) +* `pausable.buffer-strategy` - Strategy for handling already requested items that arrive while paused. Possible values are: +** `BUFFER` - Buffer items received while paused and deliver them when resumed (default) +** `DROP` - Drop already requested items received while paused +** `IGNORE` - Let already requested items flow through even while paused + +Example configuration: + +[source,properties] +---- +mp.messaging.incoming.my-channel.pausable=true +mp.messaging.incoming.my-channel.pausable.initially-paused=true +mp.messaging.incoming.my-channel.pausable.buffer-size=100 +mp.messaging.incoming.my-channel.pausable.buffer-strategy=BUFFER +mp.messaging.incoming.my-channel.pausable.late-subscription=false +---- + +== Controlling the flow of messages + +If a channel is configured to be pausable, +you can inject the `PausableChannel` qualified with the `@Channel("channel-name")` annotation to control the flow of messages. +You can also get the `PausableChannel` by channel name from the `ChannelRegistry` programmatically, +and pause or resume the channel as needed: + +[source,java] +---- +include::{examples-dir}/pausable/PausableController.java[indent=0] +---- + +[WARNING] +==== + +Pausable channels only work with back-pressure aware subscribers, with bounded downstream requests. + +==== diff --git a/content/docs/smallrye-reactive-messaging/main/concepts/signatures.adoc b/content/docs/smallrye-reactive-messaging/main/concepts/signatures.adoc new file mode 100644 index 000000000..ef35c854f --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/concepts/signatures.adoc @@ -0,0 +1,105 @@ += Supported signatures +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The following tables list the supported method signatures and indicate +the various supported features. For instance, they indicate the default +and available acknowledgement strategies (when applicable). + +== Method signatures to generate data + +[width="100%",cols="32%,68%",options="header",] +|=== +|Signature |Invocation time +|`@Outgoing Publisher++<++Message++<++O++>>++ method()` `{vbar} Called once at ++*++assembly++*++ time {vbar} {vbar}`@Outgoing Publisher method()`` |Called once at _assembly_ time +|`@Outgoing Multi++<++Message++<++O++>>++ method()` `{vbar} Called once at ++*++assembly++*++ time {vbar} {vbar}`@Outgoing Multi method()`` |Called once at _assembly_ time +|`@Outgoing Flow.Publisher++<++Message++<++O++>>++ method()` `{vbar} Called once at ++*++assembly++*++ time {vbar} {vbar}`@Outgoing Flow.Publisher method()`` |Called once at _assembly_ time +|`@Outgoing PublisherBuilder++<++Message++<++O++>>++ method()` `{vbar} Called once at ++*++assembly++*++ time {vbar} {vbar}`@Outgoing PublisherBuilder method()`` |Called once at _assembly_ time +|`@Outgoing Message++<++O++>++ method()` `{vbar} Called for every downstream request, sequentially {vbar} {vbar}`@Outgoing O method()`` |Called for every downstream request, sequentially +|`@Outgoing CompletionStage++<++Message++<++O++>>++ method()` `{vbar} Called for every downstream request, sequentially (After the completion of the last returned CompletionStage) {vbar} {vbar}`@Outgoing CompletionStage method()`` |Called for every downstream request, , sequentially (After the completion of the last returned CompletionStage) +|`@Outgoing Uni++<++Message++<++O++>>++ method()` `{vbar} Called for every downstream request, sequentially (After the completion of the last returned Uni) {vbar} {vbar}`@Outgoing Uni method()`` |Called for every downstream request, , sequentially (After the completion of the last returned Uni) +|=== + +== Method signatures to consume data + +[width="100%",cols="37%,32%,31%",options="header",] +|=== +|Signature |Invocation time |Supported Acknowledgement Strategies +|`@Incoming void method(I p)` |Called for every incoming payload (sequentially) |_POST++_++PROCESSING_, NONE, PRE++_++PROCESSING +|`@Incoming CompletionStage++<++?++>++ method(Message++<++I++>++ msg)` |Called for every incoming message (sequentially) |_MANUAL_, NONE, PRE++_++PROCESSING +|`@Incoming CompletionStage++<++?++>++ method(I p)` |Called for every incoming payload (sequentially) |_POST++_++PROCESSING_, PRE++_++PROCESSING, NONE +|`@Incoming Uni++<++?++>++ method(Message++<++I++>++ msg)` |Called for every incoming message (sequentially) |_MANUAL_, NONE, PRE++_++PROCESSING +|`@Incoming Uni++<++?++>++ method(I p)` |Called for every incoming payload (sequentially) |_POST++_++PROCESSING_, PRE++_++PROCESSING, NONE +|`@Incoming Subscriber++<++Message++<++I++>>++ method()` |Called once at _assembly_ time |_MANUAL_, POST++_++PROCESSING, NONE, PRE++_++PROCESSING +|`@Incoming Subscriber++<++I++>++ method()` |Called once at _assembly_ time |_POST++_++PROCESSING_, NONE, PRE++_++PROCESSING +|`@Incoming Flow.Subscriber++<++Message++<++I++>>++ method()` |Called once at _assembly_ time |_MANUAL_, POST++_++PROCESSING, NONE, PRE++_++PROCESSING +|`@Incoming Flow.Subscriber++<++I++>++ method()` |Called once at _assembly_ time |_POST++_++PROCESSING_, NONE, PRE++_++PROCESSING +|`@Incoming SubscriberBuilder++<++Message++<++I++>++, ?++>++ method()` |Called once at _assembly_ time |_MANUAL_, POST++_++PROCESSING, NONE, PRE++_++PROCESSING +|`@Incoming SubscriberBuilder++<++I, ?++>++ method()` |Called once at _assembly_ time |_MANUAL_, POST++_++PROCESSING, NONE, PRE++_++PROCESSING +|=== + +== Method signatures to process data + +[width="99%",cols="37%,23%,30%,10%",options="header",] +|=== +|Signature |Invocation time |Supported Acknowledgement Strategies |Metadata Propagation +|`@Outgoing @Incoming Message++<++O++>++ method(Message++<++I++>++ msg)` |Called for every incoming message (sequentially) |POST++_++PROCESSING (Smallrye only), _MANUAL_, NONE, PRE++_++PROCESSING |manual +|`@Outgoing @Incoming Message++<++O++>++ method(I payload)` |Called for every incoming message (sequentially) |_POST++_++PROCESSING_ (Smallrye only), NONE, PRE++_++PROCESSING |automatic +|`@Outgoing @Incoming O method(I payload)` |Called for every incoming payload (sequentially) |_POST++_++PROCESSING_, NONE, PRE++_++PROCESSING |automatic +|`@Outgoing @Incoming CompletionStage++<++Message++<++O++>>++ method(Message++<++I++>++ msg)` |Called for every incoming message (sequentially) |_MANUAL_, NONE, PRE++_++PROCESSING |manual +|`@Outgoing @Incoming CompletionStage++<++O++>++ method(I payload)` |Called for every incoming payload (sequentially) |_POST++_++PROCESSING_, NONE, PRE++_++PROCESSING |automatic +|`@Outgoing @Incoming CompletionStage++<++Message++<++O++>>++ method(I payload)` |Called for every incoming payload (sequentially) |_POST++_++PROCESSING_ (Smallrye only), NONE, PRE++_++PROCESSING |automatic +|`@Outgoing @Incoming Uni++<++Message++<++O++>>++ method(Message++<++I++>++ msg)` |Called for every incoming message (sequentially) |_MANUAL_, NONE, PRE++_++PROCESSING |manual +|`@Outgoing @Incoming Uni++<++Message++<++O++>>++ method(I payload)` |Called for every incoming payload (sequentially) |_POST++_++PROCESSING_ (Smallrye only), NONE, PRE++_++PROCESSING |automatic +|`@Outgoing @Incoming Uni++<++O++>++ method(I payload)` |Called for every incoming payload (sequentially) |_POST++_++PROCESSING_, NONE, PRE++_++PROCESSING |automatic +|`@Outgoing @Incoming Processor++<++Message++<++I++>++, Message++<++O++>>++ method()` |Called once at _assembly_ time |_MANUAL_, PRE++_++PROCESSING, NONE |manual +|`@Outgoing @Incoming Processor++<++I, O++>++ method()` |Called once at _assembly_ time |_PRE++_++PROCESSING_, NONE |not supported +|`@Outgoing @Incoming Flow.Processor++<++Message++<++I++>++, Message++<++O++>>++ method()` |Called once at _assembly_ time |_MANUAL_, PRE++_++PROCESSING, NONE |manual +|`@Outgoing @Incoming Flow.Processor++<++I, O++>++ method()` |Called once at _assembly_ time |_PRE++_++PROCESSING_, NONE |not supported +|`@Outgoing @Incoming ProcessorBuilder++<++Message++<++I++>++, Message++<++O++>>++ method()` |Called once at _assembly_ time |_MANUAL_, PRE++_++PROCESSING, NONE |manual +|`@Outgoing @Incoming ProcessorBuilder++<++I, O++>++ method()` |Called once at _assembly_ time |_PRE++_++PROCESSING_, NONE |not supported +|`@Outgoing @Incoming Publisher++<++Message++<++O++>>++ method(Message++<++I++>++ msg)` |Called for every incoming message (sequentially) |_MANUAL_, PRE++_++PROCESSING, NONE |manual +|`@Outgoing @Incoming Publisher++<++O++>++ method(I payload)` |Called for every incoming payload (sequentially) |_PRE++_++PROCESSING_, POST++_++PROCESSING, NONE |automatic +|`@Outgoing @Incoming Multi++<++Message++<++O++>>++ method(Message++<++I++>++ msg)` |Called for every incoming message (sequentially) |_MANUAL_, PRE++_++PROCESSING, NONE |manual +|`@Outgoing @Incoming Multi++<++O++>++ method(I payload)` |Called for every incoming payload (sequentially) |_PRE++_++PROCESSING_, POST++_++PROCESSING, NONE |automatic +|`@Outgoing @Incoming Flow.Publisher++<++Message++<++O++>>++ method(Message++<++I++>++ msg)` |Called for every incoming message (sequentially) |_MANUAL_, PRE++_++PROCESSING, NONE |manual +|`@Outgoing @Incoming Flow.Publisher++<++O++>++ method(I payload)` |Called for every incoming payload (sequentially) |_PRE++_++PROCESSING_, POST++_++PROCESSING, NONE |automatic +|`@Outgoing @Incoming PublisherBuilder++<++Message++<++O++>>++ method(Message++<++I++>++ msg)` |Called for every incoming message (sequentially) |_MANUAL_, PRE++_++PROCESSING, NONE |manual +|`@Outgoing @Incoming PublisherBuilder++<++O++>++ method(I payload)` |Called for every incoming payload (sequentially) |_PRE++_++PROCESSING_, POST++_++PROCESSING, NONE |automatic +|=== + +Note that in additional to the MicroProfile Reactive Messaging specification, +SmallRye Reactive Messaging supports the post-processing acknowledgment handling with automatic metadata propagation for the following signatures: + +* `@Outgoing @Incoming Message++<++O++>++ method(I payload)` +* `@Outgoing @Incoming CompletionStage++<++Message++<++O++>>++ method(I payload)` +* `@Outgoing @Incoming Uni++<++Message++<++O++>>++ method(I payload)` +* `@Outgoing @Incoming Message++<++O++>++ method(Message++<++I++>++ payload)` : For this signature, the post-processing acknowledgment handling is limited. +It covers cases for nacking incoming messages on caught exceptions at the method body, acking incoming messages when outgoing message is skipped by returning `null`, and chaining acknowlegment from outgoing message to the incoming. +However, if the incoming message has already been (n)acked, you will experience duplicate (n)acks. + +== Method signatures to manipulate streams + +[width="100%",cols="52%,17%,20%,11%",options="header",] +|=== +|Signature |Invocation time |Supported Acknowledgement Strategies |Metadata Propagation +|`@Outgoing @Incoming Publisher++<++Message++<++O++>>++ method(Publisher++<++Message++<++I++>>++ pub)` |Called once at _assembly_ time |_MANUAL_, NONE, PRE++_++PROCESSING |manual +|`@Outgoing @Incoming Publisher++<++O++>++ method(Publisher++<++I++>++ pub)` |Called once at _assembly_ time |_PRE++_++PROCESSING_, NONE |not supported +|`@Outgoing @Incoming Multi++<++Message++<++O++>>++ method(Multi++<++Message++<++I++>>++ pub)` |Called once at _assembly_ time |_MANUAL_, NONE, PRE++_++PROCESSING |manual +|`@Outgoing @Incoming Multi++<++O++>++ method(Multi++<++I++>++ pub)` |Called once at _assembly_ time |_PRE++_++PROCESSING_, NONE |not supported +|`@Outgoing @Incoming Flow.Publisher++<++Message++<++O++>>++ method(Flow.Publisher++<++Message++<++I++>>++ pub)` |Called once at _assembly_ time |_MANUAL_, NONE, PRE++_++PROCESSING |manual +|`@Outgoing @Incoming Flow.Publisher++<++O++>++ method(Flow.Publisher++<++I++>++ pub)` |Called once at _assembly_ time |_PRE++_++PROCESSING_, NONE |not supported +|`@Outgoing @Incoming PublisherBuilder++<++Message++<++O++>>++ method(PublisherBuilder++<++Message++<++I++>>++ pub)` |Called once at _assembly_ time |_MANUAL_, NONE, PRE++_++PROCESSING |manual +|`@Outgoing @Incoming PublisherBuilder++<++O++>++ method(PublisherBuilder++<++I++>++ pub)` |Called once at _assembly_ time |NONE, PRE++_++PROCESSING |not supported +|=== + +[IMPORTANT] +==== + +When processing `Message`, it is often required to _chain_ the incoming +`Message` to enable post-processing acknowledgement and metadata +propagation. Use the `with` (like `withPayload`) methods from the +incoming message, so it copies the metadata and ack/nack methods. It +returns a new `Message` with the right content. + +==== diff --git a/content/docs/smallrye-reactive-messaging/main/concepts/skipping.adoc b/content/docs/smallrye-reactive-messaging/main/concepts/skipping.adoc new file mode 100644 index 000000000..2717afe78 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/concepts/skipping.adoc @@ -0,0 +1,29 @@ += Skipping messages +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Sometimes you receive a message and don’t want to produce an output +message. To handle this, you have several choices: + +[arabic] +. for method processing single message or payload, producing `null` +would produce an ignored message (not forwarded) +. for method processing streams, you can generate an _empty_ stream. + +== Skipping a single item + +To skip a single message or payload, return `null`: + +[source,java] +---- +include::{examples-dir}/skip/SingleSkip.java[tags=skip,indent=0] +---- + +== Skipping in a stream + +To skip a message or payload when manipulating a stream, emit an _empty_ `Multi` (or `Publisher`): + +[source,java] +---- +include::{examples-dir}/skip/StreamSkip.java[tags=skip,indent=0] +---- diff --git a/content/docs/smallrye-reactive-messaging/main/concepts/testing.adoc b/content/docs/smallrye-reactive-messaging/main/concepts/testing.adoc new file mode 100644 index 000000000..c61cb82d6 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/concepts/testing.adoc @@ -0,0 +1,131 @@ += Testing your application +:version-root: ../ +include::{version-root}_attributes.adoc[] + +SmallRye Reactive Messaging provides a *test connector* that replaces +real broker connectors during tests. It lets you deliver messages to +incoming channels and verify messages sent to outgoing channels — without +requiring any external infrastructure. + +Add the following dependency to your project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.reactive + smallrye-reactive-messaging-testing + {project-version} + test + +---- + +Then, in a test, you can do something like: + +[source,java] +---- +include::{examples-dir}/testing/MyTest.java[indent=0] +---- + +When switching a channel to the test connector, all the +configuration properties are ignored. + +[WARNING] +==== + +This connector has been designed for testing purpose only. +Switching the channel to the test connector means that the +original connector is not invoked at all during tests. +Therefore, if your code depends on a specific connector behaviour +or a custom metadata you need to simulate those in your tests. + +==== + +The _switch_ methods return `Map++<++String, String++>++` instances containing +the set properties. While these system properties are already set, you +can retrieve them and pass them around, for example if you need to start +an external process with these properties: + +[source,java] +---- +include::{examples-dir}/testing/MyTestSetup.java[tags=code,indent=0] +---- + +[NOTE] +==== + +The test connector supports the `broadcast` and `merge` attributes. +So, if your connector is configured with `broadcast: true`, the +connector broadcasts the messages to all the channel consumers. If your +connector is configured with `merge:true`, the connector receives all +the messages sent to the mapped channel even when coming from multiple +producers. + +==== + +== Testing with connector-specific emitters + +Connector-specific emitter types such as `KafkaTransactions` or `KafkaRequestReply` work +with the test connector. When the channel uses the test connector instead of the native +connector, a fallback emitter is created automatically: + +* *Transaction emitters* (`KafkaTransactions`, `PulsarTransactions`): Transaction +semantics (begin/commit/abort) are skipped. Messages sent through the +`TransactionalEmitter` are dispatched normally and can be verified via the `TestOutgoing`. +The `markForAbort()` method is supported and causes the transaction `Uni` to fail with +`TransactionAbortedException`. All `withTransaction` variants work, including +exactly-once methods — the offset/acknowledgement management is skipped but the +work function executes normally. +* *Request-reply emitters* (`KafkaRequestReply`, `AmqpRequestReply`, +`RabbitMQRequestReply`): Requests are sent through the normal emitter path (visible +in the `TestOutgoing`). Replies are resolved via a programmatic reply function that you +configure in your test by casting the emitter to its no-op implementation and calling +`setReplyFunction()`. + +== Simulating connector-specific metadata + +When your application reads connector-specific metadata from incoming messages, +you can attach it using the `deliver(payload, metadata...)` overload: + +[source,java] +---- +incoming.deliver(new Order(...), + new IncomingKafkaRecordMetadata<>( + new ConsumerRecord<>("orders", 0, 42L, "key", "value"), "orders")); +---- + +This creates a `Message` with the given payload and metadata, so your application +code can retrieve it via `message.getMetadata(IncomingKafkaRecordMetadata.class)` +or through metadata injection in `@Incoming` method parameters. + +== Vert.x Context with Test Connector + +For the sake of simplicity, test connector channels dispatch messages on the caller thread of `TestIncoming#deliver` method. +However, most of the other connectors handle context propagation dispatching messages on separate xref:concepts/message-context.adoc[duplicated Vert.x contexts]. + +If this causes a change of behaviour in your tests, +you can configure the test connector channels with `run-on-vertx-context` attribute to dispatch events, +including messages and acknowledgements, on a Vert.x context. +Alternatively you can switch this behaviour using the `TestIncoming#runOnVertxContext` method. + +== Migrating from InMemoryConnector + +The `InMemoryConnector` class and the `smallrye-in-memory` connector name are deprecated. +Use `TestingConnector` with the `smallrye-testing` connector name instead. The old API continues +to work — both connector names are supported simultaneously, and the old classes extend +the new ones. + +[width="100%",cols="50%,50%",options="header",] +|=== +|Deprecated |Replacement +|`InMemoryConnector` |`TestingConnector` +|`InMemorySource` |`TestIncoming` +|`InMemorySink` |`TestOutgoing` +|`connector.source("channel")` |`connector.incoming("channel")` +|`connector.sink("channel")` |`connector.outgoing("channel")` +|`source.send(payload)` |`incoming.deliver(payload)` +|`sink.received()` |`outgoing.sent()` +|`InMemoryConnector.switchIncomingChannelsToInMemory(...)` |`TestingConnector.switchIncomingChannelsToTesting(...)` +|`InMemoryConnector.switchOutgoingChannelsToInMemory(...)` |`TestingConnector.switchOutgoingChannelsToTesting(...)` +|`"smallrye-in-memory"` |`"smallrye-testing"` +|`smallrye-reactive-messaging-in-memory` (Maven) |`smallrye-reactive-messaging-testing` +|=== diff --git a/content/docs/smallrye-reactive-messaging/main/getting-started.adoc b/content/docs/smallrye-reactive-messaging/main/getting-started.adoc new file mode 100644 index 000000000..9cc19d837 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/getting-started.adoc @@ -0,0 +1,63 @@ += Getting Started +include::_attributes.adoc[] + +The easiest way to start using SmallRye Reactive Messaging is from https://quarkus.io[Quarkus]. +SmallRye Reactive Messaging can also be used standalone, or with https://openliberty.io/guides/microprofile-reactive-messaging.html[Open Liberty]. + +First, go to https://code.quarkus.io/?g=io.smallrye&a=getting-started-with-reactive-messaging&e=smallrye-reactive-messaging[code.quarkus.io]. +Select the `smallrye-reactive-messaging` extension (already done if you use the link), and then click on the generate button to download the code. + +One downloaded, unzip the project and import it in your IDE. + +If you look at the `pom.xml` file, you will see the following dependency: + +[source,xml] +---- + + io.quarkus + quarkus-smallrye-reactive-messaging + +---- + +It provides the support for SmallRye Reactive Messaging. + +Ok, so far so good, but we need event-driven _beans_. + +Create the `quickstart` package, and copy the following class into it: + +For instance: + +[source,java] +---- +include::{examples-dir}/quickstart/ReactiveMessagingExample.java[indent=0] +---- + +This class contains a set of methods: + +* producing messages (`source`) +* processing messages (`toUpperCase`) +* transforming the stream by skipping messages (`filter`) +* consuming messages (`sink`) + +Each of these methods are connected through _channels_. + +Now, let's see this in action. +For the terminal, run: + +[source,shell] +---- +> ./mvnw quarkus:dev +---- + +Running the previous example should give the following output: + +[source,text] +---- +>> HELLO +>> SMALLRYE +>> REACTIVE +>> MESSAGE +---- + +Of course, this is a very simple example. +To go further, let's have a look to the core xref:concepts/concepts.adoc[concepts] behind SmallRye Reactive Messaging. diff --git a/content/docs/smallrye-reactive-messaging/main/images/chain.png b/content/docs/smallrye-reactive-messaging/main/images/chain.png new file mode 100644 index 000000000..eacfc241c Binary files /dev/null and b/content/docs/smallrye-reactive-messaging/main/images/chain.png differ diff --git a/content/docs/smallrye-reactive-messaging/main/images/channels.png b/content/docs/smallrye-reactive-messaging/main/images/channels.png new file mode 100644 index 000000000..8b2052ce4 Binary files /dev/null and b/content/docs/smallrye-reactive-messaging/main/images/channels.png differ diff --git a/content/docs/smallrye-reactive-messaging/main/images/connectors.png b/content/docs/smallrye-reactive-messaging/main/images/connectors.png new file mode 100644 index 000000000..ebd50c302 Binary files /dev/null and b/content/docs/smallrye-reactive-messaging/main/images/connectors.png differ diff --git a/content/docs/smallrye-reactive-messaging/main/images/favicon.png b/content/docs/smallrye-reactive-messaging/main/images/favicon.png new file mode 100644 index 000000000..93f705b9c Binary files /dev/null and b/content/docs/smallrye-reactive-messaging/main/images/favicon.png differ diff --git a/content/docs/smallrye-reactive-messaging/main/images/incoming.png b/content/docs/smallrye-reactive-messaging/main/images/incoming.png new file mode 100644 index 000000000..69e57a8b3 Binary files /dev/null and b/content/docs/smallrye-reactive-messaging/main/images/incoming.png differ diff --git a/content/docs/smallrye-reactive-messaging/main/images/logo.png b/content/docs/smallrye-reactive-messaging/main/images/logo.png new file mode 100644 index 000000000..2d957bd52 Binary files /dev/null and b/content/docs/smallrye-reactive-messaging/main/images/logo.png differ diff --git a/content/docs/smallrye-reactive-messaging/main/images/message.png b/content/docs/smallrye-reactive-messaging/main/images/message.png new file mode 100644 index 000000000..fb38e78b0 Binary files /dev/null and b/content/docs/smallrye-reactive-messaging/main/images/message.png differ diff --git a/content/docs/smallrye-reactive-messaging/main/images/messages.png b/content/docs/smallrye-reactive-messaging/main/images/messages.png new file mode 100644 index 000000000..c0b37e6a2 Binary files /dev/null and b/content/docs/smallrye-reactive-messaging/main/images/messages.png differ diff --git a/content/docs/smallrye-reactive-messaging/main/images/outgoing.png b/content/docs/smallrye-reactive-messaging/main/images/outgoing.png new file mode 100644 index 000000000..f5aaef29d Binary files /dev/null and b/content/docs/smallrye-reactive-messaging/main/images/outgoing.png differ diff --git a/content/docs/smallrye-reactive-messaging/main/images/overview.png b/content/docs/smallrye-reactive-messaging/main/images/overview.png new file mode 100644 index 000000000..42e00e441 Binary files /dev/null and b/content/docs/smallrye-reactive-messaging/main/images/overview.png differ diff --git a/content/docs/smallrye-reactive-messaging/main/images/processor.png b/content/docs/smallrye-reactive-messaging/main/images/processor.png new file mode 100644 index 000000000..faac01281 Binary files /dev/null and b/content/docs/smallrye-reactive-messaging/main/images/processor.png differ diff --git a/content/docs/smallrye-reactive-messaging/main/images/redhat_reversed.svg b/content/docs/smallrye-reactive-messaging/main/images/redhat_reversed.svg new file mode 100644 index 000000000..37fe83c48 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/images/redhat_reversed.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/content/docs/smallrye-reactive-messaging/main/index.adoc b/content/docs/smallrye-reactive-messaging/main/index.adoc new file mode 100644 index 000000000..153fa5e3d --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/index.adoc @@ -0,0 +1,16 @@ += SmallRye Reactive Messaging +include::_attributes.adoc[] + +*SmallRye Reactive Messaging is a framework for building event-driven, data streaming, and event-sourcing applications.* + +It lets your application interact using various messaging technologies such as Apache Kafka, AMQP or MQTT. +The framework provides a flexible programming model bridging https://www.cdi-spec.org/[CDI] and event-driven. + +SmallRye Reactive Messaging is an implementation of the Eclipse MicroProfile Reactive Messaging specification {spec-version}. + +.Event-Driven Architectures +image::images/overview.png[Event-Driven Architectures] + +SmallRye Reactive Messaging is used in http://quarkus.io[Quarkus], and https://openliberty.io/[Open Liberty]. + +Start with the xref:getting-started.adoc[Getting Started] guide to know how it can be used and introduce various features. diff --git a/content/docs/smallrye-reactive-messaging/main/jms/advanced-jms.adoc b/content/docs/smallrye-reactive-messaging/main/jms/advanced-jms.adoc new file mode 100644 index 000000000..7a33879c2 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/jms/advanced-jms.adoc @@ -0,0 +1,37 @@ += Advanced configuration +:version-root: ../ +include::{version-root}_attributes.adoc[] + +== Underlying thread pool + +Lots of JMS operations are blocking and so not cannot be done on the +_caller_ thread. For this reason, these blocking operations are executed +on a worker thread. + +You can configure the thread pool providing these worker threads using +the following MicroProfile Config properties: + +* `smallrye.jms.threads.max-pool-size` - the max number of threads +(Defaults to 10) +* `smallrye.jms.threads.ttl` - the ttl of the created threads +(Defaults to 60 seconds) + +== Selecting the ConnectionFactory + +The JMS Connector requires a `jakarta.jms.ConnectionFactory` to be exposed +as a CDI bean. The connector looks for a `jakarta.jms.ConnectionFactory` +and delegate the interaction with the JMS server to this factory. + +In case you have several connection factories, you can use the +`@Identifier` qualifier on your factory to specify the name. Then, in +the channel configuration, configure the name as follows: + +[source,properties] +---- +# Configure the connector globally +mp.messaging.connector.smallrye-jms.connection-factory-name=my-factory-name +# Configure a specific incoming channel +mp.messaging.incoming.my-channel.connection-factory-name=my-factory-name +# Configure a specific outgoing channel +mp.messaging.outgoing.my-channel.connection-factory-name=my-factory-name +---- diff --git a/content/docs/smallrye-reactive-messaging/main/jms/jms-transactions.adoc b/content/docs/smallrye-reactive-messaging/main/jms/jms-transactions.adoc new file mode 100644 index 000000000..bc62cc77c --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/jms/jms-transactions.adoc @@ -0,0 +1,169 @@ += JMS Transactions +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The JMS connector supports transactional message consumption using the `transaction-mode` channel configuration property. +Three modes are available: + +* `none` (default) — no transaction support, messages are acknowledged individually. +* `local` — JMS local (session) transactions. The session is committed on successful processing (ack) and rolled back on failure (nack). +* `xa` — XA distributed transactions. An XA transaction is started before each message receive, allowing other XA resources (e.g., databases) to participate in the same global transaction. + +== Local Transactions + +Local transactions use `SESSION++_++TRANSACTED` sessions. +Messages are processed sequentially — the next message is not received until the current one is committed or rolled back. +On ack, the session is committed. On nack, the session is rolled back and the message is redelivered by the broker. + +=== Configuration + +[source,properties] +---- +mp.messaging.incoming.orders.connector=smallrye-jms +mp.messaging.incoming.orders.transaction-mode=local +---- + +[NOTE] +==== + +The `session-mode` configuration is ignored when `transaction-mode=local`. The connector automatically uses `SESSION++_++TRANSACTED`. + +==== + +=== Example + +[source,java] +---- +@ApplicationScoped +public class OrderProcessor { + + @Incoming("orders") + public CompletionStage process(IncomingJmsMessage message) { + // Process the message... + // On ack, the JMS session is committed. + // On nack, the JMS session is rolled back and the message is redelivered. + return message.ack(); + } +} +---- + +=== Session Sharing with Outgoing Channels + +When `transaction-mode` is `local` or `xa`, messages carry a `JmsSessionContext` metadata. +If the outgoing sink detects this metadata, it sends using the incoming transactional session context, +ensuring both the receive and send participate in the same JMS transaction. + +== XA Distributed Transactions + +XA mode enables distributed transactions spanning JMS and other XA-capable resources such as databases. +An XA transaction is started before each message is received, and the JMS XA resource is enlisted in the transaction. + +=== Prerequisites + +XA transactions require: + +* A `TransactionManager` (e.g., `quarkus-narayana-jta`) +* A *pooled* `XAConnectionFactory` (e.g., `quarkus-pooled-jms` or a JCA resource adapter such as `quarkus-ironjacamar`) + +[WARNING] +==== + +Using a non-pooled `XAConnectionFactory` will cause severe performance degradation, as a new connection is created for every message poll. + +==== + +=== Configuration + +[source,properties] +---- +mp.messaging.incoming.orders.connector=smallrye-jms +mp.messaging.incoming.orders.transaction-mode=xa +---- + +=== Participating in the XA Transaction + +The XA transaction is *suspended* after receive so it can be resumed on the processing thread. +Container integrations such as Quarkus typically provide a custom invoker that handles resume/suspend automatically. + +Without a container invoker, the application can use `JmsXaTransactionMetadata.withTransaction()` +to associate the XA transaction with the current thread for the duration of a try-with-resources block: + +[source,java] +---- +@ApplicationScoped +public class OrderProcessor { + + @Inject + EntityManager entityManager; + + @Incoming("orders") + public void process(int payload, JmsXaTransactionMetadata xa) { + try (var scope = xa.withTransaction()) { + // Database and JMS participate in the same XA transaction + entityManager.persist(toEntity(payload)); + } + // On return, the framework acks and the XA transaction is committed + } +} +---- + +== Receive Timeout + +The `receive-timeout` property controls how long (in milliseconds) the JMS consumer `receive` call blocks before returning `null`. +The default is `1000` ms. +Setting it to `0` blocks indefinitely until a message arrives. + +[WARNING] +==== + +With `transaction-mode=xa` the receive happens inside the XA transaction, so `0` is rejected: +it would exceed the transaction timeout while the channel is idle. +Keep `receive-timeout` well below the transaction timeout. + +==== + +[source,properties] +---- +mp.messaging.incoming.orders.receive-timeout=2000 +---- + +== Custom Message Pollers + +The `message-poller` property allows overriding the built-in polling behavior with a custom `JmsMessagePoller.Factory` CDI bean +qualified with `@Identifier`. + +[source,java] +---- +@ApplicationScoped +@Identifier("my-poller") +public class MyPollerFactory implements JmsMessagePoller.Factory { + + @Override + public JmsMessagePoller create(JmsConnectorIncomingConfiguration config, + JmsResourceHolder resourceHolder) { + // resourceHolder is pre-configured for local and none modes, + // but null for XA mode where each poll creates its own context. + Objects.requireNonNull(resourceHolder, "This poller does not support XA mode"); + return () -> { + jakarta.jms.Message received = resourceHolder.getClient().receive(500); + return received != null ? Message.of(received) : null; + }; + } +} +---- + +[source,properties] +---- +mp.messaging.incoming.orders.message-poller=my-poller +---- + +The commit and failure handlers remain determined by the `transaction-mode` — the custom poller only controls how messages are received. + +== Failure Handling + +Transaction modes work with all failure strategies (`fail`, `ignore`, `dead-letter-queue`). + +* For `local` mode, nack triggers a session rollback before the failure strategy is applied. +* For `xa` mode, nack triggers an XA transaction rollback before the failure strategy is applied. + +In both cases the message may be redelivered by the broker, depending on the broker configuration and the failure strategy used. diff --git a/content/docs/smallrye-reactive-messaging/main/jms/jms.adoc b/content/docs/smallrye-reactive-messaging/main/jms/jms.adoc new file mode 100644 index 000000000..b94527d64 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/jms/jms.adoc @@ -0,0 +1,74 @@ += The JMS connector +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The JMS connector adds support for https://en.wikipedia.org/wiki/Jakarta_Messaging[Jakarta +Messaging] to Reactive +Messaging. It is designed to integrate with _JakartaEE_ applications +that are sending or receiving Jakarta Messaging Messages. + +Jakarta Messaging is a Java Message Oriented Middleware API for sending +messages between two or more clients. It is a programming model to +handle the producer-consumer messaging problem. It is a messaging +standard that allows application components based on Jakarta EE to +create, send, receive, and read messages. It allows the communication +between different components of a distributed application to be loosely +coupled, reliable, and asynchronous. + +Using the JMS connector + +To you the JMS Connector, add the following dependency to your project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.reactive + smallrye-reactive-messaging-jms + {project-version} + +---- + +The connector name is: `smallrye-jms`. + +So, to indicate that a channel is managed by this connector you need: + +[source,properties] +---- +# Inbound +mp.messaging.incoming.[channel-name].connector=smallrye-jms + +# Outbound +mp.messaging.outgoing.[channel-name].connector=smallrye-jms +---- + +The JMS Connector requires a `jakarta.jms.ConnectionFactory` to be exposed +(as CDI bean). The connector looks for a `jakarta.jms.ConnectionFactory` +and delegate the interaction with the JMS server to this factory. In +other words, it creates the JMS connection and context using this +factory. + +So, in order to use this connector you would need to expose a +`jakarta.jms.ConnectionFactory`: + +[source,java] +---- +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; +import jakarta.jms.ConnectionFactory; + +import org.apache.activemq.artemis.jms.client.ActiveMQJMSConnectionFactory; + +@ApplicationScoped +public class ConnectionFactoryBean { + + @Produces + ConnectionFactory factory() { + return new ActiveMQJMSConnectionFactory( + "tcp://localhost:61616", + null, null); + } + +} +---- + +The factory class may depend on your JMS connector/server. diff --git a/content/docs/smallrye-reactive-messaging/main/jms/receiving-jms-messages.adoc b/content/docs/smallrye-reactive-messaging/main/jms/receiving-jms-messages.adoc new file mode 100644 index 000000000..08f9af3f5 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/jms/receiving-jms-messages.adoc @@ -0,0 +1,115 @@ += Receiving messages from JMS +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The JMS Connector retrieves JMS Message and maps each of them into +Reactive Messaging `Messages`. + +== Example + +Let’s imagine you have a `jakarta.jms.ConnectionFactory` bean exposed and +connected to your JMS server. Don’t forget that it’s required to use the +JMS connector. + +Configure your application to receive JMS messages on the `prices` +channel as follows: + +[source,properties] +---- +mp.messaging.incoming.prices.connector=smallrye-jms +---- + +[NOTE] +==== + +You don’t need to set the destination. By default, it uses the channel +name (`prices`). You can configure the `destination` attribute to +override it. + +==== + +[NOTE] +==== + +By default the connector uses a `queue`. You can configure it to use a +`topic` by setting `destination-type=topic`. + +==== + +Then, your application receives `Message++<++Double++>++`. You can consume the +payload directly: + +[source,java] +---- +include::{examples-dir}/jms/inbound/JmsPriceConsumer.java[indent=0] +---- + +Or, you can retrieve the `Message++<++Double++>++`: + +[source,java] +---- +include::{examples-dir}/jms/inbound/JmsPriceMessageConsumer.java[indent=0] +---- + +== Deserialization + +The content of the incoming JMS message is mapped to a Java object. + +By default it extracts the JMS Message _body_ as a `java.lang.Object`. +This can be changed by setting, in the incoming JMS Message: + +[arabic] +. The `++_++classname` property +. the `JMSType` + +The value must be a fully qualified class name. The connector then load +the associated class. + +[NOTE] +==== + +The connector loads the associated `Class` using the `TCCL` and if not +found, the classloader used to load the connector. + +==== + +If the target type is a primitive type ort `String`, the resulting +message contains the mapped payload. + +If the target type is a class, the object is built using included JSON +deserializer (JSON-B and Jackson provided OOB from the `JMSType`. +If not, the default behavior is used (Java deserialization). + +== Inbound Metadata + +Messages coming from JMS contains an instance of link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-jms/{javadoc-version}/io/smallrye/reactive/messaging/jms/IncomingJmsMessageMetadata.html[`io.smallrye.reactive.messaging.jms.IncomingJmsMessageMetadata`] in the metadata. + +[source,java] +---- +include::{examples-dir}/jms/inbound/JmsMetadataExample.java[tags=code,indent=0] +---- + +== Acknowledgement + +When the Reactive Messaging `Message` gets acknowledged, the associated +JMS Message is acknowledged. As JMS acknowledgement is blocking, this +acknowledgement is delegated to a worker thread. + +== Failure Handling + +If a message produced from a JMS message is _nacked_, a failure strategy is applied. The JMS connector supports 3 strategies: + +* `fail` - (default) fail the application, no more messages will be processed. The failing message is not acknowledged and may be redelivered by the JMS broker. The application is marked as unhealthy (impacting liveness checks). +* `ignore` - the failure is logged, but the processing continues. The failing message is acknowledged and will not be redelivered. +* `dead-letter-queue` - the failing message is acknowledged and sent to a JMS _dead letter queue_ destination. The processing continues with the next message. ++ +The dead letter queue destination can be configured using the `dead-letter-queue.destination` attribute. If not specified, it defaults to `dead-letter-queue-$channel`. +Messages sent to the dead letter queue preserve the original message body and properties. In addition, the following properties are added: +** `dead++_++letter++_++exception++_++class++_++name` - the fully qualified class name of the exception +** `dead++_++letter++_++reason` - the exception message +** `dead++_++letter++_++cause++_++class++_++name` - the fully qualified class name of the root cause (if available) +** `dead++_++letter++_++cause` - the root cause exception message (if available) + +== Configuration Reference + +include::{generated-dir}/smallrye-jms-incoming.adoc[indent=0] diff --git a/content/docs/smallrye-reactive-messaging/main/jms/sending-jms-messages.adoc b/content/docs/smallrye-reactive-messaging/main/jms/sending-jms-messages.adoc new file mode 100644 index 000000000..0e107b798 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/jms/sending-jms-messages.adoc @@ -0,0 +1,106 @@ += Sending messages to JMS +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The JMS Connector can send Reactive Messaging `Messages` as JMS Message. + +== Example + +Let’s imagine you have a `jakarta.jms.ConnectionFactory` bean exposed and +connected to your JMS server. Don’t forget that it’s required to use the +JMS connector. + +Configure your application to write the messages from the `prices` +channel into a JMS Message as follows: + +[source,properties] +---- +mp.messaging.outgoing.prices.connector=smallrye-jms +---- + +[NOTE] +==== + +You don’t need to set the destination. By default, it uses the channel +name (`prices`). You can configure the `destination` attribute to +override it. + +==== + +[NOTE] +==== + +By default the connector uses a `queue`. You can configure it to use a +`topic` by setting `destination-type=topic`. + +==== + +Then, your application must send `Message++<++Double++>++` to the `prices` +channel. It can use `double` payloads as in the following snippet: + +[source,java] +---- +include::{examples-dir}/jms/outbound/JmsPriceProducer.java[indent=0] +---- + +Or, you can send `Message++<++Double++>++`: + +[source,java] +---- +include::{examples-dir}/jms/outbound/JmsPriceMessageProducer.java[indent=0] +---- + +== Serialization + +The connector serializes the incoming message payload into the body of +the outgoing JMS Message. + +If the payload is a `String` or a primitive type, the payload is encoded +as `String` and the `JMSType` is set to the target class. The +`++_++classname` property is also set. The JMS Message is a `TextMessage`. + +If the payload is a `byte++[]++`, it’s passed as `byte++[]++` in a JMS +`BytesMessage`. + +Otherwise, the payload is encoded using included JSON serializer (JSON-B and Jackson provided OOB. +The `JMSType` is set to the target class. +The `++_++classname` property is also set. +The JMS Message is a `TextMessage`. + +For example, the following code serialize the produced `Person` using +JSON-B. + +[source,java] +---- +@Incoming("...") +@Outgoing("my-channel") +public Person sendToJms(...) { + // ... + return new Person("bob", 42); +} +---- + +It requires that the `Person` class can be serialized to JSON. The +classname is passed in the `JMSType` property and `++_++classname` property. + +== Outbound Metadata + +When sending `Messages`, you can add an instance of link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-jms/{javadoc-version}/io/smallrye/reactive/messaging/jms/OutgoingJmsMessageMetadata.html[`OutgoingJmsMessageMetadata`] +to influence how the message is going to be written to JMS. + +[source,java] +---- +include::{examples-dir}/jms/outbound/JmsOutboundMetadataExample.java[tags=code,indent=0] +---- + +The metadata allow adding properties but also override the destination. + +== Acknowledgement + +Once the JMS message is sent to the JMS server, the message is +acknowledged. Sending a JMS message is a blocking operation. So, sending +is done on a worker thread. + +== Configuration Reference + +include::{generated-dir}/smallrye-jms-outgoing.adoc[indent=0] diff --git a/content/docs/smallrye-reactive-messaging/main/kafka/avro-configuration.adoc b/content/docs/smallrye-reactive-messaging/main/kafka/avro-configuration.adoc new file mode 100644 index 000000000..a714f9cb9 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/kafka/avro-configuration.adoc @@ -0,0 +1,152 @@ += Using Apache Avro serializer/deserializer +:version-root: ../ +include::{version-root}_attributes.adoc[] + +If you are using https://avro.apache.org/[Apache Avro] +serializer/deserializer, please note the following configuration +properties. + +== For https://docs.confluent.io/current/schema-registry/serdes-develop/serdes-avro.html[Confluent] Schema Registry + +Confluent Avro library is `io.confluent:kafka-avro-serializer`. Note +that this library is not available in Maven Central, you need to use the +https://docs.confluent.io/clients-kafka-java/current/overview.html[Confluent Maven +repository]. + +=== Consumer + +[width="100%",cols="25%,75%",options="header",] +|=== +|Property |Recommended value +|value.deserializer |io.confluent.kafka.serializers.KafkaAvroDeserializer +|schema.registry.url |http://%7Byour_host%7D:%7Byour_port%7D/ +|specific.avro.reader |true +|=== + +Example: + +.... +mp.messaging.incoming.[channel].value.deserializer=io.confluent.kafka.serializers.KafkaAvroDeserializer +mp.messaging.incoming.[channel].schema.registry.url=http://{your_host}:{your_port}/ +mp.messaging.incoming.[channel].specific.avro.reader=true +.... + +=== Producer + +[width="100%",cols="24%,76%",options="header",] +|=== +|Property |Recommended value +|value.serializer |io.confluent.kafka.serializers.KafkaAvroSerializer +|schema.registry.url |http://%7Byour_host%7D:%7Byour_port%7D/ +|=== + +Example: + +.... +mp.messaging.outgoing.[channel].value.serializer=io.confluent.kafka.serializers.KafkaAvroSerializer +mp.messaging.outgoing.[channel].schema.registry.url=http://{your_host}:{your_port}/ +.... + +== For https://www.apicur.io/registry/[Apicurio] Registry 1.x + +Apicurio Registry 1.x Avro library is +`io.apicurio:apicurio-registry-utils-serde`. + +The configuration properties listed here are meant to be used with the +Apicurio Registry 1.x client library and Apicurio Registry 1.x server. + +=== Consumer + +[width="100%",cols="38%,62%",options="header",] +|=== +|Property |Recommended value +|value.deserializer |io.apicurio.registry.utils.serde.AvroKafkaDeserializer +|apicurio.registry.url |http://%7Byour_host%7D:%7Byour_port%7D/api +|apicurio.registry.avro-datum-provider |io.apicurio.registry.utils.serde.avro.DefaultAvroDatumProvider +|apicurio.registry.use-specific-avro-reader |true +|=== + +Example: + +.... +mp.messaging.incoming.[channel].value.deserializer=io.apicurio.registry.utils.serde.AvroKafkaDeserializer +mp.messaging.incoming.[channel].apicurio.registry.url=http://{your_host}:{your_port}/api +mp.messaging.incoming.[channel].apicurio.registry.avro-datum-provider=io.apicurio.registry.utils.serde.avro.DefaultAvroDatumProvider +mp.messaging.incoming.[channel].apicurio.registry.use-specific-avro-reader=true +.... + +=== Producer + +[width="100%",cols="24%,76%",options="header",] +|=== +|Property |Recommended value +|value.serializer |io.apicurio.registry.utils.serde.AvroKafkaSerializer +|apicurio.registry.url |http://%7Byour_host%7D:%7Byour_port%7D/api +|=== + +To automatically register schemas with the registry, add: + +[width="100%",cols="31%,69%",options="header",] +|=== +|Property |Value +|apicurio.registry.global-id |io.apicurio.registry.utils.serde.strategy.GetOrCreateIdStrategy +|=== + +Example: + +.... +mp.messaging.outgoing.[channel].value.serializer=io.apicurio.registry.utils.serde.AvroKafkaSerializer +mp.messaging.outgoing.[channel].apicurio.registry.url=http://{your_host}:{your_port}/api +mp.messaging.outgoing.[channel].apicurio.registry.global-id=io.apicurio.registry.utils.serde.strategy.GetOrCreateIdStrategy +.... + +== For https://www.apicur.io/registry/[Apicurio] Registry 2.x + +Apicurio Registry 2.x Avro library is +`io.apicurio:apicurio-registry-serdes-avro-serde`. + +The configuration properties listed here are meant to be used with the +Apicurio Registry 2.x client library and Apicurio Registry 2.x server. + +=== Consumer + +[width="100%",cols="31%,69%",options="header",] +|=== +|Property |Recommended value +|value.deserializer |io.apicurio.registry.serde.avro.AvroKafkaDeserializer +|apicurio.registry.url |http://%7Byour_host%7D:%7Byour_port%7D/apis/registry/v2 +|apicurio.registry.use-specific-avro-reader |true +|=== + +Example: + +.... +mp.messaging.incoming.[channel].value.deserializer=io.apicurio.registry.serde.avro.AvroKafkaDeserializer +mp.messaging.incoming.[channel].apicurio.registry.url=http://{your_host}:{your_port}/apis/registry/v2 +mp.messaging.incoming.[channel].apicurio.registry.use-specific-avro-reader=true +.... + +=== Producer + +[width="100%",cols="19%,81%",options="header",] +|=== +|Property |Recommended value +|value.serializer |io.apicurio.registry.serde.avro.AvroKafkaSerializer +|apicurio.registry.url |http://%7Byour_host%7D:%7Byour_port%7D/apis/registry/v2 +|=== + +To automatically register schemas with the registry, add: + +[cols=",",options="header",] +|=== +|Property |Value +|apicurio.registry.auto-register |true +|=== + +Example: + +.... +mp.messaging.outgoing.[channel].value.serializer=io.apicurio.registry.serde.avro.AvroKafkaSerializer +mp.messaging.outgoing.[channel].apicurio.registry.url=http://{your_host}:{your_port}/apis/registry/v2 +mp.messaging.outgoing.[channel].apicurio.registry.auto-register=true +.... diff --git a/content/docs/smallrye-reactive-messaging/main/kafka/client-service.adoc b/content/docs/smallrye-reactive-messaging/main/kafka/client-service.adoc new file mode 100644 index 000000000..7c6f63dc9 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/kafka/client-service.adoc @@ -0,0 +1,60 @@ += KafkaClientService +:version-root: ../ +include::{version-root}_attributes.adoc[] + +For advanced use cases, SmallRye Reactive Messaging provides a bean of +type `KafkaClientService` that you can inject: + +[source,java] +---- +@Inject +KafkaClientService kafka; +---- + +From there, you can obtain an +`io.smallrye.reactive.messaging.kafka.KafkaProducer` and an +`io.smallrye.reactive.messaging.kafka.KafkaConsumer`. + +`KafkaProducer` and `KafkaConsumer` expose a non-blocking API on top of +the Kafka client API. They also mediate access to the threads that +SmallRye Reactive Messaging uses to run all Kafka operations: the +_polling thread_, used for consuming records from Kafka topics, and the +_sending thread_, used for producing records to Kafka topics. (Just to +be clear: each _channel_ has its own polling thread and sending thread.) + +The reason why SmallRye Reactive Messaging uses a special thread to run +the poll loop should be obvious: the `Consumer` API is blocking. The +`Producer` API, on the other hand, is documented to be non-blocking. +However, in present versions, Kafka doesn’t guarantee that in all cases; +see https://issues.apache.org/jira/browse/KAFKA-3539[KAFKA-3539] for +more details. That is why SmallRye Reactive Messaging uses a dedicated +thread to run the send operations as well. + +Sometimes, SmallRye Reactive Messaging provides direct access to the +Kafka `Producer` or `Consumer`. For example, a +xref:kafka/consumer-rebalance-listener.adoc[`KafkaConsumerRebalanceListener`] +methods are always invoked on the polling thread, so they give you +direct access to `Consumer`. In such case, you should use the +`Producer`/`Consumer` API directly, instead of the +`KafkaProducer`/`KafkaConsumer` API. + +== Manual Partition Pause/Resume + +The `KafkaConsumer` API provides per-partition pause and resume methods +that work alongside the `pause-if-no-requests` backpressure mechanism +(enabled by default). + +When you manually pause partitions using `pausePartitions(Collection++<++TopicPartition++>++)`, +those partitions remain paused even when the backpressure system resumes +consumption. This is useful for scenarios like pausing a partition during +failure handling while keeping other partitions flowing: + +include::{examples-dir}/kafka/inbound/KafkaManualPartitionPauseResume.java[tags=code,indent=0] + +If a partition is revoked during a rebalance, it is automatically +removed from the manually paused set. + +Note that the no-arg `pause()` and `resume()` methods control the +global pause state and are independent: calling `resume()` will not +resume manually paused partitions. +To pause all currently assigned partitions, use `pauseAssignedPartitions()`. diff --git a/content/docs/smallrye-reactive-messaging/main/kafka/consumer-rebalance-listener.adoc b/content/docs/smallrye-reactive-messaging/main/kafka/consumer-rebalance-listener.adoc new file mode 100644 index 000000000..fbbdd605f --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/kafka/consumer-rebalance-listener.adoc @@ -0,0 +1,67 @@ += Consumer Rebalance Listener +:version-root: ../ +include::{version-root}_attributes.adoc[] + +To handle offset commit and assigned partitions yourself, you can +provide a consumer rebalance listener. To achieve this, implement the +`io.smallrye.reactive.messaging.kafka.KafkaConsumerRebalanceListener` +interface, make the implementing class a bean, and add the `@Identifier` +qualifier. A usual use case is to store offset in a separate data store +to implement exactly-once semantic, or starting the processing at a +specific offset. + +The listener is invoked every time the consumer topic/partition +assignment changes. For example, when the application starts, it invokes +the `partitionsAssigned` callback with the initial set of +topics/partitions associated with the consumer. If, later, this set +changes, it calls the `partitionsRevoked` and `partitionsAssigned` +callbacks again, so you can implement custom logic. + +Note that the rebalance listener methods are called from the Kafka +_polling_ thread and must block the caller thread until completion. +That’s because the rebalance protocol has synchronization barriers, and +using asynchronous code in a rebalance listener may be executed after +the synchronization barrier. + +When topics/partitions are assigned or revoked from a consumer, it +pauses the message delivery and restarts once the rebalance completes. + +If the rebalance listener handles offset commit on behalf of the user +(using the `ignore` commit strategy), the rebalance listener *must* +commit the offset synchronously in the `partitionsRevoked` callback. We +also recommend applying the same logic when the application stops. + +Unlike the `ConsumerRebalanceListener` from Apache Kafka, the +`io.smallrye.reactive.messaging.kafka.KafkaConsumerRebalanceListener` +methods pass the Kafka `Consumer` and the set of topics/partitions. + +== Example + +In this example we set-up a consumer that always starts on messages from +at most 10 minutes ago (or offset 0). First we need to provide a bean +that implements the +`io.smallrye.reactive.messaging.kafka.KafkaConsumerRebalanceListener` +interface and is annotated with `@Identifier`. We then must configure +our inbound connector to use this named bean. + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaRebalancedConsumerRebalanceListener.java[indent=0] +---- + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaRebalancedConsumer.java[indent=0] +---- + +To configure the inbound connector to use the provided listener we +either set the consumer rebalance listener’s name: + +* `mp.messaging.incoming.rebalanced-example.consumer-rebalance-listener.name=rebalanced-example.rebalancer` + +Or have the listener’s name be the same as the group id: + +* `mp.messaging.incoming.rebalanced-example.group.id=rebalanced-example.rebalancer` + +Setting the consumer rebalance listener’s name takes precedence over +using the group id. diff --git a/content/docs/smallrye-reactive-messaging/main/kafka/default-configuration.adoc b/content/docs/smallrye-reactive-messaging/main/kafka/default-configuration.adoc new file mode 100644 index 000000000..a8da3024e --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/kafka/default-configuration.adoc @@ -0,0 +1,80 @@ += Retrieving Kafka default configuration +:version-root: ../ +include::{version-root}_attributes.adoc[] + +If your application/runtime exposes as a CDI _bean_ of type +`Map++<++String, Object` with the identifier `default-kafka-broker`, this +configuration is used to establish the connection with the Kafka broker. + +For example, you can imagine exposing this map as follows: + +[source,java] +---- +@Produces +@ApplicationScoped +@Identifier("default-kafka-broker") +public Map createKafkaRuntimeConfig() { + Map properties = new HashMap<>(); + + StreamSupport + .stream(config.getPropertyNames().spliterator(), false) + .map(String::toLowerCase) + .filter(name -> name.startsWith("kafka")) + .distinct() + .sorted() + .forEach(name -> { + final String key = name.substring("kafka".length() + 1).toLowerCase().replaceAll("[^a-z0-9.]", "."); + final String value = config.getOptionalValue(name, String.class).orElse(""); + properties.put(key, value); + }); + + return properties; +} +---- + +This previous example would extract all the configuration keys from +MicroProfile Config starting with `kafka`. + +[NOTE] +.Quarkus +==== + +Starting with Quarkus 1.5, a map corresponding to the previous example +is automatically provided. + +==== + +In addition to this default configuration, you can configure the name of +the `Map` producer using the `kafka-configuration` attribute: + +[source,properties] +---- +mp.messaging.incoming.my-channel.connector=smallrye-kafka +mp.messaging.incoming.my-channel.kafka-configuration=my-configuration +---- + +In this case, the connector looks for the `Map` associated with the +`my-configuration` name. If `kafka-configuration` is not set, an +optional lookup for a `Map` exposed with the channel name (`my-channel` +in the previous example) is done. + +[IMPORTANT] +==== + +If `kafka-configuration` is set and no `Map` can be found, the +deployment fails. + +==== + +Attribute values are resolved as follows: + +[arabic] +. if the attribute is set directly on the channel configuration +(`mp.messaging.incoming.my-channel.attribute=value`), this value is +used +. if the attribute is not set on the channel, the connector looks for +a `Map` with the channel name or the configured +`kafka-configuration` (if set) and the value is retrieved from that +`Map` +. If the resolved `Map` does not contain the value the default `Map` +is used (exposed with the `default-kafka-broker` name) diff --git a/content/docs/smallrye-reactive-messaging/main/kafka/health.adoc b/content/docs/smallrye-reactive-messaging/main/kafka/health.adoc new file mode 100644 index 000000000..2b233b117 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/kafka/health.adoc @@ -0,0 +1,107 @@ += Health reporting +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The Kafka connector reports the startup, readiness and liveness of each channel managed by the connector. + +[NOTE] +==== + +To disable health reporting, set the `health-enabled` attribute for the +channel to `false`. + +==== + +== Startup & Readiness + +=== Metrics-based strategy + +By default, both inbound and outbound channels use underlying Kafka client metrics to check if at least one active connection exists with a broker. +This strategy is lightweight and does not require additional remote interactions with the broker. + +=== Client-based strategy + +You can also enable another strategy by setting the `health-topic-verification-enabled` attribute to `true`. +With this second strategy, the health checks use a Kafka Admin Client to access the broker and retrieve the list of existing topics. +Retrieving this list can be a lengthy and expensive operation. +You can configure a timeout using the `health-topic-verification-timeout` attribute. +The default timeout is set to 2 seconds. +Note that if the timeout is reached, the health check fails. + +[WARNING] +.Deprecated +==== + +`health-readiness-topic-verification` and `health-readiness-timeout` attributes are deprecated and replaced by `health-topic-verification-enabled` and `health-topic-verification-timeout`. + +==== + +For *startup* checks both _inbound_ and _outbound_ side verify that the Kafka topic is created and its partitions are available in the broker. +If multiple topics are consumed using the `topics` attribute, the readiness check verifies that all the consumed topics are available. +If you use a pattern (using the `pattern` attribute), the readiness check verifies that at least one existing topic matches the pattern. + +For *readiness* checks inbound channels verify that the underlying consumer is assigned at least a partition to consume. +On the outbound side (writing records to Kafka) verify that the broker is still accessible. + +[NOTE] +==== + +If `health-topic-verification-enabled` is enabled, both for startup and readiness checks use this strategy. They can be disabled explicitly using `health-topic-verification-startup-disabled` and `health-topic-verification-readiness-disabled` flags. + +==== + +==== Admin Client Pooling + +By default, the client-based strategy creates a dedicated Kafka Admin Client per channel, using client IDs that include the channel name (e.g. `kafka-admin-incoming-prices`, `kafka-admin-outgoing-orders`). +This makes it easy to identify each client in monitoring, logs, and ACLs. + +If your application has many channels sharing the same Kafka broker configuration, you can opt in to admin client pooling to reduce the number of connections: + +[source,properties] +---- +smallrye.messaging.kafka.admin-client.pooling.enabled=true +---- + +When pooling is enabled, channels with identical broker configurations share a single Admin Client instance, using counter-based client IDs (`smallrye-kafka-admin-1`, `smallrye-kafka-admin-2`, ...). + +To summarize for startup and readiness health checks: + +*Startup* + +[width="100%",cols="12%,50%,38%",options="header",] +|=== +| |Inbound |Outbound +|Metrics-based |`connection-count` metric ++>++ 0 or + + no subscribers to the stream |`connection-count` metric ++>++ 0 +|Client-based |Subscribed topic(s) exist in the broker and are available |Produced topic exist in the broker and is available +|=== + +*Readiness* + +[width="100%",cols="11%,56%,33%",options="header",] +|=== +| |Inbound |Outbound +|Metrics-based |`connection-count` metric ++>++ 0 or + + no subscribers to the stream |`connection-count` metric ++>++ 0 +|Client-based |Consumer has at least one partition assignment + + no subscribers to the stream |Cluster is acessible using the Kafka admin API +|=== + +== Liveness + +On the inbound side (receiving records from Kafka), the liveness check +verifies that: + +* no failures have been caught +* the client is connected to the broker + +On the outbound side (writing records to Kafka), the liveness check +verifies that: + +* no failures have been caught + +Note that a message processing failures _nacks_ the message which is +then handled by the failure-strategy. It the responsibility of the +failure-strategy to report the failure and influence the outcome of the +liveness checks. The `fail` failure strategy reports the failure and so +the liveness check will report the failure. diff --git a/content/docs/smallrye-reactive-messaging/main/kafka/kafka.adoc b/content/docs/smallrye-reactive-messaging/main/kafka/kafka.adoc new file mode 100644 index 000000000..dbe86b6d0 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/kafka/kafka.adoc @@ -0,0 +1,47 @@ += Apache Kafka Connector +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The Kafka connector adds support for Kafka to Reactive Messaging. With +it you can receive Kafka Records as well as write `message` into Kafka. + +https://kafka.apache.org/[Apache Kafka] is a popular distributed +streaming platform. It lets you: + +* Publish and subscribe to streams of records, similar to a message +queue or enterprise messaging system. +* Store streams of records in a fault-tolerant durable way. +* Process streams of records as they occur. + +The Kafka cluster stores streams of _records_ in categories called +_topics_. Each record consists of a _key_, a _value_, and a _timestamp_. + +For more details about Kafka, check the +https://kafka.apache.org/intro[documentation]. + +== Using the Kafka Connector + +To use the Kafka Connector, add the following dependency to your +project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.reactive + smallrye-reactive-messaging-kafka + {project-version} + +---- + +The connector name is: `smallrye-kafka`. + +So, to indicate that a channel is managed by this connector you need: + +[source,properties] +---- +# Inbound +mp.messaging.incoming.[channel-name].connector=smallrye-kafka + +# Outbound +mp.messaging.outgoing.[channel-name].connector=smallrye-kafka +---- diff --git a/content/docs/smallrye-reactive-messaging/main/kafka/kerberos.adoc b/content/docs/smallrye-reactive-messaging/main/kafka/kerberos.adoc new file mode 100644 index 000000000..873617497 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/kafka/kerberos.adoc @@ -0,0 +1,22 @@ += Kerberos authentication +:version-root: ../ +include::{version-root}_attributes.adoc[] + +When using https://en.wikipedia.org/wiki/Kerberos_(protocol)[Kerberos] +authentication, you need to configure the connector with: + +* the security protocol set to `SASL++_++PLAINTEXT` +* the SASL mechanism set to `GSSAPI` +* the Jaas config configured with `Krb5LoginModule` +* the Kerberos service name + +The following snippet provides an example: + +[source,properties] +---- +kafka.bootstrap.servers=ip-192-168-0-207.us-east-2.compute.internal:9094 +kafka.sasl.mechanism=GSSAPI +kafka.security.protocol=SASL_PLAINTEXT +kafka.sasl.jaas.config=com.sun.security.auth.module.Krb5LoginModule required doNotPrompt=true refreshKrb5Config=true useKeyTab=true storeKey=true keyTab="file:/opt/kafka/krb5/kafka-producer.keytab" principal="kafka-producer/ip-192-168-0-207.us-east-2.compute.internal@INTERNAL"; +kafka.sasl.kerberos.service.name=kafka +---- diff --git a/content/docs/smallrye-reactive-messaging/main/kafka/protobuf-configuration.adoc b/content/docs/smallrye-reactive-messaging/main/kafka/protobuf-configuration.adoc new file mode 100644 index 000000000..36e025d1e --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/kafka/protobuf-configuration.adoc @@ -0,0 +1,51 @@ += Using Google Protobuf serializer/deserializer +:version-root: ../ +include::{version-root}_attributes.adoc[] + +If you are using https://developers.google.com/protocol-buffers/[Protocol +Buffers] +serializer/deserializer, please note the following configuration +properties. + +== For https://docs.confluent.io/platform/current/schema-registry/serdes-develop/serdes-protobuf.html#serdes-and-formatter-protobuf[Confluent] Schema Registry + +Confluent protobuf library is `io.confluent:kafka-protobuf-serializer`. +Note that this library is not available in Maven Central, you need to +use the https://docs.confluent.io/clients-kafka-java/current/overview.html[Confluent Maven +repository]. + +=== Consumer + +[width="100%",cols="49%,51%",options="header",] +|=== +|Property |Recommended value +|value.deserializer |io.confluent.kafka.serializers.protobuf.KafkaProtobufDeserializer +|schema.registry.url |http://%7Byour_host%7D:%7Byour_port%7D/ +|mp.messaging.incoming.++[++channel++]++.specific.protobuf.value.type |your.package.DomainObjectKey$Key +|mp.messaging.incoming.++[++channel++]++.specific.protobuf.key.type |your.package.DomainObjectValue$Value +|=== + +Example: + +.... +mp.messaging.incoming.[channel].value.deserializer=io.confluent.kafka.serializers.protobuf.KafkaProtobufDeserializer +mp.messaging.incoming.[channel].schema.registry.url=http://{your_host}:{your_port}/ +mp.messaging.incoming.[channel].specific.protobuf.value.type=your.package.DomainObjectKey$Key +mp.messaging.incoming.[channel].specific.protobuf.key.type=your.package.DomainObjectValue$Value +.... + +=== Producer + +[width="100%",cols="24%,76%",options="header",] +|=== +|Property |Recommended value +|value.serializer |io.confluent.kafka.serializers.protobuf.KafkaProtobufSerializer +|schema.registry.url |http://%7Byour_host%7D:%7Byour_port%7D/ +|=== + +Example: + +.... +mp.messaging.outgoing.[channel].value.serializer=io.confluent.kafka.serializers.protobuf.KafkaProtobufSerializer +mp.messaging.outgoing.[channel].schema.registry.url=http://{your_host}:{your_port}/ +.... diff --git a/content/docs/smallrye-reactive-messaging/main/kafka/receiving-kafka-records.adoc b/content/docs/smallrye-reactive-messaging/main/kafka/receiving-kafka-records.adoc new file mode 100644 index 000000000..9fb15edc9 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/kafka/receiving-kafka-records.adoc @@ -0,0 +1,1076 @@ += Receiving Kafka Records +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The Kafka Connector retrieves Kafka Records from Kafka Brokers and maps +each of them to Reactive Messaging `Messages`. + +== Example + +Let’s imagine you have a Kafka broker running, and accessible using the +`kafka:9092` address (by default it would use `localhost:9092`). +Configure your application to receive Kafka records from a Kafka _topic_ +on the `prices` channel as follows: + +[source,properties] +---- +kafka.bootstrap.servers=kafka:9092 # <1> + +mp.messaging.incoming.prices.connector=smallrye-kafka # <2> +mp.messaging.incoming.prices.value.deserializer=org.apache.kafka.common.serialization.DoubleDeserializer # <3> +mp.messaging.incoming.prices.broadcast=true # <4> +---- + +[arabic] +. Configure the broker location. You can configure it globally or per +channel +. Configure the connector to manage the `prices` channel +. Sets the (Kafka) deserializer to read the record’s value +. Make sure that we can receive from more than one consumer (see +`KafkaPriceConsumer` and `KafkaPriceMessageConsumer` below) + +[NOTE] +==== + +You don’t need to set the Kafka topic. By default, it uses the channel +name (`prices`). You can configure the `topic` attribute to override it. + +==== + +Then, your application receives `Message++<++Double++>++`. You can consume the +payload directly: + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaPriceConsumer.java[indent=0] +---- + +Or, you can retrieve the `Message++<++Double++>++`: + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaPriceMessageConsumer.java[indent=0] +---- + +== Deserialization + +The deserialization is handled by the underlying Kafka Client. You need +to configure the: + +* `mp.messaging.incoming.++[++channel-name++]++.value.deserializer` to +configure the value deserializer (mandatory) +* `mp.messaging.incoming.++[++channel-name++]++.key.deserializer` to configure +the key deserializer (optional, default to `String`) + +If you want to use a custom deserializer, add it to your `CLASSPATH` and +configure the associate attribute. + +In addition, the Kafka Connector also provides a set of _message +converters_. So you can receive _payloads_ representing records from +Kafka using: + +* link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-kafka/{javadoc-version}/io/smallrye/reactive/messaging/kafka/Record.html[`Record`] - a pair key/value +* https://kafka.apache.org/26/javadoc/index.html?org/apache/kafka/clients/consumer/ConsumerRecord.html[ConsumerRecord++<++K,V++>++] - +a structure representing the record with all its metadata + +[source,java] +---- +include::{examples-dir}/kafka/inbound/Converters.java[tags=code,indent=0] +---- + +== Inbound Metadata + +Messages coming from Kafka contains an instance of +link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-kafka/{javadoc-version}/io/smallrye/reactive/messaging/kafka/api/IncomingKafkaRecordMetadata.html[`IncomingKafkaRecordMetadata`] +in the metadata. It provides the key, topic, partitions, +headers and so on: + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaMetadataExample.java[tags=code,indent=0] +---- + +== Acknowledgement + +When a message produced from a Kafka record is acknowledged, the +connector invokes a _commit strategy_. These strategies decide when the +consumer offset for a specific topic/partition is committed. Committing +an offset indicates that all previous records have been processed. It is +also the position where the application would restart the processing +after a crash recovery or a restart. + +Committing every offset has performance penalties as Kafka offset +management can be slow. However, not committing the offset often enough +may lead to message duplication if the application crashes between two +commits. + +The Kafka connector supports three strategies: + +* `throttled` keeps track of received messages and commit to the next +offset after the latest _acked_ message in sequence. This strategy +guarantees _at-least-once delivery_ even if the channel performs +asynchronous processing. The connector tracks the received records +and periodically (period specified by `auto.commit.interval.ms` +(default: 5000)) commits the highest consecutive offset. The +connector will be marked as unhealthy if a message associated with a +record is not acknowledged in +`throttled.unprocessed-record-max-age.ms` (default: 60000). Indeed, +this strategy cannot commit the offset as soon as a single record +processing fails (see failure-strategy to configure what happens on +failing processing). If `throttled.unprocessed-record-max-age.ms` is +set to less than or equal to 0, it does not perform any health check +verification. Such a setting might lead to running out of memory if +there are poison pill messages. This strategy is the default if +`enable.auto.commit` is not explicitly set to `true`. ++ +The `throttled` strategy supports concurrent processing with ordering +guarantees using the `throttled.ordered` configuration. See +xref:#concurrent-processing-with-ordering-guarantees[Concurrent Processing with Ordering Guarantees] +for more details. +* `checkpoint` allows persisting consumer offsets on a "state store", +instead of committing them back to the Kafka broker. Using the +`CheckpointMetadata` API, consumer code can persist a processing +state with the offset to mark the progress of a consumer. +When the processing continues from a previously persisted offset, +it seeks the Kafka consumer to that offset and also restores the +persisted state, continuing the stateful processing from where it +left off. The `checkpoint` strategy holds locally the processing +state associated with the latest offset, and persists it +periodically to the state store (period specified by +`auto.commit.interval.ms` (default: 5000)). The connector will be +marked as unhealthy if no processing state is persisted to the state +store in `checkpoint.unsynced-state-max-age.ms` (default: 10000). +Using the `CheckpointMetadata` API the user code can force to persist +the state on message ack. If `checkpoint.unsynced-state-max-age.ms` +is set to less than or equal to 0, it does not perform any health +check verification. For more information, see +xref:#stateful-processing-with-checkpointing[Stateful processing with Checkpointing] +* `latest` commits the record offset received by the Kafka consumer as +soon as the associated message is acknowledged (if the offset is +higher than the previously committed offset). This strategy provides +_at-least-once_ delivery if the channel processes the message +without performing any asynchronous processing. This strategy should +not be used on high-load as offset commit is expensive. However, it +reduces the risk of duplicates. +* `ignore` performs no commit. This strategy is the default strategy +when the consumer is explicitly configured with `enable.auto.commit` +to `true`. It delegates the offset commit to the Kafka client. When +`enable.auto.commit` is `true` this strategy *DOES NOT* guarantee +at-least-once delivery. However, if the processing failed between +two commits, messages received after the commit and before the +failure will be re-processed. + +[IMPORTANT] +==== + +The Kafka connector disables the Kafka _auto commit_ if not explicitly +enabled. This behavior differs from the traditional Kafka consumer. + +==== + +If high-throughout is important for you, and not limited by the +downstream, we recommend to either: + +* Use the `throttled` policy +* or set `enable.auto.commit` to `true` and annotate the consuming +method with `@Acknowledgment(Acknowledgment.Strategy.NONE)` + +== Backpressure with Pause/Resume + +The Kafka connector automatically applies backpressure using the +`pause-if-no-requests` mechanism (default: `true`). +The connector buffers records fetched from Kafka and _pauses_ the +consumer when the buffer is full +(`max.poll.records ++*++ max-queue-size-factor`, default: 1000), +then _resumes_ when it drains to `max.poll.records` (default: 500). + +When paused, the connector does not stop polling the Kafka consumer. +Instead, it polls with a zero timeout so that no records are retrieved, +but the consumer continues to send heartbeats to the broker. +This prevents the consumer from being considered dead and kicked out +of the consumer group. + +For manual per-partition pause and resume, see the +xref:kafka/client-service.adoc#manual-partition-pauseresume[KafkaClientService] +section. + +== Concurrent Processing with Ordering Guarantees + +[WARNING] +.Experimental +==== + +Ordered concurrent processing is experimental, and APIs and features are subject to change. + +==== + +When processing Kafka messages concurrently (using `@Blocking(ordered = false)`), +you can configure ordering guarantees using the `ordered` attribute. + +=== Choosing a Commit Strategy + +* *`ordered=key`*: Messages from the same partition but with different keys can be acknowledged +out of order. Use `commit-strategy=throttled`, which tracks out-of-order acknowledgements and +commits offsets correctly. +* *`ordered=partition`*: Messages within a partition are processed sequentially, so +acknowledgements are always in order. This works with any commit strategy, including +`commit-strategy=ignore` for exactly-once transactional processing. + +=== Processing Order Modes + +* *`unordered`* (default): Messages are processed without any ordering guarantees, +allowing maximum concurrency across all partitions and keys. +* *`key`*: Messages with the same key from the same topic-partition are +processed sequentially, while messages with different keys or from different +partitions can be processed concurrently. This ensures per-key ordering within +each partition. +* *`partition`*: Messages from the same topic-partition are processed +sequentially, but messages from different partitions can be processed concurrently. +This ensures partition-level ordering while still allowing parallelism across +partitions. + +=== Examples + +Configure the `ordered` property and use the `@Blocking(ordered = false)` annotation +to enable concurrent processing with ordering guarantees: + +[source,properties] +---- +mp.messaging.incoming.orders.connector=smallrye-kafka +mp.messaging.incoming.orders.topic=orders +mp.messaging.incoming.orders.commit-strategy=throttled +mp.messaging.incoming.orders.ordered=key + +# Configure worker pool concurrency for thread pool size +smallrye.messaging.worker.order-pool.max-concurrency=10 + +# Configure merge concurrency (how many groups can process concurrently) +# Should typically match or be less than worker pool max-concurrency +mp.messaging.incoming.orders.ordered.max-concurrency=10 +---- + +[source,java] +---- +@Incoming("orders") +@Blocking(ordered = false, value = "order-pool") +public void processOrder(Order order) { + // Orders with the same key are processed sequentially + // Orders with different keys can be processed concurrently +} +---- + +For exactly-once transactional processing, use `ordered=partition` with `commit-strategy=ignore` +to avoid dual offset commits: + +[source,properties] +---- +mp.messaging.incoming.data.connector=smallrye-kafka +mp.messaging.incoming.data.topic=data +mp.messaging.incoming.data.commit-strategy=ignore +mp.messaging.incoming.data.ordered=partition +---- + +In order to allow creating more groups (of keys or partitions) that can be processed concurrently, +each group buffers ordered messages to process. +The `max-queue-size-factor` (default: 2) attribute controls the buffer size per group, calculated as +`max.poll.records ++*++ max-queue-size-factor`. +The `ordered.max-concurrency` attribute controls how many groups can be processed concurrently. +If not set, it defaults to `max.poll.records`. +When buffers of all groups are full, backpressure is applied to pause consumption. + +[NOTE] +.Deprecation +==== + +The `throttled.ordered` and `throttled.ordered.max-concurrency` attributes are deprecated. +Use `ordered` and `ordered.max-concurrency` instead, which work with any commit strategy. + +==== + +== Failure Management + +If a message produced from a Kafka record is _nacked_, a failure +strategy is applied. The Kafka connector supports 3 strategies: + +* `fail` - fail the application, no more records will be processed. +(default) The offset of the record that has not been processed +correctly is not committed. +* `ignore` - the failure is logged, but the processing continue. The +offset of the record that has not been processed correctly is +committed. +* `dead-letter-queue` - the offset of the record that has not been +processed correctly is committed, but the record is written to a +(Kafka) _dead letter queue_ topic. +* `delayed-retry-topic` - the offset of the record that has not been +processed correctly is still committed, but the record is written to +a series of Kafka topics for retrying the processing with some delay. +This allows retrying failed records by reconsuming them later without +blocking the processing of the latest records. + +The strategy is selected using the `failure-strategy` attribute. + +=== Dead Letter Queue + +In the case of `dead-letter-queue`, you can configure the following +attributes: + +* `dead-letter-queue.topic`: the topic to use to write the records not +processed correctly, default is `dead-letter-topic-$channel`, with +`$channel` being the name of the channel. +* `dead-letter-queue.producer-client-id`: the client id used by the kafka +producer when sending records to dead letter queue topic. If not specified +it will default to `kafka-dead-letter-topic-producer-$client-id`, with $client-id +being the value obtained from consumer client id. +* `dead-letter-queue.key.serializer`: the serializer used to write the +record key on the dead letter queue. By default, it deduces the +serializer from the key deserializer. +* `dead-letter-queue.value.serializer`: the serializer used to write +the record value on the dead letter queue. By default, it deduces +the serializer from the value deserializer. + +The record written on the dead letter topic contains the original +record’s headers, as well as a set of additional headers about the +original record: + +* `dead-letter-reason` - the reason of the failure (the `Throwable` +passed to `nack()`) +* `dead-letter-cause` - the cause of the failure (the `getCause()` of +the `Throwable` passed to `nack()`), if any +* `dead-letter-topic` - the original topic of the record +* `dead-letter-partition` - the original partition of the record +(integer mapped to String) +* `dead-letter-offset` - the original offset of the record (long +mapped to String) + +When using `dead-letter-queue`, it is also possible to change some +metadata of the record that is sent to the dead letter topic. To do +that, use the `Message.nack(Throwable, Metadata)` method: + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaDeadLetterExample.java[tags=code,indent=0] +---- + +The `Metadata` may contain an instance of `OutgoingKafkaRecordMetadata`. +If the instance is present, the following properties will be used: + +* key; if not present, the original record’s key will be used +* topic; if not present, the configured dead letter topic will be used +* partition; if not present, partition will be assigned automatically +* headers; combined with the original record’s headers, as well as the +`dead-letter-++*++` headers described above + +=== Delayed Retry Topic + +[NOTE] +==== + +Delayed retry topic feature is experimental. + +==== + +The delayed retry topic strategy allows failed records to be automatically retried by forwarding them to a series of retry topics. +Each retry topic is associated with a specific delay time, which is expressed in milliseconds. +When a record processing fails, it is forwarded to the first retry topic. +The failure strategy then consumes these records and dispatches them to be retried again once the delay time of the topic has elapsed. + +If the processing of a record fails again, the message is forwarded to the next topic in the list, with possibly a longer delay time. +If the processing of a record keeps failing, it will eventually be abandoned. +Alternatively, if the `dead-letter-queue.topic` property is configured, the record will be sent to the dead letter queue. + +The Kafka producer client used when forwarding records to retry topics can be configured using the _dead-letter-queue_ properties +namely, `dead-letter-queue.producer-client-id`, `dead-letter-queue.key.serializer` and `dead-letter-queue.value.serializer`. + +Delayed retry topics and delays can be configured with following attributes: + +* `delayed-retry-topic.topics` : +The comma-separated list of retry topics, each one suffixed with `++_[++DELAY++_++IN++_++MILLISECONDS++]++` for indicating the delay time. +For example, `my++_++retry++_++topic++_++2000,my++_++retry++_++topic++_++4000,my++_++retry++_++topic++_++10000` will use three topics +_my++_++retry++_++topic++_++2000_, _my++_++retry++_++topic++_++4000_ and _my++_++retry++_++topic++_++10000_, with 2000ms 4000ms and 10000ms respectively. ++ +If not configured the source channel name is used, with 10, 20 and 50 seconds of delay, +ex. for a channel named `source`, retry topics will be `source++_++retry++_++10000`, `source++_++retry++_++20000`, `source++_++retry++_++50000`. +* `delayed-retry-topic.max-retries` : +The maximum number of retries before abandoning the retries. +If configured higher than the number of retry topics the last topic is used until maximum number of retries is reached. +This can be configured to use a single retry topic with a fixed delay and multiple retries. ++ +For example, `delayed-retry-topic.topics=source++_++retry++_++10000` and `delayed-retry-topic.max-retries=4` will forward failed records +to the topic _source++_++retry++_++10000_ with maximum of 4 retries. +* `delayed-retry-topic.timeout` : +The global timeout in milliseconds for a retried record. +The timeout is calculated from the first failure for a record. +If the next retry will reach the timeout, instead of forwarding to the retry topic the retry is abandoned and, +if configured, the record is forwarded to the dead letter queue. ++ +The default is 120 seconds. + +[IMPORTANT] +==== + +While you can use xref:#retrying-processing[Smallrye Fault Tolerance to retry processing], +it will block the processing of further messages until the retried record is processed successfully, or abandoned. + +Delayed retry topic failure strategy allows effectively implementing non-blocking retries. +But it will not preserve the order of messages inside a topic-partition. + +==== + +The record written on the delayed retry topics will preserve the key and partition of the original record. +It also contains the original record’s headers, as well as a set of additional headers about the original record: + +* `delayed-retry-count` the current number of retries +* `delayed-retry-original-timestamp` the original timestamp of the record +* `delayed-retry-first-processing-timestamp` the first processing timestamp of the record +* `delayed-retry-reason` the reason of the failure (the `Throwable` passed to `nack()`) +* `delayed-retry-cause` the cause of the failure (the `getCause()` of the `Throwable` passed to `nack()`), if any +* `delayed-retry-topic` the original topic of the record +* `delayed-retry-partition` the original partition of the record +* `delayed-retry-offset` the original offset of the record +* `delayed-retry-exception-class-name` the class name of the throwable passed to `nack()` +* `delayed-retry-cause-class-name` the class name of the the `getCause()` of the `Throwable` passed to `nack()`, if any + +As for the dead letter queue it is possible to change forwarded values by providing a `OutgoingKafkaRecordMetadata` +when the message is nacked using `Message.nack(Throwable, Metadata)`. + +[NOTE] +.Multiple partitions +==== + +The delayed retry topic strategy does not create retry topics automatically. +If the source topic has multiple partitions, delayed retry and dead letter queue topics would need to be setup with the same number of partitions. + +It is possible to scale consumer application instances according to the number of partitions. +But it is not guaranteed that the retry topics consumer will be assigned the same partition(s) as the main topic consumer. +Therefore, retry processing of a record can happen in an other instance. + +==== + +== Custom commit and failure strategies + +In addition to provided strategies, it is possible to implement custom +commit and failure strategies and configure Kafka channels with them. + +For example, for a custom commit strategy, implement the +link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-kafka/{javadoc-version}/io/smallrye/reactive/messaging/kafka/commit/KafkaCommitHandler.html[`KafkaCommitHandler`] interface, +and provide a managed bean implementing the `KafkaCommitHandler.Factory` interface, +identified using `@Identifier` qualifier. + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaCustomCommit.java[indent=0] +---- + +Finally, to use the custom commit strategy, +set the `commit-strategy` attribute to the identifier of the commit handler factory: +`mp.messaging.incoming.$channel.commit-strategy=custom`. +Similarly, custom failure strategies can be configured using `failure-strategy` attribute. + +[NOTE] +==== + +If the custom strategy implementation inherits +link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-kafka/{javadoc-version}/io/smallrye/reactive/messaging/kafka/commit/ContextHolder.html[`ContextHolder`] class it can access the +Vert.x event-loop context created for the Kafka consumer + +==== + +== Retrying processing + +You can combine Reactive Messaging with https://github.com/smallrye/smallrye-fault-tolerance[SmallRye Fault +Tolerance], and +retry processing when it fails: + +[source,java] +---- +@Incoming("kafka") +@Outgoing("processed") +@Retry(delay = 10, maxRetries = 5) +public String process(String v) { + // ... retry if this method throws an exception +} +---- + +You can configure the delay, the number of retries, the jitter... + +If your method returns a `Uni`, you need to add the `@NonBlocking` +annotation: + +[source,java] +---- +@Incoming("kafka") +@Outgoing("processed") +@Retry(delay = 10, maxRetries = 5) +@NonBlocking +public Uni process(String v) { + // ... retry if this method throws an exception or the returned Uni produce a failure +} +---- + +The incoming messages are acknowledged only once the processing +completes successfully. So, it commits the offset after the successful +processing. If after the retries the processing still failed, the +message is _nacked_ and the failure strategy is applied. + +You can also use `@Retry` on methods only consuming incoming messages: + +[source,java] +---- +@Incoming("kafka") +@Retry(delay = 10, maxRetries = 5) +public void consume(String v) { + // ... retry if this method throws an exception +} +---- + +== Handling deserialization failures + +Because deserialization happens before creating a `Message`, the failure +strategy presented above cannot be applied. However, when a +deserialization failure occurs, you can intercept it and provide a +fallback value. To achieve this, create a CDI bean implementing the +link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-kafka/{javadoc-version}/io/smallrye/reactive/messaging/kafka/api/DeserializationFailureHandler.html[`DeserializationFailureHandler`] +interface: + +[source,java] +---- +@ApplicationScoped +@Identifier("failure-retry") // Set the name of the failure handler +public class MyDeserializationFailureHandler + implements DeserializationFailureHandler { // Specify the expected type + + @Override + public JsonObject decorateDeserialization(Uni deserialization, + String topic, boolean isKey, String deserializer, byte[] data, + Headers headers) { + return deserialization + .onFailure().retry().atMost(3) + .await().atMost(Duration.ofMillis(200)); + } +} +---- + +The bean must be exposed with the `@Identifier` qualifier specifying the +name of the bean. Then, in the connector configuration, specify the +following attribute: + +* `mp.messaging.incoming.$channel.key-deserialization-failure-handler`: +name of the bean handling deserialization failures happening for the +record’s key +* `mp.messaging.incoming.$channel.value-deserialization-failure-handler`: +name of the bean handling deserialization failures happening for the +record’s value, + +The handler is called with the deserialization action as a `Uni++<++T++>++`, the +record’s topic, a boolean indicating whether the failure happened on a +key, the class name of the deserializer that throws the exception, the +corrupted data, the exception, and the records headers augmented with +headers describing the failure (which ease the write to a dead letter). +On the deserialization `Uni` failure strategies like retry, providing a +fallback value or applying timeout can be implemented. Note that the +method must await on the result and return the deserialized object. +Alternatively, the handler can only implement +`handleDeserializationFailure` method and provide a fallback value, +which may be `null`. + +If you don’t configure a deserialization failure handlers and a +deserialization failure happens, the application is marked unhealthy. +You can also ignore the failure, which will log the exception and +produce a `null` value. To enable this behavior, set the +`mp.messaging.incoming.$channel.fail-on-deserialization-failure` +attribute to `false`. + +If the `fail-on-deserialization-failure` attribute is set to `false` and +the `failure-strategy` attribute is `dead-letter-queue` the failed record +will be sent to the corresponding _dead letter queue_ topic. +The forwarded record will have the original key and value, +and the following headers set: + +* `deserialization-failure-reason`: The deserialization failure message +* `deserialization-failure-cause`: The deserialization failure cause if any +* `deserialization-failure-key`: Whether the deserialization failure happened on a key +* `deserialization-failure-topic`: The topic of the incoming message when a deserialization failure happen +* `deserialization-failure-deserializer`: The class name of the underlying deserializer +* `deserialization-failure-key-data`: If applicable the key data that was not able to be deserialized +* `deserialization-failure-value-data`: If applicable the value data that was not able to be deserialized + +== Receiving Cloud Events + +The Kafka connector supports https://cloudevents.io/[Cloud Events]. +When the connector detects a _structured_ or _binary_ Cloud Events, it +adds a link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-kafka/{javadoc-version}/io/smallrye/reactive/messaging/kafka/IncomingKafkaCloudEventMetadata.html[`IncomingKafkaCloudEventMetadata`] in the metadata of the +Message. `IncomingKafkaCloudEventMetadata` contains the various (mandatory and optional) Cloud Event attributes. + +If the connector cannot extract the Cloud Event metadata, it sends the +Message without the metadata. + +=== Binary Cloud Events + +For `binary` Cloud Events, *all* mandatory Cloud Event attributes must +be set in the record header, prefixed by `ce++_++` (as mandated by the +https://github.com/cloudevents/spec/blob/v1.0/kafka-protocol-binding.md[protocol +binding]). +The connector considers headers starting with the `ce++_++` prefix but not +listed in the specification as extensions. You can access them using the +`getExtension` method from `IncomingKafkaCloudEventMetadata`. You can +retrieve them as `String`. + +The `datacontenttype` attribute is mapped to the `content-type` header +of the record. The `partitionkey` attribute is mapped to the record’s +key, if any. + +Note that all headers are read as UTF-8. + +With binary Cloud Events, the record’s key and value can use any +deserializer. + +=== Structured Cloud Events + +For `structured` Cloud Events, the event is encoded in the record’s +value. Only JSON is supported, so your event must be encoded as JSON in +the record’s value. + +Structured Cloud Event must set the `content-type` header of the record +to `application/cloudevents` or prefix the value with +`application/cloudevents` such as: +`application/cloudevents{plus}json; charset=UTF-8`. + +To receive structured Cloud Events, your value deserializer must be: + +* `org.apache.kafka.common.serialization.StringDeserializer` +* `org.apache.kafka.common.serialization.ByteArrayDeserializer` +* `io.vertx.kafka.client.serialization.JsonObjectDeserializer` + +As mentioned previously, the value must be a valid JSON object +containing at least all the mandatory Cloud Events attributes. + +If the record is a structured Cloud Event, the created Message’s payload +is the Cloud Event `data`. + +The `partitionkey` attribute is mapped to the record’s key if any. + +== Consumer Rebalance Listener + +To handle offset commit and assigned partitions yourself, you can +provide a consumer rebalance listener. To achieve this, implement the +`io.smallrye.reactive.messaging.kafka.KafkaConsumerRebalanceListener` +interface, make the implementing class a bean, and add the `@Identifier` +qualifier. A usual use case is to store offset in a separate data store +to implement exactly-once semantic, or starting the processing at a +specific offset. + +The listener is invoked every time the consumer topic/partition +assignment changes. For example, when the application starts, it invokes +the `partitionsAssigned` callback with the initial set of +topics/partitions associated with the consumer. If, later, this set +changes, it calls the `partitionsRevoked` and `partitionsAssigned` +callbacks again, so you can implement custom logic. + +Note that the rebalance listener methods are called from the Kafka +_polling_ thread and must block the caller thread until completion. +That’s because the rebalance protocol has synchronization barriers, and +using asynchronous code in a rebalance listener may be executed after +the synchronization barrier. + +When topics/partitions are assigned or revoked from a consumer, it +pauses the message delivery and restarts once the rebalance completes. + +If the rebalance listener handles offset commit on behalf of the user +(using the `ignore` commit strategy), the rebalance listener *must* +commit the offset synchronously in the `partitionsRevoked` callback. We +also recommend applying the same logic when the application stops. + +Unlike the `ConsumerRebalanceListener` from Apache Kafka, the +`io.smallrye.reactive.messaging.kafka.KafkaConsumerRebalanceListener` +methods pass the Kafka `Consumer` and the set of topics/partitions. + +=== Example + +In this example we set-up a consumer that always starts on messages from +at most 10 minutes ago (or offset 0). First we need to provide a bean +that implements the +`io.smallrye.reactive.messaging.kafka.KafkaConsumerRebalanceListener` +interface and is annotated with `@Identifier`. We then must configure +our inbound connector to use this named bean. + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaRebalancedConsumerRebalanceListener.java[indent=0] +---- + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaRebalancedConsumer.java[indent=0] +---- + +To configure the inbound connector to use the provided listener we +either set the consumer rebalance listener’s name: + +* `mp.messaging.incoming.rebalanced-example.consumer-rebalance-listener.name=rebalanced-example.rebalancer` + +Or have the listener’s name be the same as the group id: + +* `mp.messaging.incoming.rebalanced-example.group.id=rebalanced-example.rebalancer` + +Setting the consumer rebalance listener’s name takes precedence over +using the group id. + +== Receiving Kafka Records in Batches + +By default, incoming methods receive each Kafka record individually. +Under the hood, Kafka consumer clients poll the broker constantly and +receive records in batches, presented inside the `ConsumerRecords` +container. + +In *batch* mode, your application can receive all the records returned +by the consumer *poll* in one go. + +To achieve this you need to set +`mp.messaging.incoming.$channel.batch=true` *and* specify a compatible +container type to receive all the data: + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaRecordBatchPayloadExample.java[tags=code,indent=0] +---- + +The incoming method can also receive `Message++<++List++<++Payload++>++` or `ConsumerRecords++<++Key, Payload++>++` types, They +give access to record details such as offset or timestamp : + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaRecordBatchExample.java[tags=code,indent=0] +---- + +Note that the successful processing of the incoming record batch will +commit the latest offsets for each partition received inside the batch. +The configured commit strategy will be applied for these records only. + +Conversely, if the processing throws an exception, all messages are +_nacked_, applying the failure strategy for all the records inside the +batch. + +=== Accessing metadata of batch records + +When receiving records in batch mode, the metadata of each record is accessible through the `IncomingKafkaRecordBatchMetadata` : + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaRecordBatchExample.java[tags=batch,indent=0] +---- + +Like in this example, this can be useful to propagate the tracing information of each record. + +== Manual topic-partition assignment + +The default behavior of Kafka incoming channels is to subscribe to one or more topics in order to receive records from the Kafka broker. +Channel attributes `topic` and `topics` allow specifying topics to subscribe to, +or `pattern` attribute allows to subscribe to all topics matching a regular expression. +Subscribing to topics allows partitioning consumption of topics by dynamically assigning (rebalancing) partitions between members of a consumer group. + +The `assign-seek` configuration attribute allows manually assigning topic-partitions to a Kafka incoming channel, +and optionally seek to a specified offset in the partition to start consuming records. +If `assign-seek` is used, the consumer will not be dynamically subscribed to topics, +but instead will statically assign the described partitions. +In manual topic-partition rebalancing doesn't happen and therefore rebalance listeners are never called. + +The attribute takes a list of triplets separated by commas: `++<++topic++>++:++<++partition++>++:++<++offset++>++`. + +For example, the following configuration + +[source,properties] +---- +mp.messaging.incoming.data.assign-seek=topic1:0:10, topic2:1:20 +---- + +assigns the consumer to: +- Partition 0 of topic 'topic1', setting the initial position at offset 10. +- Partition 1 of topic 'topic2', setting the initial position at offset 20. + +The topic, partition, and offset in each triplet can have the following variations: +- If the topic is omitted, the configured `topic` will be used. +- If the offset is omitted, partitions are assigned to the consumer but won't be seeked to offset. +- If offset is 0, it seeks to the beginning of the topic-partition. +- If offset is -1, it seeks to the end of the topic-partition. + +== Stateful processing with Checkpointing + +[CAUTION] +.Deprecated +==== + +Checkpointing is deprecated and will be removed in a future version. + +==== + +The `checkpoint` commit strategy allows for a Kafka incoming channel to +manage topic-partition offsets, not by committing on the Kafka broker, +but by persisting consumers' advancement on a +xref:#implementing-state-stores[_state store_]. + +In addition to that, if the consumer builds an internal state as +a result of consumed records, the topic-partition offset persisted +to the state store can be associated with a _processing state_, +saving the local state to the persistent store. When a consumer +restarts or consumer group instances scale, i.e. when new partitions +get assigned to the consumer, the checkpointing works by resuming the +processing from the latest offset and its saved state. + +The `@Incoming` channel consumer code can manipulate the processing +state through the `CheckpointMetadata` API: + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaCheckpointExample.java[tags=code,indent=0] +---- + +The `transform` method allows applying a transformation function to +the current state, producing a changed state and registering it +locally for checkpointing. By default, the local state is synced +(persisted) to the state store periodically, period specified by +`auto.commit.interval.ms`, (default: 5000). If `persistOnAck` flag +is given, the latest state is persisted to the state store eagerly +on message acknowledgment. The `setNext` method works similarly +directly setting the latest state. + +The `checkpoint` commit strategy tracks when a processing state +is last persisted for each topic-partition. If an outstanding state +change can not be persisted for `checkpoint.unsynced-state-max-age.ms` +(default: 10000), the channel is marked unhealthy. + +Where and how processing states are persisted is decided by the +state store implementation. This can be configured on the incoming +channel using `checkpoint.state-store` configuration property, +using the state store factory identifier name. +The serialization of state objects depends on the state store +implementation. In order to instruct state stores for serialization +can require configuring the type name of state objects +using `checkpoint.state-type` property. + +In order to keep Smallrye Reactive Messaging free of persistence-related +dependencies, this library includes only a default state store named `file`. +It is based on Vert.x Filesystem API and stores the processing state +in Json formatted files, in a local directory configured by the +`checkpoint.file.state-dir` property. State files follow the naming +scheme `++[++consumer-group-id++]++:++[++topic++]++:++[++partition++]++`. + +=== Implementing State Stores + +State store implementations are required to implement `CheckpointStateStore` +interface, and provide a managed bean implementing +`CheckpointStateStore.Factory`, identified with `@Identifier` bean +qualifier indicating the name of the state-store. +The factory bean identifier indicates the name to configure on +`checkpoint.state-store` config property. +The factory is discovered as a CDI managed bean and state store is +created once per channel: + +[source,java] +---- +include::{examples-dir}/kafka/inbound/MyCheckpointStateStore.java[indent=0] +---- + +The checkpoint commit strategy calls the state store in following events: + +* `fetchProcessingState` : on partitions assigned, to seek the consumer to the latest offset. +* `persistProcessingState` on partitions revoked, to persist the state of last processed record. +* `persistProcessingState` on message acknowledgement, if a new state is set during the processing and `persistOnAck` flag is set. +* `persistProcessingState` on `auto.commit.interval.ms` intervals, if a new state is set during processing. +* `persistProcessingState` on channel shutdown. +* `close` on channel shutdown. + +== Receiving Kafka Records Using Kafka Queues (Share Groups) + +[WARNING] +.Experimental +==== + +Kafka Queues (Share Groups) support is experimental. + +==== + +https://cwiki.apache.org/confluence/display/KAFKA/KIP-932[Kafka Share Groups] provide a new consumption model where records from a topic are cooperatively distributed across consumers without explicit partition assignment. +Unlike traditional consumer groups, share groups do not require managing offsets or partition rebalancing -- the broker handles record distribution and acquisition locks automatically. + +This is useful for *queue-like* workloads where you want records to be processed by exactly one consumer in a group, without worrying about partition count or consumer coordination. + +=== Enabling Share Groups + +To enable share group consumption on a channel, set the `share-group.enabled` attribute to `true`: + +[source,properties] +---- +mp.messaging.incoming.queue.connector=smallrye-kafka +mp.messaging.incoming.queue.topic=prices +mp.messaging.incoming.queue.value.deserializer=org.apache.kafka.common.serialization.DoubleDeserializer +mp.messaging.incoming.queue.share-group.enabled=true # <1> +---- + +[arabic] +. Enables share group consumption for this channel. + +Then consume records as usual: + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaShareGroupConsumer.java[indent=0] +---- + +By default, successfully processed records are acknowledged with `ACCEPT`. +If the processing method throws an exception, the record is released (`RELEASED`) and will be attempted for re-delivery. + +=== Controlling Acknowledgement + +Share groups support four acknowledgement types for each record: + +* *`ACCEPT`* Record processed successfully (default on ack). +* *`RELEASE`* Record could not be processed; release it for re-delivery to another consumer (default on nack). +* *`REJECT`* Record is permanently rejected and will not be re-delivered. +* *`RENEW`* Extend the acquisition lock for long-running processing (managed automatically when processing timeout is enabled). + +You can control the acknowledgement type using the `ShareGroupAcknowledgement` metadata, which is injected as a parameter in the consuming method: + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaShareGroupAckExample.java[tags=code,indent=0] +---- + +When the message is acked (or method completes normally), the connector uses the acknowledgement type set on `ShareGroupAcknowledgement`. +If no type is explicitly set, the default is `ACCEPT`. + +When calling `nack()` on the `Message`, you can pass the desired acknowledgement type via metadata: + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaShareGroupNackExample.java[tags=code,indent=0] +---- + +If no metadata is provided on nack, the default acknowledgement type is `RELEASE`. +This can be changed using the `share-group.failure-acknowledgement-type` attribute: + +[source,properties] +---- +mp.messaging.incoming.queue.share-group.enabled=true +mp.messaging.incoming.queue.share-group.failure-acknowledgement-type=reject # <1> +---- + +[arabic] +. Nacked records are permanently rejected instead of released for re-delivery. + +=== Handling Deserialization Failures + +When `fail-on-deserialization-failure` is set to `false`, records that fail deserialization are not forwarded to the consumer method. +They are automatically nacked using the acknowledgement type configured by `share-group.failure-deserialization-acknowledgement-type` (default: `reject`). + +[source,properties] +---- +mp.messaging.incoming.queue.share-group.enabled=true +mp.messaging.incoming.queue.fail-on-deserialization-failure=false # <1> +mp.messaging.incoming.queue.share-group.failure-deserialization-acknowledgement-type=reject # <2> +---- + +[arabic] +. Deserialization failures do not kill the application. +. Records that fail deserialization are permanently rejected (default). + +A custom `DeserializationFailureHandler` can override the acknowledgement type per record by setting the `deserialization-failure-share-ack-type` header +to the set `AcknowledgeType` value (e.g., `RELEASE`, `REJECT`, `ACCEPT`). + +=== Acquisition Lock Renewal and Processing Timeout + +Each record acquired by a share group consumer has a time-limited *acquisition lock* (default 30 seconds, configured on the broker via `group.share.record.lock.duration.ms`). +If the consumer does not acknowledge the record before the lock expires, the broker automatically releases it for re-delivery. + +The connector can periodically send `RENEW` acknowledgements to extend the lock for records still being processed. +This is controlled by the `share-group.unprocessed-record-max-age.ms` attribute: + +* When set to a value greater than `0` (default: `60000`), the connector enables *processing timeout monitoring*: +** Records still in progress are periodically renewed to prevent lock expiry. +** If a record exceeds the configured timeout, the connector reports a failure and marks the channel as unhealthy. +* When set to `0`, processing timeout monitoring is disabled. No `RENEW` acknowledgements are sent. If processing exceeds the broker's lock duration, the record will be automatically released and re-delivered. + +[source,properties] +---- +mp.messaging.incoming.queue.share-group.enabled=true +mp.messaging.incoming.queue.share-group.unprocessed-record-max-age.ms=30000 # <1> +---- + +[arabic] +. Records not processed within 30 seconds trigger a failure report. + +=== Batch Mode + +Share groups support batch consumption. Set `batch=true` to receive all records from a single poll as a batch: + +[source,properties] +---- +mp.messaging.incoming.queue.share-group.enabled=true +mp.messaging.incoming.queue.batch=true +---- + +When a batch is acknowledged, all individual records in the batch are acknowledged. +When a batch is nacked, all records are nacked with the specified acknowledgement type. + +You can control the acknowledgement type per record within a batch using the `ShareGroupAcknowledgement` metadata accessible through `IncomingKafkaRecordBatchMetadata`: + +[source,java] +---- +include::{examples-dir}/kafka/inbound/KafkaShareGroupBatchAckExample.java[tags=code,indent=0] +---- + +Each record in the batch has its own `ShareGroupAcknowledgement` instance. +When the batch is acked, each record is individually acknowledged using its configured type. +This allows accepting some records while rejecting or releasing others within the same batch. + +=== Incompatible Configuration + +The following configuration attributes are not supported with share groups and will be ignored: + +* `pattern` Share groups subscribe to explicit topics only. +* `assign-seek` Share groups do not support manual partition assignment. +* `partitions` Share groups handle record distribution automatically; multiple consumer instances are not needed. +* `commit-strategy` Share groups use their own acknowledgement mechanism. +* `failure-strategy` Share groups use their own failure handling based on `AcknowledgeType`. +* `consumer-rebalance-listener.name` Share groups do not have traditional partition rebalancing. + +If any of these are configured alongside `share-group.enabled=true`, a warning is logged at startup. + +== Configuration Reference + +include::{generated-dir}/smallrye-kafka-incoming.adoc[indent=0] + +You can also pass any property supported by the underlying https://kafka.apache.org/documentation/#consumerconfigs[Kafka +consumer]. + +For example, to configure the `max.poll.records` property, use: + +[source,properties] +---- +mp.messaging.incoming.[channel].max.poll.records=1000 +---- + +Some consumer client properties are configured to sensible default +values: + +If not set, `reconnect.backoff.max.ms` is set to `10000` to avoid high +load on disconnection. + +If not set, `key.deserializer` is set to +`org.apache.kafka.common.serialization.StringDeserializer`. + +The consumer `client.id` is configured according to the number of +clients to create. + +[WARNING] +.Deprecated +==== + +The `partitions` property (`mp.messaging.incoming.++[++channel++]++.partitions`) and the associated `requests` property are *deprecated*. +Use the `concurrency` channel attribute instead, which creates distinct indexed channels with their own consumer. + +==== + +* If a `client.id` is provided, it is used as-is or suffixed with +client index if the deprecated `partitions` property is set. +* If a `client.id` is not provided, it is generated as +`kafka-consumer-++[++channel++][++-index++]++`. diff --git a/content/docs/smallrye-reactive-messaging/main/kafka/request-reply.adoc b/content/docs/smallrye-reactive-messaging/main/kafka/request-reply.adoc new file mode 100644 index 000000000..4aedd2cc9 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/kafka/request-reply.adoc @@ -0,0 +1,175 @@ += Kafka Request/Reply +:version-root: ../ +include::{version-root}_attributes.adoc[] + +[WARNING] +.Tech Preview +==== + +Kafka Request Reply Emitter is a tech preview feature. + +==== + +The Kafka https://www.enterpriseintegrationpatterns.com/patterns/messaging/RequestReply.html[Request-Reply] pattern allows you to publish a message to a Kafka topic and then await for a reply message that responds to the initial request. + +The `KafkaRequestReply` emitter implements the requestor (or the client) of the request-reply pattern for Kafka outbound channels: + +[source,java] +---- +include::{examples-dir}/kafka/outbound/KafkaRequestReplyEmitter.java[indent=0] +---- + +The `request` method publishes the request record to the configured target topic of the outgoing channel, +and polls a reply topic (by default, the target topic with `-replies` suffix) for a reply record. +When the reply is received the returned `Uni` is completed with the record value. + +The request send operation generates a correlation id and sets a header (by default `REPLY++_++CORRELATION++_++ID`), +which it expects to be sent back in the reply record. Two additional headers are set on the request record: + +* The topic from which the reply is expected, by default `REPLY++_++TOPIC` header, +which can be configured using the `reply.topic` channel attribute. +* Optionally, the partition from which the reply is expected, by default `REPLY++_++PARTITION` header. +The reply partition header is added only when the Kafka request reply is configured specifically to receive records from a topic-partition +, using the `reply.partition` channel attribute. +The reply partition header integer value is encoded in 4 bytes, +and helper methods `KafkaRequestReply#replyPartitionFromBytes` and `KafkaRequestReply#replyPartitionToBytes` can be used for custom operations. + +The replier (or the server) can be implemented using a Reactive Messaging processor: + +[source,java] +---- +include::{examples-dir}/kafka/outbound/KafkaReplier.java[indent=0] +---- + +Kafka outgoing channels detect default `REPLY++_++CORRELATION++_++ID`, `REPLY++_++TOPIC` and `REPLY++_++PARTITION` headers +and send the reply record to the expected topic-partition by propagating back the correlation id header. + +Default headers can be configured, using `reply.correlation-id.header`, `reply.topic.header` and `reply.partition.header` channel attributes. +If custom headers are used the reply server needs some more manual work. +Given the following request/reply outgoing configuration: + +[source,properties] +---- +mp.messaging.outgoing.reqrep.topic=requests +mp.messaging.outgoing.reqrep.reply.correlation-id.header=MY_CORRELATION +mp.messaging.outgoing.reqrep.reply.topic.header=MY_TOPIC +mp.messaging.outgoing.reqrep.reply.partition.header=MY_PARTITION +mp.messaging.incoming.request.topic=requests +---- + +The reply server can be implemented as the following: + +[source,java] +---- +include::{examples-dir}/kafka/outbound/KafkaCustomHeaderReplier.java[indent=0] +---- + +== Requesting with `Message` types + +Like the core Emitter's `send` methods, `request` method also can receive a `Message` type and return a message: + +[source,java] +---- +include::{examples-dir}/kafka/outbound/KafkaRequestReplyMessageEmitter.java[indent=0] +---- + +[NOTE] +==== + +The ingested reply type of the `KafkaRequestReply` is discovered at runtime, +in order to configure a `MessageConveter` to be applied on the incoming message before returning the `Uni` result. + +==== + +== Requesting multiple replies + +You can use the `requestMulti` method to expect any number of replies represented by the `Multi` return type. + +For example this can be used to aggregate multiple replies to a single request. + +[source,java] +---- +include::{examples-dir}/kafka/outbound/KafkaRequestReplyMultiEmitter.java[indent=0] +---- + +Like the other `request` you can also request `Message` types. + +[NOTE] +==== + +The channel attribute `reply.timeout` will be applied between each message, if reached the returned `Multi` will +fail. + +==== + +== Scaling Request/Reply + +If multiple requestor instances are configured on the same outgoing topic, and the same reply topic, +each requestor consumer will generate a unique consumer group.id and +therefore all requestor instances will receive replies of all instances. If an observed correlation id doesn't match +the id of any pending replies, the reply is simply discarded. +With the additional network traffic this allows scaling requestors, (and repliers) dynamically. + +Alternatively, requestor instances can be configured to consume replies from dedicated topics using `reply.topic` attribute, +or distinct partitions of a single topic, using `reply.partition` attribute. +The later will configure the Kafka consumer to assign statically to the given partition. + +== Pending replies and reply timeout + +By default, the `Uni` returned from the `request` method is configured to fail with timeout exception if no replies is received after 5 seconds. +This timeout is configurable with the channel attribute `reply.timeout`. + +A snapshot of the list of pending replies is available through the `KafkaRequestReply#getPendingReplies` method. + +== Waiting for topic-partition assignment + +The requestor can be found in a position where a request is sent, and it's reply is already published to the reply topic, +before the requestor starts and polls the consumer. +In case the reply consumer is configured with `auto.offset.reset=latest`, which is the default value, this can lead to the requestor missing replies. + +If `auto.offset.reset` is `latest`, at wiring time, before any request can take place, the `KafkaRequestReply` +finds partitions that the consumer needs to subscribe and waits for their assignment to the consumer. +The timeout of the initial subscription can be adjusted with `reply.initial-assignment-timeout` which defaults to the `reply.timeout`. +If this timeout fails, `KafkaRequestReply` will enter an invalid state which will require it to be restarted. +If set to `-1`, the `KafkaRequestReply` will not wait for the initial assignment of the reply consumer to sent requests. + +On other occasions the `KafkaRequestReply#waitForAssignments` method can be used. + +== Correlation Ids + +The Kafka Request/Reply allows configuring the correlation id mechanism completely through a `CorrelationIdHandler` implementation. +The default handler is based on randomly generated UUID strings, written to byte array in Kafka record headers. +The correlation id handler implementation can be configured using the `reply.correlation-id.handler` attribute. +As mentioned the default configuration is `uuid`, +and an alternative `bytes` implementation can be used to generate 12 bytes random correlation ids. + +Custom handlers can be implemented by proposing a CDI-managed bean with `@Identifier` qualifier. + +== Reply Error Handling + +If the reply server produces an error and can or would like to propagate the error back to the requestor, failing the returned `Uni`. + +If configured using the `reply.failure.handler` channel attribute, +the `ReplyFailureHandler` implementations are discovered through CDI, matching the `@Identifier` qualifier. + +A sample reply error handler can lookup header values and return the error to be thrown by the reply: + +[source,java] +---- +include::{examples-dir}/kafka/outbound/MyReplyFailureHandler.java[indent=0] +---- + +`null` return value indicates that no error has been found in the reply record, and it can be delivered to the application. + +== Advanced configuration for the Kafka consumer + +The underlying Kafka consumer can be configured with the `reply` property prefix. +For example the underlying Kafka consuemer can be configured to batch mode using: + +[source,properties] +---- +mp.messaging.outgoing.reqrep.topic=requests +mp.messaging.outgoing.reqrep.reply.topic=quote-results +mp.messaging.outgoing.reqrep.reply.batch=true +mp.messaging.outgoing.reqrep.reply.commit-strategy=latest +---- diff --git a/content/docs/smallrye-reactive-messaging/main/kafka/test-companion.adoc b/content/docs/smallrye-reactive-messaging/main/kafka/test-companion.adoc new file mode 100644 index 000000000..d25aa111f --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/kafka/test-companion.adoc @@ -0,0 +1,158 @@ += Kafka Companion +:version-root: ../ +include::{version-root}_attributes.adoc[] + +[WARNING] +.Experimental +==== + +Kafka Companion is experimental and APIs are subject to change in the future. + +==== + +The *Kafka Companion* is a separate Java library for helping to test Kafka applications. +It is not intended to mock Kafka, but to the contrary, connect to a Kafka broker and provide high-level features. + +It is not limited to the SmallRye Reactive Messaging testing, but intends to improve the testability of applications using Kafka. Some of its features: + +* Running In-container Kafka broker for tests via https://github.com/strimzi/test-container[strimzi-test-container]. +* Running the Kafka broker behind a https://github.com/Shopify/toxiproxy[toxiproxy] for simulating network issues. +* Running embedded Kafka Kraft broker for tests. +* Base classes for tests to bootstrap tests. +* Companion classes for easily creating tasks to produce and consume Kafka records. +* Writing assertions for produce and consume tasks, state of consumers, topics, offsets etc. + +== Getting started writing tests + +Easiest way of starting to write Kafka tests is to extend `KafkaCompanionTestBase`. +It starts a single-node Kafka broker for the test suite using testcontainers and creates the `KafkaCompanion` to connect to this broker: + +[source,java] +---- +include::{examples-dir}/kafka/companion/KafkaWithBaseTest.java[tags=code,indent=0] +---- + +`KafkaCompanion` can be used on its own to connect to a broker: + +[source,java] +---- +include::{examples-dir}/kafka/companion/KafkaTest.java[tags=companion,indent=0] +---- + +There are a couple of things to note on how Kafka companion handles *producers*, *consumers* and *tasks*: + +`ProducerBuilder` and `ConsumerBuilder` lazy descriptions of with which configuration to create a producer or a consumer. + +`ProducerTask` and `ConsumerTask` run asynchronously on dedicated threads and are started as soon as they are created. +A task terminates when it is explicitly stopped, when it's predefined duration or number of records has been produced/consumed, or when it encounters an error. +An exterior thread can await on records processed, or simply on termination of the task. +At a given time produced or consumed records are accessible through the task. + +The actual creation of the producer or consumer happens when a task is started. When the task terminates the producer or the consumer is automatically closed. + +For example, in the previous example: + +[arabic] +. We described a producer with a client id `my-producer` and `max.block.ms` of 5 seconds. +. Then we created a task to produce 100 records using the generator function, without waiting for its completion. +. We then described a consumer with group id `my-group` and which commits offset synchronously on every received record. +. Finally, we created a task to consume records for 10 seconds and await its completion. + +== Producing records + +=== Produce from records + +Produce given records: + +[source,java] +---- +include::{examples-dir}/kafka/companion/ProducerTest.java[tags=records,indent=0] +---- + +=== Produce from generator function + +Produce 10 records using the generator function: + +[source,java] +---- +include::{examples-dir}/kafka/companion/ProducerTest.java[tags=generator,indent=0] +---- + +=== Produce from CSV file + +Given a comma-separated file `records.csv` with the following content + +[source,csv] +---- +messages,0,a,asdf +messages,1,b,asdf +messages,3,c,asdf +---- + +Produce records from the file: + +[source,java] +---- +include::{examples-dir}/kafka/companion/ProducerTest.java[tags=csv,indent=0] +---- + +== Consuming records + +=== Consume from topics + +[source,java] +---- +include::{examples-dir}/kafka/companion/ConsumerTest.java[tags=topics,indent=0] +---- + +=== Consume from offsets + +[source,java] +---- +include::{examples-dir}/kafka/companion/ConsumerTest.java[tags=offsets,indent=0] +---- + +=== Consumer assignment and offsets + +During execution of the consumer task, information about the underlying consumer's topic partition assignment, position or committed offsets can be accessed. + +[source,java] +---- +include::{examples-dir}/kafka/companion/ConsumerTest.java[tags=committed,indent=0] +---- + +== Registering Custom Serdes + +KafkaCompanion handles Serializers and Deserializers for default types such as primitives, String, ByteBuffer, UUID. + +Serdes for custom types can be registered to the companion object, and will be used for producer and consumer tasks: + +[source,java] +---- +include::{examples-dir}/kafka/companion/KafkaTest.java[tags=serdes,indent=0] +---- + +== Topics + +Create, list, describe and delete topics: + +[source,java] +---- +include::{examples-dir}/kafka/companion/TopicsTest.java[tags=code,indent=0] +---- + +== Consumer Groups and Offsets + +List topic partition offsets: + +[source,java] +---- +include::{examples-dir}/kafka/companion/OffsetsTest.java[tags=offsets,indent=0] +---- + +List, describe, alter consumer groups and their offsets: + +[source,java] +---- +include::{examples-dir}/kafka/companion/OffsetsTest.java[tags=consumerGroups,indent=0] +---- diff --git a/content/docs/smallrye-reactive-messaging/main/kafka/transactions.adoc b/content/docs/smallrye-reactive-messaging/main/kafka/transactions.adoc new file mode 100644 index 000000000..927012f01 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/kafka/transactions.adoc @@ -0,0 +1,165 @@ += Kafka Transactions and Exactly-Once Processing +:version-root: ../ +include::{version-root}_attributes.adoc[] + +https://cwiki.apache.org/confluence/display/KAFKA/KIP-98+-+Exactly+Once+Delivery+and+Transactional+Messaging[Kafka transactions] enable atomic writes to multiple Kafka topics and partitions. +Inside a transaction, a producer writes records to the Kafka topic partitions as it would normally do. +If the transaction completes successfully, all the records previously written to the broker inside that transaction will be _committed_, and will be readable for consumers. +If an error during the transaction causes it to be _aborted_, none will be readable for consumers. + +There are a couple of fundamental things to consider before using transactions: + +* Each transactional producer is configured with a unique identifier called the `transactional.id`. +This is used to identify the same producer instance across application restarts. +By default, it is not configured, and transactions cannot be used. +When it is configured, the producer is limited to idempotent delivery, therefore `enable.idempotence=true` is implied. +* For *only* reading transactional messages, a consumer needs to explicitly configure its `isolation.level` property to `read++_++committed`. +This will make sure that the consumer will deliver only records committed inside a transaction, and filter out messages from aborted transactions. +* It should also be noted that this does not mean all records atomically written inside a transaction will be read atomically by the consumer. +Transactional producers can write to multiple topics and partitions inside a transaction, but consumers do not know where the transaction starts or ends. +Not only multiple consumers inside a consumer group can share those partitions, +all records which were part of a single transaction can be consumed in different batch of records by a consumer. + +Kafka connector provides `KafkaTransactions` custom emitter for writing records inside a transaction. +Before using a transaction we need to make sure that `transactional.id` is configured on the channel properties. + +.... +mp.messaging.outgoing.tx-out-example.transactional.id=example-tx-producer +.... + +It can be used as a regular emitter `@Channel`: + +[source,java] +---- +include::{examples-dir}/kafka/outbound/KafkaTransactionalProducer.java[indent=0] +---- + +[NOTE] +==== + +When `transactional.id` is provided `KafkaProducer#initTransactions` is called when the underlying Kafka producer is created. + +==== + +The function given to the `withTransaction` method receives a `TransactionalEmitter` for producing records, and returns a `Uni` that provides the result of the transaction. +If the processing completes successfully, the producer is flushed and the transaction is committed. +If the processing throws an exception, returns a failing `Uni`, or marks the `TransactionalEmitter` for abort, the transaction is aborted. + +If this method is called on a Vert.x context, the processing function is also called on that context. +Otherwise, it is called on the sending thread of the producer. + +A synchronous variant `withTransactionAndAwait` is also available for blocking contexts such as REST endpoints or `@Blocking` annotated methods. +It blocks the calling thread until the transaction completes and must not be called on a Vert.x event loop thread. +The transaction completion phase (flush, commit/abort) is bounded by the Kafka producer's `transaction.timeout.ms` setting (default 60 seconds). + +[IMPORTANT] +==== + +By default (non-pooled mode), a transaction is considered _in progress_ from the call to the `withTransaction` until the returned `Uni` results in success or failure. +While a transaction is in progress, subsequent calls to the `withTransaction`, including nested ones inside the given function, will throw `IllegalStateException`. +Note that in Reactive Messaging, the execution of processing methods is already serialized, unless `@Blocking(ordered = false)` is used. +If `withTransaction` can be called concurrently, for example from a REST endpoint, it is recommended to limit the concurrency of the execution. +This can be done using the `@Bulkhead` annotation from Microprofile Fault Tolerance, or by using the xref:#pooled-producer-for-concurrent-transactions[pooled producer mode] which allows concurrent transactions. +In pooled producer mode, multiple transactions can run concurrently, each using a different producer from the pool. + +==== + +== Exactly-Once Processing + +Kafka Transactions API also allows managing consumer offsets inside a transaction, together with produced messages. +This in turn enables coupling a consumer with a transactional producer in a consume-transform-produce pattern, +also known as exactly-once processing. +It means that an application consumes messages from a topic-partition, processes them, publishes the results to a topic-partition, +and commits offsets of the consumed messages in a transaction. + +The `KafkaTransactions` emitter also provides a way to apply exactly-once processing to an incoming Kafka message inside a transaction. + +The following example includes a batch of Kafka records inside a transaction. + +.... +mp.messaging.outgoing.tx-out-example.transactional.id=example-tx-producer +mp.messaging.incoming.in-channel.batch=true +mp.messaging.incoming.in-channel.commit-strategy=ignore +mp.messaging.incoming.in-channel.failure-strategy=ignore +.... + +[source,java] +---- +include::{examples-dir}/kafka/outbound/KafkaExactlyOnceProcessor.java[indent=0] +---- + +If the processing completes successfully, before committing the transaction, the topic partition offsets of the given batch message will be committed to the transaction. + +If the processing needs to abort, after aborting the transaction, the consumer's position is reset to the last committed offset, effectively resuming the consumption from that offset. +If no consumer offset has been committed, the consumer's position is reset to the beginning of the topic, even if the offset reset policy is `latest`. + +[IMPORTANT] +==== + +When using exactly-once processing, consumed message offset commits are handled by the transaction and therefore `commit-strategy` needs to be `ignore`. + +==== + +Any strategy can be employed for the `failure-strategy`. +Note that the `Uni` returned from the `#withTransaction` will yield a failure if the transaction fails and is aborted. + +The application can choose to handle the error case, but for the message consumption to continue, `Uni` returned from the `@Incoming` method must not result in failure. +`KafkaTransactions#withTransactionAndAck` method will ack and nack the message but will not stop the reactive stream. +Ignoring the failure simply resets the consumer to the last committed offsets and resumes the processing from there. + +Synchronous variants `withTransactionAndAwait` are also available for exactly-once processing, +accepting `IncomingKafkaRecordMetadata` or `IncomingKafkaRecordBatchMetadata` for offset management within the transaction. + +[NOTE] +==== + +It is recommended to use exactly-once processing along with the batch consumption mode. +While it is possible to use it with a single Kafka message, it'll have a significant performance impact. + +==== + +== Pooled Producer for Concurrent Transactions + +By default, `KafkaTransactions` uses a single transactional producer and a global lock, meaning only one transaction can be in progress at a time. +This limits throughput when consuming from multiple partitions concurrently. + +The *pooled producer* mode uses a pool of Kafka producers, each with its own `transactional.id`. +Each transaction reserves a producer from the pool for the duration of the transaction, enabling concurrent exactly-once processing. + +To enable pooled producer mode, set `pooled-producer.enabled` to `true` on the outgoing channel. +Combined with incoming channel `batch` and `concurrency`, multiple partitions can be processed in parallel, +each with its own independent transaction: + +[source,properties] +---- +mp.messaging.outgoing.tx-out-example.transactional.id=my-tx-producer +mp.messaging.outgoing.tx-out-example.pooled-producer.enabled=true + +mp.messaging.incoming.in-channel.ordered=partition +mp.messaging.incoming.in-channel.commit-strategy=ignore +mp.messaging.incoming.in-channel.failure-strategy=ignore +---- + +The `KafkaTransactions` API is the same as for regular exactly-once processing. +The outgoing record's partition defaults to the incoming record's partition: + +[source,java] +---- +include::{examples-dir}/kafka/outbound/KafkaPooledExactlyOnceProcessor.java[indent=0] +---- + +On commit or abort, the producer is returned to the pool and can be reused for another transaction. +Only the partitions involved in the transaction are reset on abort, so other concurrent transactions are unaffected. + +The pool grows lazily to match actual concurrency, up to `pooled-producer.max-pool-size` (default 10). +Each inner producer gets a unique `transactional.id` derived from the base value, e.g. `my-tx-producer-1`, `my-tx-producer-2`, etc. +Producers are reused across transactions. + +You can also pre-create producers at startup with `pooled-producer.initial-pool-size` (default 0). +Pre-created producers respect the `lazy-client` setting, so with `lazy-client=false` (default), they eagerly initialize the Kafka producer and call `initTransactions()` at startup. + +[source,properties] +---- +mp.messaging.outgoing.tx-out-example.pooled-producer.max-pool-size=20 +mp.messaging.outgoing.tx-out-example.pooled-producer.initial-pool-size=5 +---- diff --git a/content/docs/smallrye-reactive-messaging/main/kafka/writing-kafka-records.adoc b/content/docs/smallrye-reactive-messaging/main/kafka/writing-kafka-records.adoc new file mode 100644 index 000000000..6d27eee5b --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/kafka/writing-kafka-records.adoc @@ -0,0 +1,354 @@ += Writing Kafka Records +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The Kafka Connector can write Reactive Messaging `Messages` as Kafka +Records. + +== Example + +Let’s imagine you have a Kafka broker running, and accessible using the +`kafka:9092` address (by default it would use `localhost:9092`). +Configure your application to write the messages from the `prices` +channel into a Kafka _topic_ as follows: + +[source,properties] +---- +kafka.bootstrap.servers=kafka:9092 # <1> + +mp.messaging.outgoing.prices-out.connector=smallrye-kafka # <2> +mp.messaging.outgoing.prices-out.value.serializer=org.apache.kafka.common.serialization.DoubleSerializer # <3> +mp.messaging.outgoing.prices-out.topic=prices # <4> +---- + +[arabic] +. Configure the broker location. You can configure it globally or per +channel +. Configure the connector to manage the `prices` channel +. Sets the (Kafka) serializer to encode the message payload into the +record’s value +. Make sure the topic name is `prices` (and not the default +`prices-out`) + +Then, your application must send `Message++<++Double++>++` to the `prices` +channel. It can use `double` payloads as in the following snippet: + +[source,java] +---- +include::{examples-dir}/kafka/outbound/KafkaPriceProducer.java[indent=0] +---- + +Or, you can send `Message++<++Double++>++`: + +[source,java] +---- +include::{examples-dir}/kafka/outbound/KafkaPriceMessageProducer.java[indent=0] +---- + +== Serialization + +The serialization is handled by the underlying Kafka Client. You need to +configure the: + +* `mp.messaging.outgoing.++[++channel-name++]++.value.serializer` to configure +the value serializer (mandatory) +* `mp.messaging.outgoing.++[++channel-name++]++.key.serializer` to configure +the key serializer (optional, default to `String`) + +If you want to use a custom serializer, add it to your `CLASSPATH` and +configure the associate attribute. + +By default, the written record contains: + +* the `Message` payload as _value_ +* no key, or the key configured using the `key` attribute or the key +passed in the metadata attached to the `Message` +* the timestamp computed for the system clock (`now`) or the timestamp +passed in the metadata attached to the `Message` + +== Sending key/value pairs + +In the Kafka world, it’s often necessary to send _records_, i.e. a +key/value pair. The connector provides the link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-kafka/{javadoc-version}/io/smallrye/reactive/messaging/kafka/Record.html[`io.smallrye.reactive.messaging.kafka.Record`] +class that you can use to send a pair: + +[source,java] +---- +include::{examples-dir}/kafka/outbound/KafkaRecordExample.java[tags=code,indent=0] +---- + +When the connector receives a message with a `Record` payload, it +extracts the key and value from it. The configured serializers for the +key and the value must be compatible with the record’s key and value. +Note that the `key` and the `value` can be `null`. It is also possible +to create a record with a `null` key AND a `null` value. + +If you need more control on the written records, use +`OutgoingKafkaRecordMetadata`. + +== Outbound Metadata + +When sending `Messages`, you can add an instance of +link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-kafka/{javadoc-version}/io/smallrye/reactive/messaging/kafka/api/OutgoingKafkaRecordMetadata.html[`OutgoingKafkaRecordMetadata`] +to influence how the message is going to be written to Kafka. For example, +you can add Kafka headers, configure the record key… + +[source,java] +---- +include::{examples-dir}/kafka/outbound/KafkaOutboundMetadataExample.java[tags=code,indent=0] +---- + +== Propagating Record Key + +When processing messages, you can propagate incoming record key to the +outgoing record. + +Consider the following example method, which consumes messages from the +channel `in`, transforms the payload, and writes the result to the +channel `out`. + +[source,java] +---- +include::{examples-dir}/kafka/outbound/KafkaPriceProcessor.java[tags=code,indent=0] +---- + +Enabled with +`mp.messaging.outgoing.++[++channel-name++]++.propagate-record-key=true` +configuration, record key propagation produces the outgoing record with +the same _key_ as the incoming record. + +If the outgoing record already contains a _key_, it *won’t be +overridden* by the incoming record key. If the incoming record does +have a _null_ key, the `mp.messaging.outgoing.++[++channel-name++]++.key` +property is used. + +== Propagating Record headers + +You can also propagate incoming record headers to the outgoing record, by specifying the list of headers to be considered. + +`mp.messaging.outgoing.++[++channel-name++]++.propagate-headers=Authorization,Proxy-Authorization` + +If the ougoing record already defines a header with the same key, it won't be overriden by the incoming header. + +== Dynamic topic names + +Sometimes it is desirable to select the destination of a message +dynamically. In this case, you should not configure the topic inside +your application configuration file, but instead, use the outbound +metadata to set the name of the topic. + +For example, you can route to a dynamic topic based on the incoming +message: + +[source,java] +---- +include::{examples-dir}/kafka/outbound/KafkaOutboundDynamicTopicExample.java[tags=code,indent=0] +---- + +== Acknowledgement + +Kafka acknowledgement can take times depending on the configuration. +Also, it stores in-memory the records that cannot be written. + +By default, the connector does wait for Kafka to acknowledge the record +to continue the processing (acknowledging the received `Message`). You +can disable this by setting the `waitForWriteCompletion` attribute to +`false`. + +Note that the `acks` attribute has a huge impact on the record +acknowledgement. + +If a record cannot be written, the message is `nacked`. + +== Back-pressure and inflight records + +The Kafka outbound connector handles back-pressure monitoring the number +of in-flight messages waiting to be written to the Kafka broker. The +number of in-flight messages is configured using the +`max-inflight-messages` attribute and defaults to 1024. + +The connector only sends that amount of messages concurrently. No other +messages will be sent until at least one in-flight message gets +acknowledged by the broker. Then, the connector writes a new message to +Kafka when one of the broker’s in-flight messages get acknowledged. Be +sure to configure Kafka’s `batch.size` and `linger.ms` accordingly. + +You can also remove the limit of inflight messages by setting +`max-inflight-messages` to `0`. However, note that the Kafka Producer +may block if the number of requests reaches +`max.in.flight.requests.per.connection`. + +== Handling serialization failures + +For Kafka producer client serialization failures are not recoverable, +thus the message dispatch is not retried. However, using schema +registries, serialization may intermittently fail to contact the +registry. In these cases you may need to apply a failure strategy for +the serializer. To achieve this, create a CDI bean implementing the link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-kafka/{javadoc-version}/io/smallrye/reactive/messaging/kafka/SerializationFailureHandler.html[`SerializationFailureHandler`] +interface: + +[source,java] +---- +@ApplicationScoped +@Identifier("failure-fallback") // Set the name of the failure handler +public class MySerializationFailureHandler + implements SerializationFailureHandler { // Specify the expected type + + @Override + public byte[] decorateSerialization(Uni serialization, String topic, + boolean isKey, String serializer, + Object data, Headers headers) { + return serialization + .onFailure().retry().atMost(3) + .await().atMost(Duration.ofMillis(200)); + } +} +---- + +The bean must be exposed with the `@Identifier` qualifier specifying the +name of the bean. Then, in the connector configuration, specify the +following attribute: + +* `mp.messaging.incoming.$channel.key-serialization-failure-handler`: +name of the bean handling serialization failures happening for the +record’s key +* `mp.messaging.incoming.$channel.value-serialization-failure-handler`: +name of the bean handling serialization failures happening for the +record’s value, + +The handler is called with the serialization action as a `Uni`, the +record’s topic, a boolean indicating whether the failure happened on a +key, the class name of the deserializer that throws the exception, the +corrupted data, the exception, and the records headers. Failure +strategies like retry, providing a fallback value or applying timeout +can be implemented. Note that the method must await on the result and +return the serialized byte array. Alternatively, the handler can +implement `decorateSerialization` method which can return a fallback +value. + +== Sending Cloud Events + +The Kafka connector supports https://cloudevents.io/[Cloud Events]. +The connector sends the outbound record as Cloud Events if: + +* the message metadata contains an +`io.smallrye.reactive.messaging.ce.OutgoingCloudEventMetadata` +instance, +* the channel configuration defines the `cloud-events-type` and +`cloud-events-source` attributes. + +You can create +`io.smallrye.reactive.messaging.ce.OutgoingCloudEventMetadata` instances +using: + +[source,java] +---- +include::{examples-dir}/kafka/outbound/KafkaCloudEventProcessor.java[indent=0] +---- + +If the metadata does not contain an id, the connector generates one +(random UUID). The `type` and `source` can be configured per message or +at the channel level using the `cloud-events-type` and +`cloud-events-source` attributes. Other attributes are also +configurable. + +The metadata can be contributed by multiple methods, however, you must +always retrieve the already existing metadata to avoid overriding the +values: + +[source,java] +---- +include::{examples-dir}/kafka/outbound/KafkaCloudEventMultipleProcessors.java[indent=0] +---- + +By default, the connector sends the Cloud Events using the _binary_ +format. You can write _structured_ Cloud Events by setting the +`cloud-events-mode` to `structured`. Only JSON is supported, so the +created records had its `content-type` header set to +`application/cloudevents{plus}json; charset=UTF-8` When using the +_structured_ mode, the value serializer must be set to +`org.apache.kafka.common.serialization.StringSerializer`, otherwise the +connector reports the error. In addition, in _structured_, the connector +maps the message’s payload to JSON, except for `String` passed directly. + +The record’s key can be set in the channel configuration (`key` +attribute), in the `OutgoingKafkaRecordMetadata` or using the +`partitionkey` Cloud Event attribute. + +[NOTE] +==== + +you can disable the Cloud Event support by setting the `cloud-events` +attribute to `false` + +==== + +== Using `ProducerRecord` + +Kafka built-in type +https://kafka.apache.org/26/javadoc/index.html?org/apache/kafka/clients/producer/ProducerRecord.html[ProducerRecord++<++K,V++>++] +can also be used for producing messages: + +[source,java] +---- +include::{examples-dir}/kafka/outbound/KafkaProducerRecordExample.java[tags=code,indent=0] +---- + +[WARNING] +==== + +This is an advanced feature. The `ProducerRecord` is sent to Kafka as +is. Any possible metadata attached through `Message++<++ProducerRecord++<++K, V++>>++` are ignored and lost. + +==== + +== Producer Interceptors + +Producer interceptors can be configured for Kafka producer clients using the standard `interceptor.classes` property. +Configured classes will be instantiated by the Kafka producer on client creation. + +Alternatively, producer clients can be configured with a CDI managed-bean implementing link:https://javadoc.io/doc/org.apache.kafka/kafka-clients/{javadoc-version}/org/apache/kafka/clients/producer/ProducerInterceptor.html[`org.apache.kafka.clients.producer.ProducerInterceptor`] interface: + +To achieve this, the bean must be exposed with the `@Identifier` qualifier specifying the name of the bean: + +[source,java] +---- +include::{examples-dir}/kafka/outbound/ProducerInterceptorBeanExample.java[indent=0] +---- + +Then, in the channel configuration, specify the following attribute: +`mp.messaging.incoming.$channel.interceptor-bean=my-producer-interceptor`. + +[WARNING] +==== + +The `onSend` method will be called on the producer _sending thread_ and `onAcknowledgement` will be called on the _Kafka producer I/O thread_. +In both cases if implementations are not fast, sending of messages could be delayed. + +==== + +== Configuration Reference + +include::{generated-dir}/smallrye-kafka-outgoing.adoc[indent=0] + +You can also pass any property supported by the underlying https://kafka.apache.org/documentation/#producerconfigs[Kafka +producer]. + +For example, to configure the `batch.size` property, use: + +[source,properties] +---- +mp.messaging.outgoing.[channel].batch.size=32768 +---- + +Some producer client properties are configured to sensible default +values: + +If not set, `reconnect.backoff.max.ms` is set to `10000` to avoid high +load on disconnection. + +If not set, `key.serializer` is set to +`org.apache.kafka.common.serialization.StringSerializer`. + +If not set, producer `client.id` is generated as +`kafka-producer-++[++channel++]++`. diff --git a/content/docs/smallrye-reactive-messaging/main/mqtt/client-customization.adoc b/content/docs/smallrye-reactive-messaging/main/mqtt/client-customization.adoc new file mode 100644 index 000000000..768513593 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/mqtt/client-customization.adoc @@ -0,0 +1,21 @@ += Customizing the underlying MQTT client +:version-root: ../ +include::{version-root}_attributes.adoc[] + +You can customize the underlying MQTT Client configuration by +_producing_ an instance of +`io.smallrye.reactive.messaging.mqtt.session.MqttClientSessionOptions`: + +[source,java] +---- +include::{examples-dir}/mqtt/customization/ClientProducers.java[tags=named,indent=0] +---- + +This instance is retrieved and used to configure the client used by the +connector. You need to indicate the name of the client using the +`client-options-name` attribute: + +[source,properties] +---- +mp.messaging.incoming.prices.client-options-name=my-options +---- diff --git a/content/docs/smallrye-reactive-messaging/main/mqtt/mqtt.adoc b/content/docs/smallrye-reactive-messaging/main/mqtt/mqtt.adoc new file mode 100644 index 000000000..afa02d06f --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/mqtt/mqtt.adoc @@ -0,0 +1,44 @@ += MQTT Connector +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The MQTT connector adds support for MQTT to Reactive Messaging. + +It lets you receive messages from an MQTT server or broker as well as +send MQTT messages. The MQTT connector is based on the https://vertx.io/docs/vertx-mqtt/java/#_vert_x_mqtt_client[Vert.x MQTT +Client]. + +== Introduction + +http://mqtt.org/[MQTT] is a machine-to-machine (M2M)/"Internet of +Things" connectivity protocol. It was designed as an extremely +lightweight publish/subscribe messaging transport. + +The MQTT Connector allows consuming messages from MQTT as well as +sending MQTT messages. + +== Using the MQTT connector + +To you the MQTT Connector, add the following dependency to your project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.reactive + smallrye-reactive-messaging-mqtt + {project-version} + +---- + +The connector name is: `smallrye-mqtt`. + +So, to indicate that a channel is managed by this connector you need: + +[source,properties] +---- +# Inbound +mp.messaging.incoming.[channel-name].connector=smallrye-mqtt + +# Outbound +mp.messaging.outgoing.[channel-name].connector=smallrye-mqtt +---- diff --git a/content/docs/smallrye-reactive-messaging/main/mqtt/receiving-mqtt-messages.adoc b/content/docs/smallrye-reactive-messaging/main/mqtt/receiving-mqtt-messages.adoc new file mode 100644 index 000000000..5a1e176bb --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/mqtt/receiving-mqtt-messages.adoc @@ -0,0 +1,114 @@ += Receiving messages from MQTT +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The MQTT Connector connects to a MQTT broker or router, and forward the +messages to the Reactive Messaging application. It maps each of them +into Reactive Messaging `Messages`. + +== Example + +Let’s imagine you have a MQTT server/broker running, and accessible +using the `mqtt:1883` address (by default it would use +`localhost:1883`). Configure your application to receive MQTT messages +on the `prices` channel as follows: + +[source,properties] +---- +mp.messaging.incoming.prices.connector=smallrye-mqtt # <1> +mp.messaging.incoming.prices.host=mqtt # <2> +mp.messaging.incoming.prices.port=1883 # <3> +---- + +[arabic] +. Sets the connector for the `prices` channel +. Configure the broker/server host name. +. Configure the broker/server port. 1883 is the default. + +[NOTE] +==== + +You don’t need to set the MQTT topic. By default, it uses the channel +name (`prices`). You can configure the `topic` attribute to override it. + +==== + +[NOTE] +==== + +It is generally recommended to set the `client-id`. By default, the connector is generating a unique `client-id`. + +==== + +[IMPORTANT] +==== + +Message coming from MQTT have a `byte++[]++` payload. + +==== + +Then, your application receives `Message++<++byte++[]>++`. You can consume the +payload directly: + +[source,java] +---- +include::{examples-dir}/mqtt/inbound/MqttPriceConsumer.java[indent=0] +---- + +Or, you can retrieve the `Message++<++byte++[]>++`: + +[source,java] +---- +include::{examples-dir}/mqtt/inbound/MqttPriceMessageConsumer.java[indent=0] +---- + +The inbound topic can use the https://mosquitto.org/man/mqtt-7.html[MQTT +wildcards] (`{plus}` and `#`). + +== Deserialization + +The MQTT Connector does not handle the deserialization and creates a +`Message++<++byte++[]>++`. + +== Inbound Metadata + +The MQTT connector does not provide inbound metadata. + +== Failure Management + +If a message produced from a MQTT message is _nacked_, a failure +strategy is applied. The MQTT connector supports 3 strategies: + +* `fail` - fail the application, no more MQTT messages will be +processed. (default) The offset of the record that has not been +processed correctly is not committed. +* `ignore` - the failure is logged, but the processing continue. + +== Configuration Reference + +include::{generated-dir}/smallrye-mqtt-incoming.adoc[indent=0] + +The MQTT connector is based on the https://vertx.io/docs/vertx-mqtt/java/#_vert_x_mqtt_client[Vert.x MQTT +client]. So +you can pass any attribute supported by this client. + +[IMPORTANT] +==== + +A single instance of `MqttClient` and a single connection is used for +each `host` / `port` / `server-name` / `client-id`. This client is +reused for both the inbound and outbound connectors. + +==== + +[IMPORTANT] +==== + +Using `auto-clean-session=false` the MQTT Connector send Subscribe requests +to the broken only if a Persistent Session is not present (like on the first +connection). This means that if a Session is already present (maybe for a +previous run) and you add a new incoming channel, this will not be subscribed. +Beware to check always the subscription present on Broker when use +`auto-clean-session=false`. + +==== diff --git a/content/docs/smallrye-reactive-messaging/main/mqtt/sending-messages-to-mqtt.adoc b/content/docs/smallrye-reactive-messaging/main/mqtt/sending-messages-to-mqtt.adoc new file mode 100644 index 000000000..bc8017414 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/mqtt/sending-messages-to-mqtt.adoc @@ -0,0 +1,94 @@ += Sending messages to MQTT +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The MQTT Connector can write Reactive Messaging `Messages` as MQTT +Message. + +== Example + +Let’s imagine you have a MQTT server/broker running, and accessible +using the `mqtt:1883` address (by default it would use +`localhost:1883`). Configure your application to write the messages from +the `prices` channel into a MQTT Messages as follows: + +[source,properties] +---- +mp.messaging.outgoing.prices.type=smallrye-mqtt +mp.messaging.outgoing.prices.host=mqtt +mp.messaging.outgoing.prices.port=1883 +---- + +[arabic] +. Sets the connector for the `prices` channel +. Configure the broker/server host name. +. Configure the broker/server port. 1883 is the default. + +[NOTE] +==== + +You don’t need to set the MQTT topic. By default, it uses the channel +name (`prices`). You can configure the `topic` attribute to override it. +NOTE: It is generally recommended to set the `client-id`. By default, +the connector is generating a unique `client-id`. + +==== + +Then, your application must send `Message++<++Double++>++` to the `prices` +channel. It can use `double` payloads as in the following snippet: + +[source,java] +---- +include::{examples-dir}/mqtt/outbound/MqttPriceProducer.java[indent=0] +---- + +Or, you can send `Message++<++Double++>++`: + +[source,java] +---- +include::{examples-dir}/mqtt/outbound/MqttPriceMessageProducer.java[indent=0] +---- + +== Serialization + +The `Message` sent to MQTT can have various payload types: + +* https://vertx.io/docs/apidocs/io/vertx/core/json/JsonObject.html[`JsonObject`]: +JSON string encoded as `byte++[]++` +* https://vertx.io/docs/apidocs/io/vertx/core/json/JsonArray.html[`JsonArray`]: +JSON string encoded as `byte++[]++` +* `java.lang.String` and Java primitive types: `toString` encoded as +`byte++[]++` +* `byte++[]++` +* complex objects: The objects are encoded to JSON and passed as +`byte++[]++` + +== Outbound Metadata + +The MQTT connector does not provide outbound metadata. + +== Acknowledgement + +MQTT acknowledgement depends on the QoS level. The message is +acknowledged when the broker indicated the successful reception of the +message (or immediately if the level of QoS does not support +acknowledgment). + +If a MQTT message cannot be sent to the broker, the message is `nacked`. + +== Configuration Reference + +include::{generated-dir}/smallrye-mqtt-outgoing.adoc[indent=0] + +The MQTT connector is based on the https://vertx.io/docs/vertx-mqtt/java/#_vert_x_mqtt_client[Vert.x MQTT +client]. So +you can pass any attribute supported by this client. + +[IMPORTANT] +==== + +A single instance of `MqttClient` and a single connection is used for +each `host` / `port` / `server-name` / `client-id`. This client is +reused for both the inbound and outbound connectors. + +==== diff --git a/content/docs/smallrye-reactive-messaging/main/pulsar/client-configuration.adoc b/content/docs/smallrye-reactive-messaging/main/pulsar/client-configuration.adoc new file mode 100644 index 000000000..c0e021963 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/pulsar/client-configuration.adoc @@ -0,0 +1,98 @@ += Configuring Pulsar clients, consumers and producers +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Pulsar clients, consumers and producers are very customizable to configure how a Pulsar client application behaves. + +The Pulsar connector creates a Pulsar client and, a consumer or a producer per channel, each with sensible defaults to ease their configuration. +Although the creation is handled, all available configuration options remain configurable through Pulsar channels. + +While idiomatic way of creating `PulsarClient`, `PulsarConsumer` or `PulsarProducer` are through builder APIs, in its essence +those APIs build each time a configuration object, to pass onto the implementation. +Those are link:https://javadoc.io/doc/org.apache.pulsar/pulsar-client-original/{javadoc-version}/org/apache/pulsar/client/impl/conf/ClientConfigurationData.html[`ClientConfigurationData`], +link:https://javadoc.io/doc/org.apache.pulsar/pulsar-client-original/{javadoc-version}/org/apache/pulsar/client/impl/conf/ConsumerConfigurationData.html[`ConsumerConfigurationData`], +link:https://javadoc.io/doc/org.apache.pulsar/pulsar-client-original/{javadoc-version}/org/apache/pulsar/client/impl/conf/ProducerConfigurationData.html[`ProducerConfigurationData`]. + +Pulsar Connector allows receiving properties for those configuration objects directly. +For example, the broker authentication information for `PulsarClient` is received using `authPluginClassName` and `authParams` properties. +In order to configure the authentication for the incoming channel `data` : + +[source,properties] +---- +mp.messaging.incoming.data.connector=smallrye-pulsar +mp.messaging.incoming.data.serviceUrl=pulsar://localhost:6650 +mp.messaging.incoming.data.topic=topic +mp.messaging.incoming.data.subscriptionInitialPosition=Earliest +mp.messaging.incoming.data.schema=INT32 +mp.messaging.incoming.data.authPluginClassName=org.apache.pulsar.client.impl.auth.AuthenticationBasic +mp.messaging.incoming.data.authParams={"userId":"superuser","password":"admin"} +---- + +Note that the Pulsar consumer property `subscriptionInitialPosition` is also configured with the `Earliest` value which represents with enum value `SubscriptionInitialPosition.Earliest`. + +This approach covers most of the configuration cases. +However, non-serializable objects such as `CryptoKeyReader`, `ServiceUrlProvider` etc. cannot be configured this way. +The Pulsar Connector allows taking into account instances of Pulsar configuration data objects – +`ClientConfigurationData`, `ConsumerConfigurationData`, `ProducerConfigurationData`: + +[source,java] +---- +include::{examples-dir}/pulsar/configuration/PulsarClientConfigProducers.java[tags=consumer,indent=0] +---- + +This instance is retrieved and used to configure the client used by the connector. +You need to indicate the name of the client using the `client-configuration`, `consumer-configuration` or `producer-configuration` attributes: + +[source,properties] +---- +mp.messaging.incoming.prices.consumer-configuration=my-consumer-options +---- + +If no `++[++client++|++consumer++|++producer++]++-configuration` is configured, the connector will look for instances identified with the channel name: + +[source,java] +---- +include::{examples-dir}/pulsar/configuration/PulsarClientConfigProducers.java[tags=client,indent=0] +---- + +You also can provide a `Map++<++String, Object++>++` containing configuration values by key: + +[source,java] +---- +include::{examples-dir}/pulsar/configuration/PulsarClientConfigProducers.java[tags=producer,indent=0] +---- + +Different configuration sources are loaded in the following order of precedence, from the least important to the highest: + +[arabic] +. `Map++<++String, Object++>++` config map produced with default config identifier, `default-pulsar-client`, `default-pulsar-consumer`, `default-pulsar-producer`. +. `Map++<++String, Object++>++` config map produced with identifier in the configuration or channel name +. `++[++Client++|++Producer++|++Consuemr++]++ConfigurationData` object produced with identifier in the channel configuration or the channel name +. Channel configuration properties named with `++[++Client++|++Producer++|++Consuemr++]++ConfigurationData` field names. + +Following is the configuration reference for the `PulsarClient`. + +Corresponding sections list the xref:pulsar/receiving-pulsar-messages.adoc#configuration-reference[Consumer Configuration Reference] and +xref:pulsar/sending-messages-to-pulsar.adoc#configuration-reference[Producer Configuration Reference]. +Configuration properties not configurable in configuration files (non-serializable) is noted in the column `Config file`. + +== Shared Resources + +The Pulsar connector shares an event loop group and DNS resolver across all Pulsar clients using +link:https://javadoc.io/doc/org.apache.pulsar/pulsar-client-api/{javadoc-version}/org/apache/pulsar/client/api/PulsarClientSharedResources.html[`PulsarClientSharedResources`]. +This reduces thread usage and avoids creating separate thread pools per client. + +By default, the connector creates and manages a `PulsarClientSharedResources` instance with shared `EventLoopGroup` and `DnsResolver`. +To customize the shared resources, produce a CDI bean of type `PulsarClientSharedResources`: + +[source,java] +---- +include::{examples-dir}/pulsar/configuration/PulsarClientConfigProducers.java[tags=shared-resources,indent=0] +---- + +When a `PulsarClientSharedResources` bean is provided, the connector uses it for all Pulsar clients and does not manage its lifecycle. +The application is responsible for closing the shared resources when they are no longer needed. + +== `PulsarClient` Configuration Reference + +include::{partialsdir}/pulsar/config/smallrye-pulsar-client.adoc[] diff --git a/content/docs/smallrye-reactive-messaging/main/pulsar/client-service.adoc b/content/docs/smallrye-reactive-messaging/main/pulsar/client-service.adoc new file mode 100644 index 000000000..a50e7db91 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/pulsar/client-service.adoc @@ -0,0 +1,24 @@ += PulsarClientService +:version-root: ../ +include::{version-root}_attributes.adoc[] + +For advanced use cases, SmallRye Reactive Messaging provides a bean of +type `PulsarClientService` that you can inject: + +[source,java] +---- +@Inject +PulsarClientService pulsarClients; +---- + +From there, you can obtain +`org.apache.pulsar.client.api.Consumer`, +`org.apache.pulsar.client.api.Producer` and `org.apache.pulsar.client.api.PulsarClient` by channel name. + +[IMPORTANT] +==== + +Note that the https://pulsar.apache.org/docs/3.0.x/admin-api-get-started/[`PulsarAdmin`] is a separate library and the Pulsar connector doesn't provide it out-of-the-box. +You'd need to explicitly add the dependency, i.e. `org.apache.pulsar:pulsar-client-admin-original` artifact. + +==== diff --git a/content/docs/smallrye-reactive-messaging/main/pulsar/health.adoc b/content/docs/smallrye-reactive-messaging/main/pulsar/health.adoc new file mode 100644 index 000000000..090f18c9c --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/pulsar/health.adoc @@ -0,0 +1,24 @@ += Health reporting +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The Pulsar connector reports the startup, readiness and liveness of each channel managed by the connector: + +* *Startup & Readiness* For both inbound and outbound, probes report _OK_ when the +connection with the broker is established. +* *Liveness* For both inbound and outbound, the liveness check verifies that the +connection is established *AND* that no failures have been caught. + +[NOTE] +==== + +To disable health reporting, set the `health-enabled` attribute for the +channel to `false`. + +==== + +Note that a message processing failures _nacks_ the message which is +then handled by the failure-strategy. It is the responsibility of the +failure-strategy to report the failure and influence the outcome of the +liveness checks. The `fail` failure strategy reports the failure and so +the liveness check will report the failure. diff --git a/content/docs/smallrye-reactive-messaging/main/pulsar/pulsar.adoc b/content/docs/smallrye-reactive-messaging/main/pulsar/pulsar.adoc new file mode 100644 index 000000000..29e31c8b8 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/pulsar/pulsar.adoc @@ -0,0 +1,64 @@ += Apache Pulsar Connector +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The Pulsar connector adds support for Apache Pulsar to Reactive Messaging. With +it you can consume Pulsar Messages as well as produce _message_ into Pulsar. + +https://pulsar.apache.org/[Apache Pulsar] an open-source, distributed messaging +and streaming platform built for the cloud. For more details about Pulsar, check the +https://pulsar.apache.org/docs/next/concepts-overview/[documentation]. + +Pulsar implements the publish-subscribe pattern. +Producers publish _messages_ to __topic__s. +Consumers create __subscription__s to those topics to receive and process incoming messages, +and send _acknowledgments_ to the broker when processing is finished. + +When a _subscription_ is created, Pulsar retains all messages, even if the consumer is disconnected. +The retained messages are discarded only when a consumer acknowledges that all these messages are processed successfully. + +A Pulsar _message_ consists of + +* *value*, payload data that message contains, while Pulsar messages contain payloads as unstructured byte array, +a *schema* is applied to write and read with an enforced data structure +* *key* of type string, used for partitioning +* *properties*, optional key/value map +* *topic*, the topic that the message is published to +* *producer name*, the name of the producer who produces the message +* *message ID*, the ID assigned by bookies to a message as soon as the message is persistently stored +* *sequence ID*, the ID assigned by producers, indicating its order in that sequence +* *publish time*, timestamp automatically added by the producer +* *event time*, optional timestamp added by the application + +A Pulsar cluster consists of +- One or more **broker**s, which are stateless components +- A *metadata store* for maintaining topic metadata, schema, coordination and cluster configuration. +By default, a Zookeeper cluster is used, except the standalone mode +- A ensemble of *bookies* used for persistent storage of messages + +== Using the Pulsar Connector + +To use the Pulsar Connector, add the following dependency to your +project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.reactive + smallrye-reactive-messaging-pulsar + {project-version} + +---- + +The connector name is: `smallrye-pulsar`. + +So, to indicate that a channel is managed by this connector you need: + +[source,properties] +---- +# Inbound +mp.messaging.incoming.[channel-name].connector=smallrye-pulsar + +# Outbound +mp.messaging.outgoing.[channel-name].connector=smallrye-pulsar +---- diff --git a/content/docs/smallrye-reactive-messaging/main/pulsar/receiving-pulsar-messages.adoc b/content/docs/smallrye-reactive-messaging/main/pulsar/receiving-pulsar-messages.adoc new file mode 100644 index 000000000..bafa43e0b --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/pulsar/receiving-pulsar-messages.adoc @@ -0,0 +1,196 @@ += Receiving messages from Pulsar +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The Pulsar Connector connects to a Pulsar broker using a Pulsar client and creates consumers to +receive messages from Pulsar brokers and it maps each of them into Reactive Messaging `Messages`. + +== Example + +Let’s imagine you have a Pulsar broker running, and accessible +using the `pulsar:6650` address (by default it would use +`localhost:6650`). Configure your application to receive Pulsar messages +on the `prices` channel as follows: + +[source,properties] +---- +mp.messaging.incoming.prices.connector=smallrye-pulsar # <1> +mp.messaging.incoming.prices.serviceUrl=pulsar://pulsar:6650 # <2> +mp.messaging.incoming.prices.schema=DOUBLE # <3> +mp.messaging.incoming.prices.subscriptionInitialPosition=Earliest # <4> +---- + +[arabic] +. Sets the connector for the `prices` channel +. Configure the Pulsar broker service url. +. Configure the schema to consume prices as Double. +. Make sure consumer subscription starts receiving messages from the `Earliest` position. + +[NOTE] +==== + +You don’t need to set the Pulsar topic, nor the consumer name. +By default, the connector uses the channel name (`prices`). +You can configure the `topic` and `consumerName` attributes to override them. + +==== + +[NOTE] +==== + +In Pulsar, consumers need to provide a `subscriptionName` for topic subscriptions. +If not provided the connector is generating a unique *subscription name*. + +==== + +Then, your application can receive the `double` payload directly: + +[source,java] +---- +include::{examples-dir}/pulsar/inbound/PulsarPriceConsumer.java[indent=0] +---- + +Or, you can retrieve the `Message++<++Double++>++`: + +[source,java] +---- +include::{examples-dir}/pulsar/inbound/PulsarPriceMessageConsumer.java[indent=0] +---- + +== Consumer Configuration + +The Pulsar Connector allows flexibly configuring the underlying Pulsar consumer. +One of the ways is to set consumer properties directly on the channel configuration. +The list of available configuration properties are listed in xref:#configuration-reference[Configuration Reference]. + +See the xref:pulsar/client-configuration.adoc[Configuring Pulsar consumers, producers and clients] for more information. + +== Deserialization and Pulsar Schema + +The Pulsar Connector allows configuring Schema configuration for the underlying Pulsar consumer. +See the xref:pulsar/schema-configuration.adoc[Configuring the schema used for Pulsar channels] for more information. + +== Inbound Metadata + +The incoming Pulsar messages include an instance of `PulsarIncomingMessageMetadata` in the metadata. +It provides the key, topic, partitions, headers and so on: + +[source,java] +---- +include::{examples-dir}/pulsar/inbound/PulsarMetadataExample.java[tags=code,indent=0] +---- + +== Acknowledgement + +When a message produced from a Pulsar Message is _acknowledged_, the connector sends an https://pulsar.apache.org/docs/3.0.x/concepts-messaging/#acknowledgment[acknowledgement request] to the Pulsar broker. +All Reactive Messaging messages need to be _acknowledged_, which is handled automatically in most cases. +Acknowledgement requests can be sent to the Pulsar broker using the following two strategies: + +* *Individual acknowledgement* is the default strategy, an acknowledgement request is to the broker for each message. +* *Cumulative acknowledgement*, configured using `ack-strategy=cumulative`, the consumer only acknowledges the last message it received. +All messages in the stream up to (and including) the provided message are not redelivered to that consumer. + +== Failure Management + +If a message produced from a Pulsar message is _nacked_, a failure strategy is applied. +The Pulsar connector supports 4 strategies: + +* `nack` _(default)_ sends https://pulsar.apache.org/docs/3.0.x/concepts-messaging/#negative-acknowledgment[negative acknowledgment] to the broker, triggering the broker to redeliver this message to the consumer. +The negative acknowledgment can be further configured using `negativeAckRedeliveryDelayMicros` and `negativeAck.redeliveryBackoff` properties. +* `fail` fail the application, no more messages will be processed. +* `ignore` the failure is logged, but the acknowledgement strategy will be applied and the processing will continue. +* `continue` the failure is logged, but processing continues without applying acknowledgement or negative acknowledgement. This strategy can be used with xref:#acknowledgement-timeout[acknowledgement timeout] configuration. +* `reconsume-later` sends the message to the https://pulsar.apache.org/docs/3.0.x/concepts-messaging/#retry-letter-topic[retry letter topic] using the `reconsumeLater` API to be reconsumed with a delay. +The delay can be configured using the `reconsumeLater.delay` property and defaults to 3 seconds. +Custom delay or properties per message can be configured by adding an instance of link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/io/smallrye/reactive/messaging/pulsar/PulsarReconsumeLaterMetadata.html[`PulsarReconsumeLaterMetadata`] to the failure metadata. + +For example the following configuration for the incoming channel `data` uses `reconsumer-later` failure strategy with default delays of 60 seconds: + +[source,properties] +---- +mp.messaging.incoming.data.connector=smallrye-pulsar +mp.messaging.incoming.data.serviceUrl=pulsar://localhost:6650 +mp.messaging.incoming.data.topic=data +mp.messaging.incoming.data.schema=INT32 +mp.messaging.incoming.data.failure-strategy=reconsume-later +mp.messaging.incoming.data.retryEnable=true +mp.messaging.incoming.data.reconsumeLater.delay=60 // in seconds +mp.messaging.incoming.data.deadLetterPolicy.retryLetterTopic=data-retry +mp.messaging.incoming.data.deadLetterPolicy.maxRedeliverCount=2 +---- + +=== Acknowledgement timeout + +Similar to the negative acknowledgement, with the https://pulsar.apache.org/docs/3.0.x/concepts-messaging/#acknowledgment-timeout[acknowledgment timeout] mechanism, the Pulsar client tracks the unacknowledged messages, +for the given _ackTimeout_ period and sends _redeliver unacknowledged messages request_ to the broker, thus the broker resends the unacknowledged messages to the consumer. + +To configure the timeout and redelivery backoff mechanism you can set `ackTimeoutMillis` and `ackTimeout.redeliveryBackoff` properties. +The `ackTimeout.redeliveryBackoff` value accepts comma separated values of min delay in milliseconds, max delay in milliseconds and multiplier respectively: + +[source,properties] +---- +mp.messaging.incoming.data.connector=smallrye-pulsar +mp.messaging.incoming.data.failure-strategy=continue +mp.messaging.incoming.data.ackTimeoutMillis=10000 +mp.messaging.incoming.data.ackTimeout.redeliveryBackoff=1000,60000,2 +---- + +=== Dead-letter topic + +The https://pulsar.apache.org/docs/3.0.x/concepts-messaging/#dead-letter-topic[dead letter topic] pushes messages that are not consumed successfully to a dead letter topic an continue message consumption. +Note that dead letter topic can be used in different message redelivery methods, such as acknowledgment timeout, negative acknowledgment or retry letter topic. + +[source,properties] +---- +mp.messaging.incoming.data.connector=smallrye-pulsar +mp.messaging.incoming.data.failure-strategy=nack +mp.messaging.incoming.data.deadLetterPolicy.maxRedeliverCount=2 +mp.messaging.incoming.data.deadLetterPolicy.deadLetterTopic=my-dead-letter-topic +mp.messaging.incoming.data.deadLetterPolicy.initialSubscriptionName=my-dlq-subscription +mp.messaging.incoming.data.subscriptionType=Shared +---- + +[IMPORTANT] +==== + +_Negative acknowledgment_ or _acknowledgment timeout_ methods for redelivery will redeliver the whole batch of messages containing at least an unprocessed message. +See xref:pulsar/sending-messages-to-pulsar.adoc#producer-batching[producer batching] for more information. + +==== + +== Receiving Pulsar Messages in Batches + +By default, incoming methods receive each Pulsar message individually. +You can enable batch mode using `batchReceive=true` property, or setting a `batchReceivePolicy` in consumer configuration. + +[source,java] +---- +include::{examples-dir}/pulsar/inbound/PulsarMessageBatchExample.java[tags=code,indent=0] +---- + +Or you can directly receive the list of payloads to the consume method: + +[source,java] +---- +include::{examples-dir}/pulsar/inbound/PulsarMessageBatchPayloadExample.java[tags=code,indent=0] +---- + +=== Accessing metadata of batch records + +When receiving records in batch mode, the metadata of each record is accessible through the `PulsarIncomingBatchMessageMetadata` : + +[source,java] +---- +include::{examples-dir}/pulsar/inbound/PulsarMessageBatchExample.java[tags=batch,indent=0] +---- + +Like in this example, this can be useful to propagate the tracing information of each record. + +== Configuration Reference + +include::{generated-dir}/smallrye-pulsar-incoming.adoc[indent=0] + +In addition to the configuration properties provided by the connector, +following Pulsar consumer properties can also be set on the channel: + +include::{partialsdir}/pulsar/config/smallrye-pulsar-consumer.adoc[] diff --git a/content/docs/smallrye-reactive-messaging/main/pulsar/schema-configuration.adoc b/content/docs/smallrye-reactive-messaging/main/pulsar/schema-configuration.adoc new file mode 100644 index 000000000..67aaf89d5 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/pulsar/schema-configuration.adoc @@ -0,0 +1,57 @@ += Configuring the schema used for Pulsar channels +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Pulsar messages are stored with payloads as unstructured byte array. +A Pulsar *schema* defines how to serialize structured data to the raw message bytes +The *schema* is applied in producers and consumers to write and read with an enforced data structure. +It serializes data into raw bytes before they are published to a topic and deserializes the raw bytes before they are delivered to consumers. + +Pulsar uses a schema registry as a central repository to store the registered schema information, +which enables producers/consumers to coordinate the schema of a topic's messages through brokers. +By default the Apache BookKeeper is used to store schemas. + +Pulsar API provides built-in schema information for a number of +https://pulsar.apache.org/docs/3.0.x/schema-understand#primitive-type[primitive types] +and https://pulsar.apache.org/docs/3.0.x/schema-understand#complex-type[complex types] such as Key/Value, Avro and Protobuf. + +The Pulsar Connector allows specifying the schema as a primitive type using the `schema` property: + +[source,properties] +---- +mp.messaging.incoming.prices.connector=smallrye-pulsar +mp.messaging.incoming.prices.schema=INT32 + +mp.messaging.outgoing.prices-out.connector=smallrye-pulsar +mp.messaging.outgoing.prices-out.schema=DOUBLE +---- + +If the value for the `schema` property matches a https://javadoc.io/doc/org.apache.pulsar/pulsar-client-api/latest/org/apache/pulsar/common/schema/SchemaType.html[Schema Type] +a simple schema will be created with that type and will be used for that channel. + +The Pulsar Connector allows configuring complex schema types by providing `Schema` beans through CDI, identified with the `@Identifier` qualifier. + +For example the following bean provides an Avro schema and a Key/Value schema: + +[source,java] +---- +include::{examples-dir}/pulsar/configuration/PulsarSchemaProvider.java[indent=0] +---- + +To configure the incoming channel `users` with defined schema, you need to set the `schema` property to the identifier of the schema `user-schema`: + +[source,properties] +---- +mp.messaging.incoming.users.connector=smallrye-pulsar +mp.messaging.incoming.users.schema=user-schema +---- + +If no `schema` property is found, the connector looks for `Schema` beans identified with the channel name. +For example, the outgoing channel `a-channel` will use the key/value schema. + +[source,properties] +---- +mp.messaging.outgoing.a-channel.connector=smallrye-pulsar +---- + +If no schema information is provided incoming channels will use `Schema.AUTO++_++CONSUME()`, whereas outgoing channels will use `Schema.AUTO++_++PRODUCE++_++BYTES()` schemas. diff --git a/content/docs/smallrye-reactive-messaging/main/pulsar/sending-messages-to-pulsar.adoc b/content/docs/smallrye-reactive-messaging/main/pulsar/sending-messages-to-pulsar.adoc new file mode 100644 index 000000000..f3561a08a --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/pulsar/sending-messages-to-pulsar.adoc @@ -0,0 +1,160 @@ += Sending messages to Pulsar +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The Pulsar Connector can write Reactive Messaging `Message`s as Pulsar Message. + +== Example + +Let’s imagine you have a Pulsar broker running, and accessible +using the `pulsar:6650` address (by default it would use +`localhost:1883`). Configure your application to write the messages from +the `prices` channel into a Pulsar Messages as follows: + +[source,properties] +---- +mp.messaging.outgoing.prices.connector=smallrye-pulsar # <1> +mp.messaging.outgoing.prices.serviceUrl=pulsar://pulsar:6650 # <2> +mp.messaging.outgoing.prices.schema=DOUBLE # <3> +---- + +[arabic] +. Sets the connector for the `prices` channel +. Configure the Pulsar broker service url. +. Configure the schema to consume prices as Double. + +[NOTE] +==== + +You don’t need to set the Pulsar topic, nor the producer name. +By default, the connector uses the channel name (`prices`). +You can configure the `topic` and `producerName` attributes to override them. + +==== + +Then, your application must send `Message++<++Double++>++` to the `prices` +channel. It can use `double` payloads as in the following snippet: + +[source,java] +---- +include::{examples-dir}/pulsar/outbound/PulsarPriceProducer.java[indent=0] +---- + +Or, you can send `Message++<++Double++>++`: + +[source,java] +---- +include::{examples-dir}/pulsar/outbound/PulsarPriceMessageProducer.java[indent=0] +---- + +== Producer Configuration + +The Pulsar Connector allows flexibly configuring the underlying Pulsar producer. +One of the ways is to set producer properties directly on the channel configuration. +The list of available configuration properties are listed in xref:#configuration-reference[Configuration Reference]. + +See the xref:pulsar/client-configuration.adoc[Configuring Pulsar consumers, producers and clients] for more information. + +== Serialization and Pulsar Schema + +The Pulsar Connector allows configuring Schema configuration for the underlying Pulsar producer. +See the xref:pulsar/schema-configuration.adoc[Configuring the schema used for Pulsar channels] for more information. + +== Sending key/value pairs + +In order to send Kev/Value pairs to Pulsar, you can configure the Pulsar producer Schema with a +link:https://javadoc.io/doc/org.apache.pulsar/pulsar-client-api/{javadoc-version}/org/apache/pulsar/common/schema/KeyValue.html[`org.apache.pulsar.common.schema.KeyValue`] type: + +[source,java] +---- +include::{examples-dir}/pulsar/outbound/PulsarKeyValueExample.java[tags=code,indent=0] +---- + +If you need more control on the written records, use +`PulsarOutgoingMessageMetadata`. + +== Outbound Metadata + +When sending `Message`s, you can add an instance of +link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-pulsar/{javadoc-version}/io/smallrye/reactive/messaging/pulsar/PulsarOutgoingMessageMetadata.html[`PulsarOutgoingMessageMetadata`] +to influence how the message is going to be written to Pulsar. +For example, configure the record key, and set message properties: + +[source,java] +---- +include::{examples-dir}/pulsar/outbound/PulsarOutboundMetadataExample.java[tags=code,indent=0] +---- + +== OutgoingMessage + +Using link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-pulsar/{javadoc-version}/io/smallrye/reactive/messaging/pulsar/OutgoingMessage.html[`OutgoingMessage`], +is an easy way of customizing the Pulsar message to be published when dealing with payloads and not `Message`s. + +You can create an `OutgoingMessage` with key and value, or from an incoming Pulsar Message: + +[source,java] +---- +include::{examples-dir}/pulsar/outbound/PulsarOutgoingMessageExample.java[tags=code,indent=0] +---- + +== Acknowledgement + +Upon receiving a message from a Producer, a Pulsar broker assigns a `MessageId` to the message and sends it back to the producer, +confirming that the message is published. + +By default, the connector does wait for Pulsar to acknowledge the record +to continue the processing (acknowledging the received `Message`). +You can disable this by setting the `waitForWriteCompletion` attribute to `false`. + +If a record cannot be written, the message is `nacked`. + +[IMPORTANT] +==== + +The Pulsar client automatically retries sending messages in case of failure, until the _send timeout_ is reached. +The _send timeout_ is configurable with `sendTimeoutMs` attribute and by default is is 30 seconds. + +==== + +== Back-pressure and inflight records + +The Pulsar outbound connector handles back-pressure monitoring the number +of pending messages waiting to be written to the Pulsar broker. +The number of pending messages is configured using the +`max-inflight-messages` attribute and defaults to 1000. + +The connector only sends that amount of messages concurrently. No other +messages will be sent until at least one pending message gets +acknowledged by the broker. Then, the connector writes a new message to +Pulsar when one of the broker’s pending messages get acknowledged. + +The Pulsar producer also has a limit on the number of pending messages controlled by +`maxPendingMessages` and for partitioned topics `maxPendingMessagesAcrossPartitions` producer configuration properties. +The default value for the both is `0` which means the limit is enforced by the connector and not the underlying producer. +If a `maxPendingMessagesAcrossPartitions` is set and subscribed topic is partitioned, the connector will set the `max-inflight-messages` use the producer's limit. + +[WARNING] +.Deprecation +==== + +Previously a single connector attribute `maxPendingMessages` controlled both the connector back-pressure and the Pulsar producer configuration. +The `max-inflight-messages` attribute is not the same as the Pulsar producer `maxPendingMessages` configuration property. + +==== + +== Producer Batching + +By default, the Pulsar producer batches individual messages together to be published to the broker. +You can configure batching parameters using `batchingMaxPublishDelayMicros`, `batchingPartitionSwitchFrequencyByPublishDelay`, +`batchingMaxMessages`, `batchingMaxBytes` configuration properties, or disable it completely with `batchingEnabled=false`. + +When using `Key++_++Shared` consumer subscriptions, the `batcherBuilder` can be configured to `BatcherBuilder.KEY++_++BASED`. + +== Configuration Reference + +include::{generated-dir}/smallrye-pulsar-outgoing.adoc[indent=0] + +In addition to the configuration properties provided by the connector, +following Pulsar producer properties can also be set on the channel: + +include::{partialsdir}/pulsar/config/smallrye-pulsar-producer.adoc[] diff --git a/content/docs/smallrye-reactive-messaging/main/pulsar/transactions.adoc b/content/docs/smallrye-reactive-messaging/main/pulsar/transactions.adoc new file mode 100644 index 000000000..d46f4e6ba --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/pulsar/transactions.adoc @@ -0,0 +1,89 @@ += Pulsar Transactions and Exactly-Once Processing +:version-root: ../ +include::{version-root}_attributes.adoc[] + +https://pulsar.apache.org/docs/3.0.x/txn-why/[Pulsar transactions] enable event streaming applications to consume, process, and produce messages in one atomic operation. + +Transactions allow one or multiple producers to send batch of messages to multiple topics where all messages in the batch are eventually visible to any consumer, or none is ever visible to consumers. + +In order to be used, transaction support needs to be activated on the broker configuration: + +[source,properties] +---- +# used to enable transaction coordinator +transactionCoordinatorEnabled=true + +# used to create systemTopic used for transaction buffer snapshot +systemTopicEnabled=true +---- + +On the client side, the transaction support also needs to be enabled on `PulsarClient` configuration: + +[source,properties] +---- +mp.messaging.outgoing.tx-producer.enableTransaction=true +---- + +Pulsar connector provides `PulsarTransactions` custom emitter for writing records inside a transaction. + +It can be used as a regular emitter `@Channel`: + +[source,java] +---- +include::{examples-dir}/pulsar/outbound/PulsarTransactionalProducer.java[indent=0] +---- + +The function given to the `withTransaction` method receives a `TransactionalEmitter` for producing records, and returns a `Uni` that provides the result of the transaction. +If the processing completes successfully, the producer is flushed and the transaction is committed. +If the processing throws an exception, returns a failing `Uni`, or marks the `TransactionalEmitter` for abort, the transaction is aborted. + +[NOTE] +==== + +Multiple transactional producers can participate in a single transaction. +This ensures all messages are sent using the started transaction and before the transaction is committed, all participating producers are flushed. + +==== + +If this method is called on a Vert.x context, the processing function is also called on that context. +Otherwise, it is called on the sending thread of the producer. + +== Exactly-Once Processing + +Pulsar Transactions API also allows managing consumer offsets inside a transaction, together with produced messages. +This in turn enables coupling a consumer with a transactional producer in a consume-transform-produce pattern, +also known as exactly-once processing. +It means that an application consumes messages, processes them, publishes the results to a topic, and commits offsets of the consumed messages in a transaction. + +The `PulsarTransactions` emitter also provides a way to apply exactly-once processing to an incoming Pulsar message inside a transaction. + +The following example includes a batch of Pulsar messages inside a transaction. + +[source,properties] +---- + mp.messaging.outgoing.tx-out-example.enableTransaction=true + # ... + mp.messaging.incoming.in-channel.enableTransaction=true + mp.messaging.incoming.in-channel.batchReceive=true +---- + +[source,java] +---- +include::{examples-dir}/pulsar/outbound/PulsarExactlyOnceProcessor.java[indent=0] +---- + +If the processing completes successfully, the message is acknowledged inside the transaction and the transaction is committed. + +[IMPORTANT] +==== + +When using exactly-once processing, messages can only be acked individually rather than cumulatively. + +==== + +If the processing needs to abort, the message is nack'ed. One of the failure strategies can be employed in order to retry the processing or simply fail-stop. +Note that the `Uni` returned from the `#withTransaction` will yield a failure if the transaction fails and is aborted. + +The application can choose to handle the error case, but for the message consumption to continue, `Uni` returned from the `@Incoming` method must not result in failure. +`PulsarTransactions#withTransactionAndAck` method will ack and nack the message but will not stop the reactive stream. +Ignoring the failure simply resets the consumer to the last committed offsets and resumes the processing from there. diff --git a/content/docs/smallrye-reactive-messaging/main/rabbitmq/rabbitmq-og-client-customization.adoc b/content/docs/smallrye-reactive-messaging/main/rabbitmq/rabbitmq-og-client-customization.adoc new file mode 100644 index 000000000..2eeebf8fb --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/rabbitmq/rabbitmq-og-client-customization.adoc @@ -0,0 +1,57 @@ += Customizing the underlying RabbitMQ client +:version-root: ../ +include::{version-root}_attributes.adoc[] + +You can customize the underlying RabbitMQ Client configuration by +_producing_ an instance of +https://rabbitmq.github.io/rabbitmq-java-client/api/current/com/rabbitmq/client/ConnectionFactory.html[`ConnectionFactory`]: + +[source,java] +---- +include::{examples-dir}/rabbitmq/og/customization/RabbitMQProducers.java[tags=named,indent=0,opts=optional] +---- + +This instance is retrieved and used to configure the client used by the +connector. You need to indicate the name of the client using the +`client-options-name` attribute: + +.... +mp.messaging.incoming.prices.client-options-name=my-named-options +.... + +== Credentials Provider + +The OG connector supports RabbitMQ's +https://rabbitmq.github.io/rabbitmq-java-client/api/current/com/rabbitmq/client/impl/CredentialsProvider.html[`CredentialsProvider`] +interface for dynamic credential management. This is useful when +credentials are rotated or fetched from an external secrets manager. + +To use a credentials provider, expose a CDI bean implementing +`com.rabbitmq.client.impl.CredentialsProvider` with an `@Identifier` +qualifier, and reference it via the `credentials-provider-name` attribute: + +[source,properties] +---- +mp.messaging.incoming.prices.credentials-provider-name=my-credentials-provider +---- + +== Cluster mode + +To connect to a RabbitMQ cluster, use the `addresses` attribute to +specify multiple broker addresses. When set, this overrides the `host` +and `port` attributes: + +[source,properties] +---- +rabbitmq-addresses=host1:5672,host2:5672,host3:5672 +---- + +== NIO Sockets + +The connector supports using NIO sockets for the RabbitMQ connection. +Enable NIO mode with: + +[source,properties] +---- +rabbitmq-use-nio=true +---- diff --git a/content/docs/smallrye-reactive-messaging/main/rabbitmq/rabbitmq-og-cloud.adoc b/content/docs/smallrye-reactive-messaging/main/rabbitmq/rabbitmq-og-cloud.adoc new file mode 100644 index 000000000..b1df1aed5 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/rabbitmq/rabbitmq-og-cloud.adoc @@ -0,0 +1,52 @@ += Connecting to managed instances +:version-root: ../ +include::{version-root}_attributes.adoc[] + +This section describes the connector configuration to use managed +RabbitMQ instances (hosted on the Cloud). + +== Cloud AMQP + +To connect to an instance of RabbitMQ hosted on https://www.cloudamqp.com/[Cloud +AMQP], use the following configuration: + +[source,properties] +---- +rabbitmq-host=host-name +rabbitmq-port=5671 +rabbitmq-username=user-name +rabbitmq-password=password +rabbitmq-virtual-host=user-name +rabbitmq-ssl=true +---- + +You can extract the values from the `AMQPS` url displayed on the +administration portal: + +.... +amqps://user-name:password@host/user-name +.... + +== Amazon MQ + +https://aws.amazon.com/amazon-mq/[Amazon MQ] can host RabbitMQ brokers +(as well as AMQP 1.0 brokers). To connect to a RabbitMQ instance hosted +on Amazon MQ, use the following configuration: + +[source,properties] +---- +rabbitmq-host=host-name +rabbitmq-port=5671 +rabbitmq-username=user-name +rabbitmq-password=password +rabbitmq-ssl=true +---- + +You can extract the host value from the `AMQPS` url displayed on the +administration console: + +.... +amqps://foobarbaz.mq.us-east-2.amazonaws.com:5671 +.... + +The username and password are configured during the broker creation. diff --git a/content/docs/smallrye-reactive-messaging/main/rabbitmq/rabbitmq-og-health.adoc b/content/docs/smallrye-reactive-messaging/main/rabbitmq/rabbitmq-og-health.adoc new file mode 100644 index 000000000..ff7bd4d47 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/rabbitmq/rabbitmq-og-health.adoc @@ -0,0 +1,27 @@ += Health reporting +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The RabbitMQ OG connector reports the readiness and liveness of each +channel managed by the connector. + +On the inbound side (receiving messages from RabbitMQ), the check +verifies that the receiver is connected to the broker. + +On the outbound side (sending records to RabbitMQ), the check verifies +that the sender is not disconnected from the broker; the sender _may_ +still be in an initialized state (connection not yet attempted), but +this is regarded as live/ready. + +You can disable health reporting by setting the `health-enabled` attribute of the channel to `false`. +It disables both liveness and readiness. +You can disable readiness reporting by setting the `health-readiness-enabled` attribute of the channel to `false`. + +== @Channel and lazy subscription + +When you inject a channel using `@Channel` annotation, you are responsible for subscribing to the channel. +Until the subscription happens, the channel is not connected to the broker and thus cannot receive messages. +The default health check will fail in this case. + +To handle this use case, you need to configure the `health-lazy-subscription` attribute of the channel to `true`. +It configures the health check to not fail if there are no subscription yet. diff --git a/content/docs/smallrye-reactive-messaging/main/rabbitmq/rabbitmq-og-tracing.adoc b/content/docs/smallrye-reactive-messaging/main/rabbitmq/rabbitmq-og-tracing.adoc new file mode 100644 index 000000000..46b1ecce4 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/rabbitmq/rabbitmq-og-tracing.adoc @@ -0,0 +1,41 @@ += OpenTelemetry Tracing +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The RabbitMQ OG connector supports https://opentelemetry.io/[OpenTelemetry] +tracing for both incoming and outgoing channels. + +== Enabling tracing + +Tracing is enabled by default. You can disable it per channel with: + +[source,properties] +---- +mp.messaging.incoming.prices.tracing.enabled=false +mp.messaging.outgoing.prices.tracing.enabled=false +---- + +== How it works + +When tracing is enabled: + +* *Outgoing messages*: The connector creates a `PUBLISH` span and +injects the trace context into the message headers before sending. +* *Incoming messages*: The connector extracts the trace context from +the message headers and creates a `RECEIVE` span linked to the +producer's trace context. + +The span attributes include the exchange name and routing key. + +== Including headers as span attributes + +You can configure specific message headers to be recorded as span +attributes using the `tracing.attribute-headers` property: + +[source,properties] +---- +mp.messaging.incoming.prices.tracing.attribute-headers=my-header,another-header +---- + +This is a comma-separated list of header names whose values will be +added as attributes to the tracing span. diff --git a/content/docs/smallrye-reactive-messaging/main/rabbitmq/rabbitmq-og.adoc b/content/docs/smallrye-reactive-messaging/main/rabbitmq/rabbitmq-og.adoc new file mode 100644 index 000000000..77a76a528 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/rabbitmq/rabbitmq-og.adoc @@ -0,0 +1,68 @@ += RabbitMQ OG Connector +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The RabbitMQ OG Connector adds support for RabbitMQ to Reactive Messaging, +based on the AMQP 0-9-1 Protocol Specification. + +Advanced Message Queuing Protocol 0-9-1 (https://www.rabbitmq.com/resources/specs/amqp0-9-1.pdf[AMQP +0-9-1]) is an +open standard for passing business messages between applications or +organizations. + +With this connector, your application can: + +* receive messages from a RabbitMQ queue +* send messages to a RabbitMQ exchange + +The RabbitMQ OG connector is based on the https://www.rabbitmq.com/client-libraries/java-client[RabbitMQ Java Client], +the official RabbitMQ client library. + +[NOTE] +==== + +This connector is an alternative to the existing RabbitMQ connector +(`smallrye-rabbitmq`), which is based on the Vert.x RabbitMQ client. +The OG connector uses the original RabbitMQ Java client directly, +providing improved reconnection handling and direct access to all +RabbitMQ client features. + +==== + +[IMPORTANT] +==== + +The *AMQP connector* supports the AMQP 1.0 protocol, which is very +different from AMQP 0-9-1. You _can_ use the AMQP connector with +RabbitMQ provided that the latter has the https://github.com/rabbitmq/rabbitmq-amqp1.0/blob/v3.7.x/README.md[AMQP 1.0 +Plugin] +installed, albeit with reduced functionality. + +==== + +== Using the RabbitMQ OG connector + +To use the RabbitMQ OG Connector, add the following dependency to your +project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.reactive + smallrye-reactive-messaging-rabbitmq-og + {project-version} + +---- + +The connector name is: `smallrye-rabbitmq-og`. + +So, to indicate that a channel is managed by this connector you need: + +[source,properties] +---- +# Inbound +mp.messaging.incoming.[channel-name].connector=smallrye-rabbitmq-og + +# Outbound +mp.messaging.outgoing.[channel-name].connector=smallrye-rabbitmq-og +---- diff --git a/content/docs/smallrye-reactive-messaging/main/rabbitmq/receiving-messages-from-rabbitmq.adoc b/content/docs/smallrye-reactive-messaging/main/rabbitmq/receiving-messages-from-rabbitmq.adoc new file mode 100644 index 000000000..43ee6049d --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/rabbitmq/receiving-messages-from-rabbitmq.adoc @@ -0,0 +1,308 @@ += Receiving messages from RabbitMQ +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The RabbitMQ OG connector lets you retrieve messages from a https://www.rabbitmq.com/[RabbitMQ +broker]. The RabbitMQ connector retrieves +_RabbitMQ Messages_ and maps each of them into Reactive Messaging +`Messages`. + +[NOTE] +==== + +In this context, the reactive messaging concept of a _Channel_ is +realised as a https://www.rabbitmq.com/queues.html[RabbitMQ Queue]. + +==== + +== Example + +Let's imagine you have a RabbitMQ broker running, and accessible using +the `rabbitmq:5672` address (by default it would use `localhost:5672`). +Configure your application to receive RabbitMQ Messages on the `prices` +channel as follows: + +[source,properties] +---- +rabbitmq-host=rabbitmq # <1> +rabbitmq-port=5672 # <2> +rabbitmq-username=my-username # <3> +rabbitmq-password=my-password # <4> + +mp.messaging.incoming.prices.connector=smallrye-rabbitmq-og # <5> +mp.messaging.incoming.prices.queue.name=my-queue # <6> +mp.messaging.incoming.prices.routing-keys=urgent # <7> +---- + +[arabic] +. Configures the broker/router host name. You can do it per channel +(using the `host` attribute) or globally using `rabbitmq-host`. +. Configures the broker/router port. You can do it per channel (using +the `port` attribute) or globally using `rabbitmq-port`. The default +is 5672. +. Configures the broker/router username if required. You can do it per +channel (using the `username` attribute) or globally using +`rabbitmq-username`. +. Configures the broker/router password if required. You can do it per +channel (using the `password` attribute) or globally using +`rabbitmq-password`. +. Instructs the `prices` channel to be managed by the RabbitMQ OG +connector. +. Configures the RabbitMQ queue to read messages from. +. Configures the binding between the RabbitMQ exchange and the +RabbitMQ queue using a routing key. The default is `#` (all messages +will be forwarded from the exchange to the queue) but in general +this can be a comma-separated list of one or more keys. + +Then, your application receives `Message++<++String++>++`. You can consume the +payload directly: + +[source,java] +---- +include::{examples-dir}/rabbitmq/og/inbound/RabbitMQPriceConsumer.java[indent=0,opts=optional] +---- + +Or, you can retrieve the `Message++<++String++>++`: + +[source,java] +---- +include::{examples-dir}/rabbitmq/og/inbound/RabbitMQPriceMessageConsumer.java[indent=0,opts=optional] +---- + +[NOTE] +==== + +Whether you need to explicitly acknowledge the message depends on the +`auto-acknowledgement` channel setting; if that is set to `true` then +your message will be automatically acknowledged on receipt. + +==== + +== Deserialization + +The connector converts incoming RabbitMQ Messages into Reactive +Messaging `Message++<++T++>++` instances. Incoming messages are received as +`byte++[]++` and can be automatically converted to `String` using the +built-in message converter. + +[cols=",",options="header",] +|=== +|content++_++type |Type +|`text/plain` |`String` +|_Anything else or unset_ |`byte++[]++` +|=== + +[NOTE] +==== + +Unlike the Vert.x-based RabbitMQ connector, the OG connector does not +provide automatic conversion to Vert.x `JsonObject` or `JsonArray` +types. For JSON payloads, receive the message as `String` or `byte++[]++` +and use your preferred JSON library (such as Jackson) for +deserialization. + +==== + +== Inbound Metadata + +Messages coming from RabbitMQ contain an instance of link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-rabbitmq-og/{javadoc-version}/io/smallrye/reactive/messaging/rabbitmq/og/IncomingRabbitMQMetadata.html[`IncomingRabbitMQMetadata`] +in the metadata. + +RabbitMQ message headers can be accessed from the metadata either by +calling `getHeader(String header)` to retrieve a single +header value as a `String`, or `getHeader(String header, Class++<++T++>++ type)` to retrieve +a typed header value, or `getHeaders()` to get a map of all header values. + +[source,java] +---- +include::{examples-dir}/rabbitmq/og/inbound/RabbitMQMetadataExample.java[tags=code,indent=0,opts=optional] +---- + +The type `++<++T++>++` of the header value depends on the RabbitMQ type used for +the header: + +[cols=",",options="header",] +|=== +|RabbitMQ Header Type |T +|String |`String` +|Boolean |`Boolean` +|Number |`Number` +|List |`java.util.List` +|=== + +[NOTE] +==== + +The `IncomingRabbitMQMetadata` in the OG connector returns direct types +(e.g. `String`, `Integer`, `Date`) for most property accessors rather +than `Optional` wrappers. Values will be `null` when not set by the +producer. The `getHeader` methods still return `Optional`. + +==== + +== Acknowledgement + +When a Reactive Messaging Message associated with a RabbitMQ Message is +acknowledged, it informs the broker that the message has been +_accepted_. + +Whether you need to explicitly acknowledge the message depends on the +`auto-acknowledgement` setting for the channel; if that is set to `true` +then your message will be automatically acknowledged on receipt. + +== Failure Management + +If a message produced from a RabbitMQ message is _nacked_, a failure +strategy is applied. The RabbitMQ OG connector supports four strategies, +controlled by the `failure-strategy` channel setting: + +* `fail` - fail the application; no more RabbitMQ messages will be +processed. The RabbitMQ message is marked as rejected. +* `accept` - this strategy marks the RabbitMQ message as accepted. The +processing continues ignoring the failure. +* `reject` - this strategy marks the RabbitMQ message as rejected +(default). The processing continues with the next message. +* `requeue` - this strategy marks the RabbitMQ message as rejected +with requeue flag to true. The processing continues with the next message, +but the requeued message will be redelivered to the consumer. + +The RabbitMQ reject `requeue` flag can be controlled on different failure strategies +using the link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/io/smallrye/reactive/messaging/rabbitmq/og/RabbitMQRejectMetadata.html[`RabbitMQRejectMetadata`]. +To do that, use the `Message.nack(Throwable, Metadata)` method by including the +`RabbitMQRejectMetadata` metadata with `requeue` to `true`. + +[source,java] +---- +include::{examples-dir}/rabbitmq/og/inbound/RabbitMQRejectMetadataExample.java[tags=code,indent=0,opts=optional] +---- + +[WARNING] +.Experimental +==== + +`RabbitMQFailureHandler` is experimental and APIs are subject to change in the future + +==== + +In addition, you can also provide your own failure strategy. +To provide a failure strategy implement a bean exposing the interface +link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/{javadoc-version}/io/smallrye/reactive/messaging/rabbitmq/og/fault/RabbitMQFailureHandler.html[`RabbitMQFailureHandler`], +qualified with a `@Identifier`. +Set the name of the bean as the `failure-strategy` channel setting. + +== Configuration Reference + +NOTE: This table is generated from the project's build and is not available for this version. + +To use an existing _queue_, you need to configure the `queue.name` +attribute. + +For example, if you have a RabbitMQ broker already configured with a +queue called `peopleQueue` that you wish to read messages from, you need +the following configuration: + +[source,properties] +---- +mp.messaging.incoming.people.connector=smallrye-rabbitmq-og +mp.messaging.incoming.people.queue.name=peopleQueue +---- + +If you want RabbitMQ to create the queue for you but bind it to an +existing topic exchange `people`, you need the following configuration: + +[source,properties] +---- +mp.messaging.incoming.people.connector=smallrye-rabbitmq-og +mp.messaging.incoming.people.queue.name=peopleQueue +mp.messaging.incoming.people.queue.declare=true +---- + +[NOTE] +==== + +In the above the channel name `people` is implicitly assumed to be the +name of the exchange; if this is not the case you would need to name the +exchange explicitly using the `exchange.name` property. + +==== + +[NOTE] +==== + +The connector supports RabbitMQ's "Server-named Queues" feature to create +an exclusive, auto-deleting, non-durable and randomly named queue. To +enable this feature you set the queue name to exactly `(server.auto)`. +Using this name not only enables the server named queue feature but also +automatically makes ths queue exclusive, auto-deleting, and non-durable; +therefore ignoring any values provided for the `exclusive`, `auto-delete` +and `durable` options. + +==== + +If you want RabbitMQ to create the `people` exchange, queue and binding, +you need the following configuration: + +[source,properties] +---- +mp.messaging.incoming.people.connector=smallrye-rabbitmq-og +mp.messaging.incoming.people.exchange.declare=true +mp.messaging.incoming.people.queue.name=peopleQueue +mp.messaging.incoming.people.queue.declare=true +mp.messaging.incoming.people.queue.routing-keys=tall,short +---- + +In the above we have used an explicit list of routing keys rather than +the default (`#`). Each component of the list creates a separate binding +between the queue and the exchange, so in the case above we would have +two bindings; one based on a routing key of `tall`, the other based on +one of `short`. + +[NOTE] +==== + +The default value of `routing-keys` is `#` (indicating a match against +all possible routing keys) which is only appropriate for _topic_ +Exchanges. If you are using other types of exchange and/or need to +declare queue bindings, you'll need to supply a valid value for the +exchange in question. + +==== + +== Custom arguments for Queue declaration + +When queue declaration is made by the Reactive Messaging channel, using the `queue.declare=true` configuration, +custom queue arguments can be specified using the `queue.arguments` attribute. +`queue.arguments` accepts the identifier (using the `@Identifier` qualifier) of a `Map++<++String, Object++>++` exposed as a CDI bean. +If no arguments has been configured, the default *rabbitmq-queue-arguments* identifier is looked for. + +The following CDI bean produces such a configuration identified with *my-arguments*: + +[source,java] +---- +include::{examples-dir}/rabbitmq/og/customization/ArgumentProducers.java[indent=0,opts=optional] +---- + +Then the channel can be configured to use those arguments in queue declaration: + +[source,properties] +---- +mp.messaging.incoming.data.queue.arguments=my-arguments +---- + +Similarly, the `dead-letter-queue.arguments` allows configuring custom arguments for dead letter queue when one is declared (`auto-bind-dlq=true`). + +== Consumer configuration + +The OG connector provides additional consumer options: + +* `consumer-tag` - a custom consumer tag; if not provided, the broker +generates one automatically. +* `consumer-exclusive` - whether the consumer has exclusive access to +the queue. +* `consumer-arguments` - a comma-separated list of arguments +(`key1:value1,key2:value2,...`) for the consumer. +* `content-type-override` - overrides the `content++_++type` attribute of +the incoming message; should be a valid MIME type. +* `max-outstanding-messages` - the maximum number of unacknowledged +messages being processed concurrently. This controls backpressure +through RabbitMQ's QoS (prefetch count) mechanism. diff --git a/content/docs/smallrye-reactive-messaging/main/rabbitmq/request-reply.adoc b/content/docs/smallrye-reactive-messaging/main/rabbitmq/request-reply.adoc new file mode 100644 index 000000000..094e929e0 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/rabbitmq/request-reply.adoc @@ -0,0 +1,139 @@ += RabbitMQ Request/Reply +:version-root: ../ +include::{version-root}_attributes.adoc[] + +[WARNING] +.Experimental +==== + +RabbitMQ Request Reply Emitter is an experimental feature. + +==== + +The RabbitMQ https://www.enterpriseintegrationpatterns.com/patterns/messaging/RequestReply.html[Request-Reply] pattern allows you to publish a message to a RabbitMQ address and then await for a reply message that responds to the initial request, which can be used to implement https://www.rabbitmq.com/tutorials/tutorial-six-java[RPC over RabbitMQ]. + +Given the following request/reply outgoing configuration example: + +[source,properties] +---- +mp.messaging.outgoing.my-request.connector=smallrye-rabbitmq +mp.messaging.outgoing.my-request.exchange.name=rpc +mp.messaging.outgoing.my-request.exchange.type=direct +mp.messaging.outgoing.my-request.default-routing-key=rpc-request +---- + +The `RabbitMQRequestReply` emitter implements the requestor (or the client) of the request-reply pattern for RabbitMQ outbound channels: + +[source,java] +---- +include::{examples-dir}/rabbitmq/outbound/RabbitMQRequestReplyEmitter.java[indent=0] +---- + +[NOTE] +==== + +While in this example we use a direct exchange, it is also possible to use a topic or fanout exchange as well as https://www.rabbitmq.com/docs/local-random-exchange[Local Random Exchanges] for request/reply. + +==== + +The `request` method publishes the request message to the configured target address of the outgoing channel leveraging RabbitMQ https://www.rabbitmq.com/docs/direct-reply-to[Direct Reply-To], +and waits for a reply. +When the reply is received the returned `Uni` is completed with the message payload. + +The request send operation generates a correlation id and sets the `correlationId` property, +which it expects to be sent back in the reply message. + +The replier (or the server) can be implemented using a Reactive Messaging processor: + +[source,properties] +---- +mp.messaging.incoming.request.connector=smallrye-rabbitmq +mp.messaging.incoming.request.exchange.name=rpc +mp.messaging.incoming.request.exchange.type=direct +mp.messaging.incoming.request.routing-keys=rpc-request + +mp.messaging.outgoing.reply.connector=smallrye-rabbitmq +mp.messaging.outgoing.reply.exchange.name="" +---- + +[source,java] +---- +include::{examples-dir}/rabbitmq/outbound/RabbitMQReplier.java[indent=0] +---- + +== Requesting with `Message` types + +Like the core Emitter's `send` methods, `request` method also can receive a `Message` type and return a message: + +[source,java] +---- +include::{examples-dir}/rabbitmq/outbound/RabbitMQRequestReplyMessageEmitter.java[indent=0] +---- + +[NOTE] +==== + +The ingested reply type of the `RabbitMQRequestReply` is discovered at runtime, +in order to configure a `MessageConverter` to be applied on the incoming message before returning the `Uni` result. + +==== + +== Requesting multiple replies + +You can use the `requestMulti` method to expect any number of replies represented by the `Multi` return type. + +For example this can be used to aggregate multiple replies to a single request. + +[source,java] +---- +include::{examples-dir}/rabbitmq/outbound/RabbitMQRequestReplyMultiEmitter.java[indent=0] +---- + +Like the other `request` you can also request `Message` types. + +[NOTE] +==== + +The channel attribute `reply.timeout` will be applied between each message, if reached the returned `Multi` will +fail. + +==== + +== Scaling Request/Reply + +If multiple requestor instances are configured on the same outgoing address, and the same reply address, +each requestor instance will receive replies of all instances. If an observed correlation id doesn't match +the id of any pending replies, the reply is simply discarded. +With the additional network traffic this allows scaling requestors, (and repliers) dynamically. + +== Pending replies and reply timeout + +By default, the `Uni` returned from the `request` method is configured to fail with timeout exception if no replies is received after 5 seconds. +This timeout is configurable with the channel attribute `reply.timeout`. + +A snapshot of the list of pending replies is available through the `RabbitMQRequestReply#getPendingReplies` method. + +== Correlation Ids + +The RabbitMQ Request/Reply allows configuring the correlation id mechanism completely through a `CorrelationIdHandler` implementation. +The default handler is based on randomly generated UUID strings. +The correlation id handler implementation can be configured using the `reply.correlation-id.handler` attribute. +As mentioned the default configuration is `uuid`. + +Custom handlers can be implemented by proposing a CDI-managed bean with `@Identifier` qualifier. + +== Reply Error Handling + +If the reply server produces an error and can or would like to propagate the error back to the requestor, failing the returned `Uni`. + +If configured using the `reply.failure.handler` channel attribute, +the `ReplyFailureHandler` implementations are discovered through CDI, matching the `@Identifier` qualifier. + +A sample reply error handler can lookup header values and return the error to be thrown by the reply: + +[source,java] +---- +include::{examples-dir}/rabbitmq/outbound/MyRabbitMQReplyFailureHandler.java[indent=0] +---- + +`null` return value indicates that no error has been found in the reply, and it can be delivered to the application. diff --git a/content/docs/smallrye-reactive-messaging/main/rabbitmq/sending-messages-to-rabbitmq.adoc b/content/docs/smallrye-reactive-messaging/main/rabbitmq/sending-messages-to-rabbitmq.adoc new file mode 100644 index 000000000..0dbb3830c --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/rabbitmq/sending-messages-to-rabbitmq.adoc @@ -0,0 +1,241 @@ += Sending messages to RabbitMQ +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The RabbitMQ OG connector can write Reactive Messaging `Messages` as +RabbitMQ Messages. + +[NOTE] +==== + +In this context, the reactive messaging concept of a _Channel_ is +realised as a https://www.rabbitmq.com/tutorials/amqp-concepts.html#exchanges[RabbitMQ +Exchange]. + +==== + +== Example + +Let's imagine you have a RabbitMQ broker running, and accessible using +the `rabbitmq:5672` address (by default it would use `localhost:5672`). +Configure your application to send the messages from the `prices` +channel as a RabbitMQ Message as follows: + +.... +rabbitmq-host=rabbitmq # <1> +rabbitmq-port=5672 # <2> +rabbitmq-username=my-username # <3> +rabbitmq-password=my-password # <4> + +mp.messaging.outgoing.prices.connector=smallrye-rabbitmq-og # <5> +mp.messaging.outgoing.prices.default-routing-key=normal # <6> +.... + +[arabic] +. Configures the broker/router host name. You can do it per channel +(using the `host` attribute) or globally using `rabbitmq-host` +. Configures the broker/router port. You can do it per channel (using +the `port` attribute) or globally using `rabbitmq-port`. The default +is `5672`. +. Configures the broker/router username if required. You can do it per +channel (using the `username` attribute) or globally using +`rabbitmq-username`. +. Configures the broker/router password if required. You can do it per +channel (using the `password` attribute) or globally using +`rabbitmq-password`. +. Instructs the `prices` channel to be managed by the RabbitMQ OG +connector +. Supplies the default routing key to be included in outbound +messages; this will be used if the "raw payload" form of message +sending is used (see below). + +[NOTE] +==== + +You don't need to set the RabbitMQ exchange name. By default, it uses +the channel name (`prices`) as the name of the exchange to send messages +to. You can configure the `exchange.name` attribute to override it. + +==== + +Then, your application can send `Message++<++Double++>++` to the prices channel. +It can use `double` payloads as in the following snippet: + +[source,java] +---- +include::{examples-dir}/rabbitmq/og/outbound/RabbitMQPriceProducer.java[indent=0,opts=optional] +---- + +Or, you can send `Message++<++Double++>++`, which affords the opportunity to +explicitly specify metadata on the outgoing message: + +[source,java] +---- +include::{examples-dir}/rabbitmq/og/outbound/RabbitMQPriceMessageProducer.java[indent=0,opts=optional] +---- + +== Serialization + +When sending a `Message++<++T++>++`, the connector converts the message into a +RabbitMQ Message. The payload is converted to the RabbitMQ Message body. + +[width="100%",cols="32%,68%",options="header",] +|=== +|T |RabbitMQ Message Body +|primitive types or `UUID`/`String` |String value with `content++_++type` set to `text/plain` +|`byte++[]++` |Binary content, with `content++_++type` set to `application/octet-stream` +|Any other class |The payload is converted via `toString()` with `content++_++type` set to `application/json` +|=== + +[NOTE] +==== + +Unlike the Vert.x-based RabbitMQ connector, the OG connector does not +handle Vert.x `JsonObject`, `JsonArray`, or `Buffer` types directly. +For JSON payloads, serialize to `String` or `byte++[]++` before sending. + +==== + +If the message payload cannot be serialized, the message is _nacked_. + +== Outbound Metadata + +When sending `Messages`, you can add an instance of link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-rabbitmq-og/{javadoc-version}/io/smallrye/reactive/messaging/rabbitmq/og/OutgoingRabbitMQMetadata.html[`OutgoingRabbitMQMetadata`] +to influence how the message is handled by RabbitMQ. For example, you +can configure the routing key, timestamp and headers: + +[source,java] +---- +include::{examples-dir}/rabbitmq/og/outbound/RabbitMQOutboundMetadataExample.java[tags=code,indent=0,opts=optional] +---- + +[NOTE] +==== + +The `OutgoingRabbitMQMetadata` in the OG connector uses +`OutgoingRabbitMQMetadata.builder()` as the entry point for building +metadata (instead of `new OutgoingRabbitMQMetadata.Builder()`). It +also uses `java.util.Date` for timestamps instead of `ZonedDateTime`. + +==== + +== Publisher Confirms + +The OG connector supports https://www.rabbitmq.com/docs/confirms#publisher-confirms[publisher confirms] +for reliable message publishing. When enabled, the connector waits for +the broker to acknowledge each published message before considering the +send operation complete. + +[source,properties] +---- +mp.messaging.outgoing.prices.publish-confirms=true +---- + +When publisher confirms are enabled, message acknowledgement in +Reactive Messaging is tied to the broker's confirmation. If the broker +does not confirm the message, the Reactive Messaging message is _nacked_. + +== Acknowledgement + +By default, the Reactive Messaging `Message` is acknowledged when the +broker acknowledges the message. + +== Configuration Reference + +NOTE: This table is generated from the project's build and is not available for this version. + +== Using existing destinations + +To use an existing _exchange_, you may need to configure the +`exchange.name` attribute. + +For example, if you have a RabbitMQ broker already configured with an +exchange called `people` that you wish to send messages to, you need the +following configuration: + +[source,properties] +---- +mp.messaging.outgoing.people.connector=smallrye-rabbitmq-og +---- + +You would need to configure the `exchange.name` attribute, if the +exchange name were not the channel name: + +[source,properties] +---- +mp.messaging.outgoing.people-out.connector=smallrye-rabbitmq-og +mp.messaging.outgoing.people-out.exchange.name=people +---- + +If you want RabbitMQ to create the `people` exchange, you need the +following configuration: + +[source,properties] +---- +mp.messaging.outgoing.people-out.connector=smallrye-rabbitmq-og +mp.messaging.outgoing.people-out.exchange.name=people +mp.messaging.outgoing.people-out.exchange.declare=true +---- + +[NOTE] +==== + +The above example will create a `topic` exchange and use an empty +default `routing-key` (unless overridden programatically using outgoing +metadata for the message). If you want to create a different type of +exchange or have a different default routing key, then the +`exchange.type` and `default-routing-key` properties need to be +explicitly specified. + +==== + +== Sending to specific queues via the default exchange + +To send a message to a specific queue (usually a reply queue), +you have to configure the default exchange as an outgoing channel and set the name of the queue as routing key in the message metadata. +The name of the exchange needs to be set to `""`. + +[source,properties] +---- +mp.messaging.outgoing.channel-name-for-default-exchange.connector=smallrye-rabbitmq-og +mp.messaging.outgoing.channel-name-for-default-exchange.exchange.name="" +---- + +== Custom arguments for Exchange declaration + +When exchange declaration is made by the Reactive Messaging channel, using the `exchange.declare=true` configuration, +custom exchange arguments can be specified using the `exchange.arguments` attribute. +`exchange.arguments` accepts the identifier (using the `@Identifier` qualifier) of a `Map++<++String, Object++>++` exposed as a CDI bean. +If no arguments has been configured, the default *rabbitmq-exchange-arguments* identifier is looked for. + +The following CDI bean produces such a configuration identified with *my-arguments*: + +[source,java] +---- +include::{examples-dir}/rabbitmq/og/customization/ArgumentProducers.java[indent=0,opts=optional] +---- + +Then the channel can be configured to use those arguments in exchange declaration: + +[source,properties] +---- +mp.messaging.outgoing.data.exchange.arguments=my-arguments +---- + +Similarly, the `dead-letter-exchange.arguments` allows configuring custom arguments for dead letter exchange when one is declared (`dlx.declare=true`). + +== Retry on failure + +The OG connector supports automatic retry when message publishing fails. +This is controlled by two configuration attributes: + +* `retry-on-fail-attempts` (default: 6) - the number of retry attempts +before giving up. +* `retry-on-fail-interval` (default: 5 seconds) - the interval between +retry attempts. + +[source,properties] +---- +mp.messaging.outgoing.prices.retry-on-fail-attempts=10 +mp.messaging.outgoing.prices.retry-on-fail-interval=3 +---- diff --git a/content/docs/smallrye-reactive-messaging/main/sns/sending-aws-sns-messages.adoc b/content/docs/smallrye-reactive-messaging/main/sns/sending-aws-sns-messages.adoc new file mode 100644 index 000000000..69282c824 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/sns/sending-aws-sns-messages.adoc @@ -0,0 +1,72 @@ += Sending AWS SNS Messages +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The AWS SNS connector allows you to send messages to an AWS SNS topic. + +== Sending messages + +Before you start, you need to have an AWS account and an SNS topic created. +To send messages to an SNS topic, you need to create a method that produces messages to the topic. + +Then, your application can send `Message++<++String++>++` to the prices channel. +It can use `String` payloads as in the following snippet: + +[source,java] +---- +include::{examples-dir}/sns/outbound/SnsProducer.java[indent=0] +---- + +Or, you can send `Message++<++Double++>++`, which affords the opportunity to +explicitly specify metadata on the outgoing message: + +[source,java] +---- +include::{examples-dir}/sns/outbound/SnsMessageProducer.java[indent=0] +---- + +== Sending messages in batch + +You can configure the outbound channel to send messages in batch of maximum 10 messages (AWS SNS limitation). + +You can customize the size of batches, `10` being the default batch size, and the delay to wait for new messages to be added to the batch, 3000ms being the default delay: + +[source,java] +---- +mp.messaging.outgoing.prices.connector=smallrye-sns +mp.messaging.outgoing.prices.topic=prices +mp.messaging.outgoing.prices.batch=true +mp.messaging.outgoing.prices.batch-size=5 +mp.messaging.outgoing.prices.batch-delay=3000 +---- + +== Serialization + +When sending a `Message++<++T++>++`, the connector converts the message into a AWS SNS Message. +How the message is converted depends on the payload type: + +* If the payload is of type `PublishRequest` it is sent as is. +* If the payload is of type `PublishRequest.Builder`, the topic url is set and sent. +* If the payload is of primitive types the payload is converted to String and the message attribute `++_++classname` is set to the class name of the payload. +* If the payload is of any other object type, the payload is serialized (using the `JsonMapping` implementation discovered) and the message attribute `++_++classname` is set to the class name of the payload. + +If the message payload cannot be serialized to JSON, the message is _nacked_. + +== Outbound Metadata + +When sending `Messages`, you can add an instance of link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-aws-sns/{javadoc-version}/io/smallrye/reactive/messaging/aws/sns/SnsOutboundMetadata.html[`SnsOutboundMetadata`] +to influence how the message is handled by AWS SNS. For example, you can configure the routing key, timestamp and headers: + +[source,java] +---- +include::{examples-dir}/sns/outbound/SnsOutboundMetadataExample.java[tags=code,indent=0] +---- + +== Acknowledgement + +By default, the Reactive Messaging `Message` is acknowledged when the +send message request is successful. If the message is not sent successfully, the message is _nacked_. + +== Configuration Reference + +include::{generated-dir}/smallrye-sns-outgoing.adoc[indent=0] diff --git a/content/docs/smallrye-reactive-messaging/main/sns/sns.adoc b/content/docs/smallrye-reactive-messaging/main/sns/sns.adoc new file mode 100644 index 000000000..b24bf08f7 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/sns/sns.adoc @@ -0,0 +1,59 @@ += AWS SNS Connector +:version-root: ../ +include::{version-root}_attributes.adoc[] + +[WARNING] +.Preview +==== + +The AWS SNS Connector is currently in *preview*. + +==== + +The AWS SNS Connector adds support for AWS SNS to Reactive Messaging. + +With this connector, your application can send messages to a AWS SNS topic. + +The AWS SNS connector is based on the https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/home.html[AWS SDK for Java V2]. + +== Using the AWS SNS connector + +To use the AWS SNS Connector, add the following dependency to your project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.reactive + smallrye-reactive-messaging-aws-sns + {project-version} + +---- + +The connector name is: `smallrye-sns`. + +So, to indicate that a channel is managed by this connector you need: + +[source,properties] +---- +# Inbound +mp.messaging.incoming.[channel-name].connector=smallrye-sns + +# Outbound +mp.messaging.outgoing.[channel-name].connector=smallrye-sns +---- + +== Configuration + +When available the AWS SNS connector discovers the SNS client as a CDI bean. +This allows using the connector with the https://docs.quarkiverse.io/quarkus-amazon-services/dev/amazon-sns.html[Quarkiverse AWS SNS extension]. +If the SNS client is not available as a CDI bean, the connector creates a new client using the provided configuration. + +* `topic` - The name of the SNS queue, defaults to channel name if not provided. +* `region` - The name of the SNS region. +* `endpoint-override` - The endpoint url override. +* `credentials-provider` - The fully qualified class name of the credential provider to be used in the client, if not provided the default provider chain is used. + +== Additional Resources + +* https://docs.aws.amazon.com/sns/index.html[AWS SNS Documentation] +* https://docs.quarkiverse.io/quarkus-amazon-services/dev/amazon-sns.html[Quarkiverse AWS SNS Extension] diff --git a/content/docs/smallrye-reactive-messaging/main/sqs/receiving-aws-sqs-messages.adoc b/content/docs/smallrye-reactive-messaging/main/sqs/receiving-aws-sqs-messages.adoc new file mode 100644 index 000000000..2b03c4832 --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/sqs/receiving-aws-sqs-messages.adoc @@ -0,0 +1,140 @@ += Receiving AWS SQS Messages +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The AWS SQS connector allows you to receive messages from an AWS SQS queue. + +== Receiving messages + +Before you start, you need to have an AWS account and a SQS queue created. +To receive messages from an SQS queue, you need to create a method that consumes messages from the queue. + +[source,properties] +---- +mp.messaging.incoming.data.connector=smallrye-sqs +mp.messaging.incoming.data.queue=my-queue +---- + +Then, your application receives `Message++<++String++>++`. +You can consume the payload directly: + +[source,java] +---- +include::{examples-dir}/sqs/inbound/SqsStringConsumer.java[indent=0] +---- + +Or, you can retrieve the `Message++<++String++>++`: + +[source,java] +---- +include::{examples-dir}/sqs/inbound/SqsMessageStringConsumer.java[indent=0] +---- + +You also can directly consume the `software.amazon.awssdk.services.sqs.model.Message`: + +[source,java] +---- +include::{examples-dir}/sqs/inbound/SqsSdkMessageConsumer.java[indent=0] +---- + +=== Receive message request customizer + +The receive message requests sent to AWS SQS can be customized by providing a CDI bean implementation of +link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-aws-sqs/{javadoc-version}/io/smallrye/reactive/messaging/aws/sqs/SqsReceiveMessageRequestCustomizer.html[`SqsReceiveMessageRequestCustomizer`] +and configuring it's identifier using the `receive.request.customizer` connector attribute. + +[source,java] +---- +include::{examples-dir}/sqs/inbound/SqsReceiveMessageRequestCustomizerExample.java[indent=0] +---- + +[source,properties] +---- +mp.messaging.incoming.data.connector=smallrye-sqs +mp.messaging.incoming.data.queue=my-queue +mp.messaging.incoming.data.receive.request.customizer=my-customizer +---- + +Receive requests failed with retryable exceptions are retried automatically, by setting the failed request id. + +=== Receive message request pause and resume + +The AWS SQS connector fetches messages by continuously sending receive message requests. +If messages are not processed in a timely manner, the connector pauses fetching messages until queued messages are processed. + +The pause resume can be disabled using the `receive.request.pause.resume` connector attribute. + +[source,properties] +---- +mp.messaging.incoming.data.receive.request.pause.resume=false +---- + +== Deserialization + +The connector converts incoming SQS Messages into Reactive Messaging `Message++<++T++>++` instances. + +The payload type `T` is determined based on the value of the SQS message attribute `++_++classname`. + +If you send messages with the AWS SQS connector (outbound connector), +the `++_++classname` attribute is automatically added to the message. +The primitive types are transformed from the string representation to the corresponding Java type. +For objects, if one of the `JsonMapping` modules is present on the classpath, +the connector used that JSON module to deserialize the message body to objects. + +If the `++_++classname` attribute is not present, the payload is deserialized as a `String`. + +[source,java] +---- +include::{examples-dir}/sqs/json/SqsJsonMapping.java[tags=code,indent=0] +---- + +== Inbound Metadata + +Messages coming from SQS contain an instance of link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-aws-sqs/{javadoc-version}/io/smallrye/reactive/messaging/aws/sqs/SqsIncomingMetadata.html[`SqsIncomingMetadata`] +in the metadata. + +SQS message attributes can be accessed from the metadata either by name or by the `MessageAttributeValue` object. + +[source,java] +---- +include::{examples-dir}/sqs/inbound/SqsMetadataExample.java[indent=0] +---- + +== Acknowledgement Strategies + +The default strategy for acknowledging AWS SQS Message is to _delete_ the message from the queue. +You can set the `ack-strategy` attribute to `ignore` if you want to ignore the message. + +++[++NOTE++]++ Deprecated +`ack.delete` attribute is deprecated and will be removed in a future release. + +You can implement a custom strategy by implementing the link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-aws-sqs/{javadoc-version}/io/smallrye/reactive/messaging/aws/sqs/SqsAckHandler.html[`SqsAckHandler`], +interface with a `Factory` class and registering it as a CDI bean with an `@Identifier`. + +[source,java] +---- +include::{examples-dir}/sqs/inbound/SqsCustomAckStrategy.java[indent=0] +---- + +== Failure Strategies + +The default strategy for handling message processing failures is `ignore`. +It lets the visibility timeout of the message consumer to expire and reconsume the message. + +Other possible strategies are: + +* `fail`: the failure is logged and the channel fail-stops. +* `delete`: the message is removed from the queue. +* `visibility`: the message visibility timeout is reset to 0. + +You can implement a custom strategy by implementing the link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-aws-sqs/{javadoc-version}/io/smallrye/reactive/messaging/aws/sqs/SqsFailureHandler.html[`SqsFailureHandler`], +interface with a `Factory` class and registering it as a CDI bean with an `@Identifier`. + +[source,java] +---- +include::{examples-dir}/sqs/inbound/SqsCustomNackStrategy.java[indent=0] +---- + +== Configuration Reference + +include::{generated-dir}/smallrye-sqs-incoming.adoc[indent=0] diff --git a/content/docs/smallrye-reactive-messaging/main/sqs/sending-aws-sqs-messages.adoc b/content/docs/smallrye-reactive-messaging/main/sqs/sending-aws-sqs-messages.adoc new file mode 100644 index 000000000..94e92d1ce --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/sqs/sending-aws-sqs-messages.adoc @@ -0,0 +1,74 @@ += Sending AWS SQS Messages +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The AWS SQS connector allows you to send messages to an AWS SQS queue. + +== Sending messages + +Before you start, you need to have an AWS account and an SQS queue created. +To send messages to an SQS queue, you need to create a method that produces messages to the queue. + +Then, your application can send `Message++<++String++>++` to the prices channel. +It can use `String` payloads as in the following snippet: + +[source,java] +---- +include::{examples-dir}/sqs/outbound/SqsStringProducer.java[indent=0] +---- + +Or, you can send `Message++<++Double++>++`, which affords the opportunity to +explicitly specify metadata on the outgoing message: + +[source,java] +---- +include::{examples-dir}/sqs/outbound/SqsMessageStringProducer.java[indent=0] +---- + +== Sending messages in batch + +You can configure the outbound channel to send messages in batch of maximum 10 messages (AWS SQS limitation). + +You can customize the size of batches, `10` being the default batch size, and the delay to wait for new messages to be added to the batch, 3000ms being the default delay: + +[source,java] +---- +mp.messaging.outgoing.prices.connector=smallrye-sqs +mp.messaging.outgoing.prices.queue=prices +mp.messaging.outgoing.prices.batch=true +mp.messaging.outgoing.prices.batch-size=5 +mp.messaging.outgoing.prices.batch-delay=3000 +---- + +== Serialization + +When sending a `Message++<++T++>++`, the connector converts the message into a AWS SQS Message. +How the message is converted depends on the payload type: + +* If the payload is of type `SendMessageRequest` it is sent as is. +* If the payload is of type `SendMessageRequest.Builder`, the queue url is set and sent. +* If the payload is of type `software.amazon.awssdk.services.sqs.model.Message` it is usd to set the message body and attributes. +* If the payload is of primitive types the paylaod is converted to String and the message attribute `++_++classname` is set to the class name of the payload. +* If the payload is of any other object type, the payload is serialized (using the `JsonMapping` implementation discovered) and the message attribute `++_++classname` is set to the class name of the payload. + +If the message payload cannot be serialized to JSON, the message is _nacked_. + +== Outbound Metadata + +When sending `Messages`, you can add an instance of link:https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-aws-sqs/{javadoc-version}/io/smallrye/reactive/messaging/aws/sqs/SqsOutboundMetadata.html[`SqsOutboundMetadata`] +to influence how the message is handled by AWS SQS. For example, you +can configure the routing key, timestamp and headers: + +[source,java] +---- +include::{examples-dir}/sqs/outbound/SqsOutboundMetadataExample.java[tags=code,indent=0] +---- + +== Acknowledgement + +By default, the Reactive Messaging `Message` is acknowledged when the +send message request is successful. If the message is not sent successfully, the message is _nacked_. + +== Configuration Reference + +include::{generated-dir}/smallrye-sqs-outgoing.adoc[indent=0] diff --git a/content/docs/smallrye-reactive-messaging/main/sqs/sqs.adoc b/content/docs/smallrye-reactive-messaging/main/sqs/sqs.adoc new file mode 100644 index 000000000..83b664f6d --- /dev/null +++ b/content/docs/smallrye-reactive-messaging/main/sqs/sqs.adoc @@ -0,0 +1,62 @@ += AWS SQS Connector +:version-root: ../ +include::{version-root}_attributes.adoc[] + +[WARNING] +.Preview +==== + +The AWS SQS Connector is currently in *preview*. + +==== + +The AWS SQS Connector adds support for AWS SQS to Reactive Messaging. + +With this connector, your application can: + +* receive messages from a RabbitMQ queue +* send messages to a RabbitMQ exchange + +The AWS SQS connector is based on the https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/home.html[AWS SDK for Java V2]. + +== Using the AWS SQS connector + +To use the AWS SQS Connector, add the following dependency to your project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.reactive + smallrye-reactive-messaging-aws-sqs + {project-version} + +---- + +The connector name is: `smallrye-sqs`. + +So, to indicate that a channel is managed by this connector you need: + +[source,properties] +---- +# Inbound +mp.messaging.incoming.[channel-name].connector=smallrye-sqs + +# Outbound +mp.messaging.outgoing.[channel-name].connector=smallrye-sqs +---- + +== Configuration + +When available the AWS SQS connector discovers the SQS client as a CDI bean. +This allows using the connector with the https://docs.quarkiverse.io/quarkus-amazon-services/dev/amazon-sqs.html[Quarkiverse AWS SQS extension]. +If the SQS client is not available as a CDI bean, the connector creates a new client using the provided configuration. + +* `queue` - The name of the SQS queue, defaults to channel name if not provided. +* `region` - The name of the SQS region. +* `endpoint-override` - The endpoint url override. +* `credentials-provider` - The fully qualified class name of the credential provider to be used in the client, if not provided the default provider chain is used. + +== Additional Resources + +* https://docs.aws.amazon.com/sqs/index.html[AWS SQS Documentation] +* https://docs.quarkiverse.io/quarkus-amazon-services/dev/amazon-sqs.html[Quarkiverse AWS SQS Extension] diff --git a/content/docs/smallrye-stork/1/_attributes.adoc b/content/docs/smallrye-stork/1/_attributes.adoc new file mode 100644 index 000000000..332c29b89 --- /dev/null +++ b/content/docs/smallrye-stork/1/_attributes.adoc @@ -0,0 +1,20 @@ +:project-version: 1.4.2 +// Attributes shared by every page. smallrye.io overrides the ones it needs when it publishes. +// Pages in subdirectories set :version-root: (../ per level) before including this file. +ifndef::version-root[:version-root:] +:idprefix: +:idseparator: - +:sectanchors: +:examples-dir: {version-root}_examples +:partialsdir: {version-root}_partials +:project-name: SmallRye Stork +:javadoc-version: latest +:generated-dir: {version-root}_generated + +// Added by smallrye.io (tools/docs/Docs.java sync) +:javadoc-version: 1.4.2 +:relfileprefix: /docs/smallrye-stork/1/ +:imagesdir: /docs/smallrye-stork/1 +:docs-project: smallrye-stork +:docs-version: 1 +:docs-ref: 1.4.2 diff --git a/content/docs/smallrye-stork/1/_examples/examples/AcmeDiscoveryApiUsage.java b/content/docs/smallrye-stork/1/_examples/examples/AcmeDiscoveryApiUsage.java new file mode 100644 index 000000000..5463ed212 --- /dev/null +++ b/content/docs/smallrye-stork/1/_examples/examples/AcmeDiscoveryApiUsage.java @@ -0,0 +1,18 @@ +package examples; + +import io.smallrye.mutiny.Uni; +import io.smallrye.stork.api.ServiceDefinition; +import io.smallrye.stork.api.ServiceInstance; +import io.smallrye.stork.api.StorkServiceRegistry; + +public class AcmeDiscoveryApiUsage { + + public void example(StorkServiceRegistry stork) { + stork.defineIfAbsent("my-service", ServiceDefinition.of( + new AcmeConfiguration().withHost("my-host")) + ); + + Uni uni = stork.getService("my-service").selectInstance(); + } + +} diff --git a/content/docs/smallrye-stork/1/_examples/examples/AcmeLoadBalancer.java b/content/docs/smallrye-stork/1/_examples/examples/AcmeLoadBalancer.java new file mode 100644 index 000000000..6fb0b1be1 --- /dev/null +++ b/content/docs/smallrye-stork/1/_examples/examples/AcmeLoadBalancer.java @@ -0,0 +1,32 @@ +package examples; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Random; + +import io.smallrye.stork.api.LoadBalancer; +import io.smallrye.stork.api.NoServiceInstanceFoundException; +import io.smallrye.stork.api.ServiceInstance; + +public class AcmeLoadBalancer implements LoadBalancer { + + private final Random random; + + public AcmeLoadBalancer(AcmeLoadBalancerConfiguration config) { + random = new Random(); + } + + @Override + public ServiceInstance selectServiceInstance(Collection serviceInstances) { + if (serviceInstances.isEmpty()) { + throw new NoServiceInstanceFoundException("No services found."); + } + int index = random.nextInt(serviceInstances.size()); + return new ArrayList<>(serviceInstances).get(index); + } + + @Override + public boolean requiresStrictRecording() { + return false; + } +} diff --git a/content/docs/smallrye-stork/1/_examples/examples/AcmeLoadBalancerProvider.java b/content/docs/smallrye-stork/1/_examples/examples/AcmeLoadBalancerProvider.java new file mode 100644 index 000000000..d793517c9 --- /dev/null +++ b/content/docs/smallrye-stork/1/_examples/examples/AcmeLoadBalancerProvider.java @@ -0,0 +1,20 @@ +package examples; + +import io.smallrye.stork.api.LoadBalancer; +import io.smallrye.stork.api.ServiceDiscovery; +import io.smallrye.stork.api.config.LoadBalancerAttribute; +import io.smallrye.stork.api.config.LoadBalancerType; +import io.smallrye.stork.spi.LoadBalancerProvider; + +@LoadBalancerType("acme-load-balancer") +@LoadBalancerAttribute(name = "my-attribute", + description = "Attribute that alters the behavior of the LoadBalancer") +public class AcmeLoadBalancerProvider implements + LoadBalancerProvider { + + @Override + public LoadBalancer createLoadBalancer(AcmeLoadBalancerConfiguration config, + ServiceDiscovery serviceDiscovery) { + return new AcmeLoadBalancer(config); + } +} diff --git a/content/docs/smallrye-stork/1/_examples/examples/AcmeSelectorApiUsage.java b/content/docs/smallrye-stork/1/_examples/examples/AcmeSelectorApiUsage.java new file mode 100644 index 000000000..aa996d68b --- /dev/null +++ b/content/docs/smallrye-stork/1/_examples/examples/AcmeSelectorApiUsage.java @@ -0,0 +1,21 @@ +package examples; + +import io.smallrye.mutiny.Uni; +import io.smallrye.stork.api.ServiceDefinition; +import io.smallrye.stork.api.ServiceInstance; +import io.smallrye.stork.api.StorkServiceRegistry; +import io.smallrye.stork.servicediscovery.staticlist.StaticConfiguration; + +public class AcmeSelectorApiUsage { + + public void example(StorkServiceRegistry stork) { + String list = "localhost:8080, localhost:8081"; + stork.defineIfAbsent("my-service", ServiceDefinition.of( + new StaticConfiguration().withAddressList(list), + new AcmeLoadBalancerConfiguration().withMyAttribute("my-value")) + ); + + Uni uni = stork.getService("my-service").selectInstance(); + } + +} diff --git a/content/docs/smallrye-stork/1/_examples/examples/AcmeServiceDiscovery.java b/content/docs/smallrye-stork/1/_examples/examples/AcmeServiceDiscovery.java new file mode 100644 index 000000000..b9368c4b4 --- /dev/null +++ b/content/docs/smallrye-stork/1/_examples/examples/AcmeServiceDiscovery.java @@ -0,0 +1,32 @@ +package examples; + +import java.util.Collections; +import java.util.List; + +import io.smallrye.mutiny.Uni; +import io.smallrye.stork.api.ServiceDiscovery; +import io.smallrye.stork.api.ServiceInstance; +import io.smallrye.stork.impl.DefaultServiceInstance; +import io.smallrye.stork.utils.ServiceInstanceIds; + +public class AcmeServiceDiscovery implements ServiceDiscovery { + + private final String host; + private final int port; + + public AcmeServiceDiscovery(AcmeConfiguration configuration) { + this.host = configuration.getHost(); + this.port = Integer.parseInt(configuration.getPort()); + } + + @Override + public Uni> getServiceInstances() { + // Proceed to the lookup... + // Here, we just return a DefaultServiceInstance with the configured host and port + // The last parameter specifies whether the communication with the instance should + // happen over a secure connection + DefaultServiceInstance instance = + new DefaultServiceInstance(ServiceInstanceIds.next(), host, port, false); + return Uni.createFrom().item(() -> Collections.singletonList(instance)); + } +} diff --git a/content/docs/smallrye-stork/1/_examples/examples/AcmeServiceDiscoveryProvider.java b/content/docs/smallrye-stork/1/_examples/examples/AcmeServiceDiscoveryProvider.java new file mode 100644 index 000000000..8ef1d50f6 --- /dev/null +++ b/content/docs/smallrye-stork/1/_examples/examples/AcmeServiceDiscoveryProvider.java @@ -0,0 +1,26 @@ +package examples; + +import io.smallrye.stork.api.ServiceDiscovery; +import io.smallrye.stork.api.config.ServiceConfig; +import io.smallrye.stork.api.config.ServiceDiscoveryAttribute; +import io.smallrye.stork.api.config.ServiceDiscoveryType; +import io.smallrye.stork.spi.StorkInfrastructure; +import io.smallrye.stork.spi.ServiceDiscoveryProvider; + +@ServiceDiscoveryType("acme") +@ServiceDiscoveryAttribute(name = "host", + description = "Host name of the service discovery server.", required = true) +@ServiceDiscoveryAttribute(name = "port", + description = "Hort of the service discovery server.", required = false) +public class AcmeServiceDiscoveryProvider + implements ServiceDiscoveryProvider { + + @Override + public ServiceDiscovery createServiceDiscovery( + AcmeConfiguration config, + String serviceName, + ServiceConfig serviceConfig, + StorkInfrastructure storkInfrastructure) { + return new AcmeServiceDiscovery(config); + } +} diff --git a/content/docs/smallrye-stork/1/_examples/examples/CachedAcmeServiceDiscovery.java b/content/docs/smallrye-stork/1/_examples/examples/CachedAcmeServiceDiscovery.java new file mode 100644 index 000000000..204aa78c9 --- /dev/null +++ b/content/docs/smallrye-stork/1/_examples/examples/CachedAcmeServiceDiscovery.java @@ -0,0 +1,31 @@ +package examples; + +import io.smallrye.mutiny.Uni; +import io.smallrye.stork.api.ServiceDiscovery; +import io.smallrye.stork.api.ServiceInstance; +import io.smallrye.stork.impl.CachingServiceDiscovery; +import io.smallrye.stork.impl.DefaultServiceInstance; +import io.smallrye.stork.utils.ServiceInstanceIds; + +import java.util.Collections; +import java.util.List; + +public class CachedAcmeServiceDiscovery extends CachingServiceDiscovery { + + private final String host; + private final int port; + + public CachedAcmeServiceDiscovery(CachedAcmeConfiguration configuration) { + super(configuration.getRefreshPeriod()); // (1) + this.host = configuration.getHost(); + this.port = Integer.parseInt(configuration.getPort()); + } + + @Override // (2) + public Uni> fetchNewServiceInstances(List previousInstances) { + // Retrieve services... + DefaultServiceInstance instance = + new DefaultServiceInstance(ServiceInstanceIds.next(), host, port, false); + return Uni.createFrom().item(() -> Collections.singletonList(instance)); + } +} diff --git a/content/docs/smallrye-stork/1/_examples/examples/CachedAcmeServiceDiscoveryProvider.java b/content/docs/smallrye-stork/1/_examples/examples/CachedAcmeServiceDiscoveryProvider.java new file mode 100644 index 000000000..ac3e9e2f3 --- /dev/null +++ b/content/docs/smallrye-stork/1/_examples/examples/CachedAcmeServiceDiscoveryProvider.java @@ -0,0 +1,30 @@ +package examples; + +import io.smallrye.stork.api.ServiceDiscovery; +import io.smallrye.stork.api.config.ServiceConfig; +import io.smallrye.stork.api.config.ServiceDiscoveryAttribute; +import io.smallrye.stork.api.config.ServiceDiscoveryType; +import io.smallrye.stork.impl.CachingServiceDiscovery; +import io.smallrye.stork.spi.ServiceDiscoveryProvider; +import io.smallrye.stork.spi.StorkInfrastructure; + +@ServiceDiscoveryType("cached-acme") +@ServiceDiscoveryAttribute(name = "host", + description = "Host name of the service discovery server.", required = true) +@ServiceDiscoveryAttribute(name = "port", + description = "Hort of the service discovery server.", required = false) +@ServiceDiscoveryAttribute(name = "refresh-period", + description = "Service discovery cache refresh period.", + defaultValue = CachingServiceDiscovery.DEFAULT_REFRESH_INTERVAL) +public class CachedAcmeServiceDiscoveryProvider + implements ServiceDiscoveryProvider { + + @Override + public ServiceDiscovery createServiceDiscovery( + CachedAcmeConfiguration config, + String serviceName, + ServiceConfig serviceConfig, + StorkInfrastructure storkInfrastructure) { + return new CachedAcmeServiceDiscovery(config); + } +} diff --git a/content/docs/smallrye-stork/1/_examples/examples/DefinitionExample.java b/content/docs/smallrye-stork/1/_examples/examples/DefinitionExample.java new file mode 100644 index 000000000..f3bb7b73c --- /dev/null +++ b/content/docs/smallrye-stork/1/_examples/examples/DefinitionExample.java @@ -0,0 +1,23 @@ +package examples; + +import io.smallrye.stork.Stork; +import io.smallrye.stork.api.ServiceDefinition; +import io.smallrye.stork.loadbalancer.random.RandomConfiguration; +import io.smallrye.stork.servicediscovery.staticlist.StaticConfiguration; + +public class DefinitionExample { + + public static void example(Stork stork) { + String example = "localhost:8080, localhost:8081"; + + // A service using a static list of locations as discovery + // As not set, it defaults to round-robin to select the instance. + stork.defineIfAbsent("my-service", + ServiceDefinition.of(new StaticConfiguration().withAddressList(example))); + + // Another service using the random selection strategy, instead of round-robin + stork.defineIfAbsent("my-second-service", + ServiceDefinition.of(new StaticConfiguration().withAddressList(example), + new RandomConfiguration())); + } +} diff --git a/content/docs/smallrye-stork/1/_examples/examples/HelloClient.java b/content/docs/smallrye-stork/1/_examples/examples/HelloClient.java new file mode 100644 index 000000000..3f3021a8d --- /dev/null +++ b/content/docs/smallrye-stork/1/_examples/examples/HelloClient.java @@ -0,0 +1,19 @@ +package examples; + +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +import javax.ws.rs.Consumes; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +@Path("/") +@RegisterRestClient(baseUri = "stork://hello-service/hello") +public interface HelloClient { + @POST + @Produces(MediaType.TEXT_PLAIN) + @Consumes(MediaType.TEXT_PLAIN) + String echo(String name); +} + diff --git a/content/docs/smallrye-stork/1/_examples/examples/InitializationExample.java b/content/docs/smallrye-stork/1/_examples/examples/InitializationExample.java new file mode 100644 index 000000000..cb93ca0bd --- /dev/null +++ b/content/docs/smallrye-stork/1/_examples/examples/InitializationExample.java @@ -0,0 +1,18 @@ +package examples; + +import io.smallrye.stork.Stork; +import io.smallrye.stork.api.ServiceDefinition; +import io.smallrye.stork.api.ServiceInstance; +import io.smallrye.stork.loadbalancer.random.RandomConfiguration; +import io.smallrye.stork.servicediscovery.staticlist.StaticConfiguration; + +import java.time.Duration; + +public class InitializationExample { + + public static void main(String[] args) { + Stork.initialize(); + Stork stork = Stork.getInstance(); + // ... + } +} diff --git a/content/docs/smallrye-stork/1/_examples/examples/LookupExample.java b/content/docs/smallrye-stork/1/_examples/examples/LookupExample.java new file mode 100644 index 000000000..71e232d12 --- /dev/null +++ b/content/docs/smallrye-stork/1/_examples/examples/LookupExample.java @@ -0,0 +1,27 @@ +package examples; + +import io.smallrye.mutiny.Uni; +import io.smallrye.stork.Stork; +import io.smallrye.stork.api.Service; +import io.smallrye.stork.api.ServiceDefinition; +import io.smallrye.stork.api.ServiceInstance; +import io.smallrye.stork.loadbalancer.random.RandomConfiguration; +import io.smallrye.stork.servicediscovery.staticlist.StaticConfiguration; + +import java.util.List; +import java.util.Map; + +public class LookupExample { + + public static void example(Stork stork) { + Service service = stork.getService("my-service"); + + // Gets all the available instances: + Uni> instances = service.getInstances(); + // Select one instance using the load balancing strategy + Uni instance = service.selectInstance(); + + // Gets all the managed services: + Map services = stork.getServices(); + } +} diff --git a/content/docs/smallrye-stork/1/_examples/examples/StorkApiExample.java b/content/docs/smallrye-stork/1/_examples/examples/StorkApiExample.java new file mode 100644 index 000000000..9936cd13e --- /dev/null +++ b/content/docs/smallrye-stork/1/_examples/examples/StorkApiExample.java @@ -0,0 +1,33 @@ +package examples; + +import io.smallrye.stork.Stork; +import io.smallrye.stork.api.ServiceDefinition; +import io.smallrye.stork.api.ServiceInstance; +import io.smallrye.stork.loadbalancer.random.RandomConfiguration; +import io.smallrye.stork.servicediscovery.staticlist.StaticConfiguration; + +import java.time.Duration; + +public class StorkApiExample { + + public static void main(String[] args) { + Stork.initialize(); + Stork stork = Stork.getInstance(); + + String example = "localhost:8080, localhost:8082"; + + // A service using a static list of locations as discovery + // As not set, it defaults to round-robin to select the instance. + stork.defineIfAbsent("my-service", + ServiceDefinition.of(new StaticConfiguration().withAddressList(example))); + + // Another service using the random selection strategy, instead of round-robin + stork.defineIfAbsent("my-second-service", + ServiceDefinition.of(new StaticConfiguration().withAddressList(example), + new RandomConfiguration())); + + ServiceInstance instance = stork.getService("my-second-service").selectInstance() + .await().atMost(Duration.ofSeconds(1)); + System.out.println(instance.getHost() + ":" + instance.getPort()); + } +} diff --git a/content/docs/smallrye-stork/1/_examples/examples/StorkEntryPointExample.java b/content/docs/smallrye-stork/1/_examples/examples/StorkEntryPointExample.java new file mode 100644 index 000000000..99343bd51 --- /dev/null +++ b/content/docs/smallrye-stork/1/_examples/examples/StorkEntryPointExample.java @@ -0,0 +1,14 @@ +package examples; + +import io.smallrye.stork.Stork; + +public class StorkEntryPointExample { + + public static void main(String[] args) { + Stork.initialize(); + Stork stork = Stork.getInstance(); + // ... + Stork.shutdown(); + } + +} diff --git a/content/docs/smallrye-stork/1/_examples/examples/StorkServiceExample.java b/content/docs/smallrye-stork/1/_examples/examples/StorkServiceExample.java new file mode 100644 index 000000000..c0b7dbb06 --- /dev/null +++ b/content/docs/smallrye-stork/1/_examples/examples/StorkServiceExample.java @@ -0,0 +1,18 @@ +package examples; + +import io.smallrye.stork.api.Service; +import io.smallrye.stork.Stork; + +public class StorkServiceExample { + + public static void main(String[] args) { + Stork.initialize(); + Stork stork = Stork.getInstance(); + + Service service = stork.getService("my-service"); + + // ... + Stork.shutdown(); + } + +} diff --git a/content/docs/smallrye-stork/1/_examples/examples/StorkServiceLookupExample.java b/content/docs/smallrye-stork/1/_examples/examples/StorkServiceLookupExample.java new file mode 100644 index 000000000..70c03cdc1 --- /dev/null +++ b/content/docs/smallrye-stork/1/_examples/examples/StorkServiceLookupExample.java @@ -0,0 +1,24 @@ +package examples; + +import java.time.Duration; +import java.util.List; + +import io.smallrye.stork.Stork; +import io.smallrye.stork.api.Service; +import io.smallrye.stork.api.ServiceInstance; + +public class StorkServiceLookupExample { + + public static void main(String[] args) { + Stork.initialize(); + Stork stork = Stork.getInstance(); + + Service service = stork.getService("my-service"); + List instances = service.getInstances() + .await().atMost(Duration.ofSeconds(5)); + + // ... + Stork.shutdown(); + } + +} diff --git a/content/docs/smallrye-stork/1/_examples/examples/StorkServiceSelectionExample.java b/content/docs/smallrye-stork/1/_examples/examples/StorkServiceSelectionExample.java new file mode 100644 index 000000000..a9e3dcc85 --- /dev/null +++ b/content/docs/smallrye-stork/1/_examples/examples/StorkServiceSelectionExample.java @@ -0,0 +1,25 @@ +package examples; + +import java.time.Duration; + +import io.smallrye.stork.Stork; +import io.smallrye.stork.api.Service; +import io.smallrye.stork.api.ServiceInstance; + +public class StorkServiceSelectionExample { + + public static void main(String[] args) { + Stork.initialize(); + Stork stork = Stork.getInstance(); + + Service service = stork.getService("my-service"); + ServiceInstance instance = service.selectInstance() + .await().atMost(Duration.ofSeconds(5)); + + System.out.println(instance.getHost() + ":" + instance.getPort()); + + // ... + Stork.shutdown(); + } + +} diff --git a/content/docs/smallrye-stork/1/_generated/composite-sd-attributes.adoc b/content/docs/smallrye-stork/1/_generated/composite-sd-attributes.adoc new file mode 100644 index 000000000..ae556fc30 --- /dev/null +++ b/content/docs/smallrye-stork/1/_generated/composite-sd-attributes.adoc @@ -0,0 +1,5 @@ +[cols=",,,",options="header",] +|=== +|Attribute |Mandatory |Default Value |Description +|`services` |Yes | |A comma-separated list of services that this services consists of. +|=== diff --git a/content/docs/smallrye-stork/1/_generated/consul-sd-attributes.adoc b/content/docs/smallrye-stork/1/_generated/consul-sd-attributes.adoc new file mode 100644 index 000000000..edf07b924 --- /dev/null +++ b/content/docs/smallrye-stork/1/_generated/consul-sd-attributes.adoc @@ -0,0 +1,10 @@ +[cols=",,,",options="header",] +|=== +|Attribute |Mandatory |Default Value |Description +|`consul-host` |No |`localhost` |The Consul host. +|`consul-port` |No |`8500` |The Consul port. +|`use-health-checks` |No |`true` |Whether to use health check. +|`application` |No | |The application name; if not defined Stork service name will be used. +|`refresh-period` |No |`5M` |Service discovery cache refresh period. +|`secure` |No | |whether the connection with the service should be encrypted with TLS. +|=== diff --git a/content/docs/smallrye-stork/1/_generated/consul-sr-attributes.adoc b/content/docs/smallrye-stork/1/_generated/consul-sr-attributes.adoc new file mode 100644 index 000000000..d126f6a7a --- /dev/null +++ b/content/docs/smallrye-stork/1/_generated/consul-sr-attributes.adoc @@ -0,0 +1,6 @@ +[cols=",,,",options="header",] +|=== +|Attribute |Mandatory |Default Value |Description +|`consul-host` |No |`localhost` |The Consul host. +|`consul-port` |No |`8500` |The Consul port. +|=== diff --git a/content/docs/smallrye-stork/1/_generated/eureka-sd-attributes.adoc b/content/docs/smallrye-stork/1/_generated/eureka-sd-attributes.adoc new file mode 100644 index 000000000..2a2581bcb --- /dev/null +++ b/content/docs/smallrye-stork/1/_generated/eureka-sd-attributes.adoc @@ -0,0 +1,13 @@ +[cols=",,,",options="header",] +|=== +|Attribute |Mandatory |Default Value |Description +|`eureka-host` |Yes | |The Eureka server host. +|`eureka-port` |No |`8761` |The Eureka server port. +|`eureka-context-path` |No |`/` |The Eureka server root context path. +|`application` |No | |The Eureka application Id; if not defined Stork service name will be used +|`eureka-trust-all` |No |`false` |Enable/Disable the TLS certificate verification +|`eureka-tls` |No |`false` |Use TLS to connect to the Eureka server +|`instance` |No | |The Eureka application instance Id +|`refresh-period` |No |`5M` |Service discovery cache refresh period. +|`secure` |No |`false` |Whether is should select the secured endpoint of the retrieved services. +|=== diff --git a/content/docs/smallrye-stork/1/_generated/eureka-sr-attributes.adoc b/content/docs/smallrye-stork/1/_generated/eureka-sr-attributes.adoc new file mode 100644 index 000000000..c07f772aa --- /dev/null +++ b/content/docs/smallrye-stork/1/_generated/eureka-sr-attributes.adoc @@ -0,0 +1,9 @@ +[cols=",,,",options="header",] +|=== +|Attribute |Mandatory |Default Value |Description +|`eureka-host` |No |`localhost` |The Eureka host. +|`eureka-port` |No |`8761` |The Eureka port. +|`eureka-context-path` |No |`/` |The Eureka server root context path. +|`eureka-trust-all` |No |`false` |Enable/Disable the TLS certificate verification +|`eureka-tls` |No |`false` |Use TLS to connect to the Eureka server +|=== diff --git a/content/docs/smallrye-stork/1/_generated/kubernetes-sd-attributes.adoc b/content/docs/smallrye-stork/1/_generated/kubernetes-sd-attributes.adoc new file mode 100644 index 000000000..5f19e7922 --- /dev/null +++ b/content/docs/smallrye-stork/1/_generated/kubernetes-sd-attributes.adoc @@ -0,0 +1,9 @@ +[cols=",,,",options="header",] +|=== +|Attribute |Mandatory |Default Value |Description +|`k8s-host` |No | |The Kubernetes API host. +|`k8s-namespace` |No | |The namespace of the service. Use all to discover all namespaces. +|`application` |No | |The Kubernetes application Id; if not defined Stork service name will be used. +|`refresh-period` |No |`5M` |Service discovery cache refresh period. +|`secure` |No | |Whether the connection with the service should be encrypted with TLS. +|=== diff --git a/content/docs/smallrye-stork/1/_generated/least-response-time-lb-attributes.adoc b/content/docs/smallrye-stork/1/_generated/least-response-time-lb-attributes.adoc new file mode 100644 index 000000000..dd977d2d0 --- /dev/null +++ b/content/docs/smallrye-stork/1/_generated/least-response-time-lb-attributes.adoc @@ -0,0 +1,7 @@ +[cols=",,,",options="header",] +|=== +|Attribute |Mandatory |Default Value |Description +|`declining-factor` |No |`0.9` |How much _score_ should decline in time, see Score calculation in the docs for details. +|`error-penalty` |No |`60s` |This load balancer treats an erroneous response as a response after this time. +|`use-secure-random` |No |`false` |Whether the load balancer should use a SecureRandom instead of a Random (default). Check https://stackoverflow.com/questions/11051205/difference-between-java-util-random-and-java-security-securerandom[this page] to understand the difference +|=== diff --git a/content/docs/smallrye-stork/1/_generated/power-of-two-choices-lb-attributes.adoc b/content/docs/smallrye-stork/1/_generated/power-of-two-choices-lb-attributes.adoc new file mode 100644 index 000000000..95a37a4df --- /dev/null +++ b/content/docs/smallrye-stork/1/_generated/power-of-two-choices-lb-attributes.adoc @@ -0,0 +1,5 @@ +[cols=",,,",options="header",] +|=== +|Attribute |Mandatory |Default Value |Description +|`use-secure-random` |No |`false` |Whether the load balancer should use a SecureRandom instead of a Random (default). Check https://stackoverflow.com/questions/11051205/difference-between-java-util-random-and-java-security-securerandom[this page] to understand the difference +|=== diff --git a/content/docs/smallrye-stork/1/_generated/random-lb-attributes.adoc b/content/docs/smallrye-stork/1/_generated/random-lb-attributes.adoc new file mode 100644 index 000000000..95a37a4df --- /dev/null +++ b/content/docs/smallrye-stork/1/_generated/random-lb-attributes.adoc @@ -0,0 +1,5 @@ +[cols=",,,",options="header",] +|=== +|Attribute |Mandatory |Default Value |Description +|`use-secure-random` |No |`false` |Whether the load balancer should use a SecureRandom instead of a Random (default). Check https://stackoverflow.com/questions/11051205/difference-between-java-util-random-and-java-security-securerandom[this page] to understand the difference +|=== diff --git a/content/docs/smallrye-stork/1/_generated/static-sd-attributes.adoc b/content/docs/smallrye-stork/1/_generated/static-sd-attributes.adoc new file mode 100644 index 000000000..469ca6230 --- /dev/null +++ b/content/docs/smallrye-stork/1/_generated/static-sd-attributes.adoc @@ -0,0 +1,7 @@ +[cols=",,,",options="header",] +|=== +|Attribute |Mandatory |Default Value |Description +|`address-list` |Yes | |A comma-separated list of addresses (host:port). The default port is 80. +|`secure` |No | |Whether the connection with the service should be encrypted with TLS. Default is false, except if the host:port uses the port is 443. +|`shuffle` |No |`false` |Whether the list of address must be shuffled to avoid using the first address on every startup. +|=== diff --git a/content/docs/smallrye-stork/1/_nav.yml b/content/docs/smallrye-stork/1/_nav.yml new file mode 100644 index 000000000..bc74a7346 --- /dev/null +++ b/content/docs/smallrye-stork/1/_nav.yml @@ -0,0 +1,46 @@ +title: SmallRye Stork +nav: +- title: Overview + page: index +- title: Concepts + page: concepts +- title: Programmatic API + page: programmatic-api +- title: Javadoc + url: https://javadoc.io/doc/io.smallrye.stork/smallrye-stork-api/latest/index.html +- title: Using Stork with Quarkus + page: quarkus +- title: Service Discovery + items: + - title: Consul + page: service-discovery/consul + - title: DNS + page: service-discovery/dns + - title: Kubernetes + page: service-discovery/kubernetes + - title: KNative + page: service-discovery/knative + - title: Eureka + page: service-discovery/eureka + - title: Composite + page: service-discovery/composite + - title: Static List + page: service-discovery/static-list + - title: Custom Service Discovery + page: service-discovery/custom-service-discovery +- title: Load-Balancing + items: + - title: Round Robin + page: load-balancer/round-robin + - title: Random + page: load-balancer/random + - title: Least Requests + page: load-balancer/least-requests + - title: Response Time + page: load-balancer/response-time + - title: Power Of Two Choices + page: load-balancer/power-of-two-choices + - title: Sticky + page: load-balancer/sticky + - title: Custom Load Balancer + page: load-balancer/custom-load-balancer diff --git a/content/docs/smallrye-stork/1/concepts.adoc b/content/docs/smallrye-stork/1/concepts.adoc new file mode 100644 index 000000000..2df7639d2 --- /dev/null +++ b/content/docs/smallrye-stork/1/concepts.adoc @@ -0,0 +1,108 @@ += Concepts +include::_attributes.adoc[] + +This page presents the concepts used in Stork. +When using Stork in a _managed_ environment, such as Quarkus, all these concepts are hidden, as you only configure the lookup and selection. +However, when using the programmatic API, you will use these concepts directly. + +image::images/diagrams/plantuml-383c9b353dda.svg[stork] + +== Process overview + +When using the programmatic API of Stork, you can: + +[arabic] +. Retrieve the _singleton_ `Stork` instance. This instance is configured with the set of `Service` it manages. +. Retrieve the `Service` you want to use. Each `Service` is associated with a name. +. Retrieve the `ServiceInstance` which will provide the metadata to access the actual service. + +image::images/diagrams/plantuml-a0003ba546d7.svg[sequence] + +Behind the scenes, Stork will handle the service lookup and selection. + +[NOTE] +==== + +The service lookup and selection are asynchronous operations. +Thus, the API returns instances of `Uni`. + +==== + +== Stork + +`io.smallrye.stork.Stork` is the entry-point of the API. +The `Stork` instance is a _singleton_. +It needs to be initialized once (when the application starts) and shutdown when the application stops: + +[source,java] +---- +include::{examples-dir}/examples/StorkEntryPointExample.java[] +---- + +During the initialization, Stork looks for `io.smallrye.stork.config.ConfigProvider` SPI provider and retrieves the list of managed services: + +* A service is identified by a *name*. +* A service has a service discovery configuration indicating how Stork will look for service instances +* A service can have a load-balancer configuration indicating how Stork can select the most appropriate instance. + +== Service + +A `io.smallrye.stork.Service` is the structure representing a _service_ used by the application. +Services are pre-configured with their name, service discovery, and optionally, their load-balancer. +You retrieve a `Service` using the `Stork#getService(String name)` method. + +[source,java] +---- +include::{examples-dir}/examples/StorkServiceExample.java[] +---- + +The `Service` lets you retrieve the list of `ServiceInstance`, or select a single one, when a load-balancer is configured. + +== Service Instance + +The `io.smallrye.stork.api.ServiceInstance` represents an actual instance of the service. +It provides the metadata to configure a _client_ to interact with that specific instance of service. + +[source,java] +---- +include::{examples-dir}/examples/StorkServiceLookupExample.java[] +---- + +The service selection is a two-steps process: + +[arabic] +. Service lookup - using the service discovery +. Service selection - using the load balancer + +[source,java] +---- +include::{examples-dir}/examples/StorkServiceSelectionExample.java[] +---- + +== Service Discovery + +The `io.smallrye.stork.api.ServiceDiscovery` represents a service discovery mechanism, such as DNS, Consul, or Eureka. + +You can implement a custom service discovery for Stork by implementing the `ServiceDiscoveryProvider` interface. +The corresponding `ServiceRegistrarProviderLoader` and `RegistrarConfiguration` classes will be automatically generated during compilation time. + +Please note that the `ServiceDiscovery` implementation must be non-blocking. + +== Load Balancer + +The `io.smallrye.stork.api.LoadBalancer` represents a load-balancer strategy, such as round-robin. + +To implement a custom load balancer for Stork, implement the `LoadBalancerProvider` interface. +The corresponding `LoadBalancerProviderLoader` and `Configuration` classes will be automatically generated during compilation time. + +Please note that the `LoadBalancer` implementation, similarly to `ServiceDiscovery` +must be non-blocking. + +== Service registration + +The `io.smallrye.stork.api.ServiceRegistrar` represents a service registration mechanism for Consul and Eureka. + +You can implement a custom service registrar for Stork by implementing the `ServiceRegistrarProvider` interface. +The corresponding `ServiceRegistrarProviderLoader` and `RegistrarConfiguration` classes will be automatically generated during compilation time. + +Please note that the `ServiceRegistrar` implementation must be non-blocking. diff --git a/content/docs/smallrye-stork/1/images/diagrams/plantuml-22429aea1395.svg b/content/docs/smallrye-stork/1/images/diagrams/plantuml-22429aea1395.svg new file mode 100644 index 000000000..bd06e097e --- /dev/null +++ b/content/docs/smallrye-stork/1/images/diagrams/plantuml-22429aea1395.svg @@ -0,0 +1 @@ +ApplicationStorkDNS ServerApplicationStorkDNS ServerApplicationStorkDNS Serverget service instancesget SRV records for hostnamelist of SRV recordsget A/AAAA records for each SRV record targetlist of A/AAAAlist of ServiceInstances \ No newline at end of file diff --git a/content/docs/smallrye-stork/1/images/diagrams/plantuml-383c9b353dda.svg b/content/docs/smallrye-stork/1/images/diagrams/plantuml-383c9b353dda.svg new file mode 100644 index 000000000..0e4b865f3 --- /dev/null +++ b/content/docs/smallrye-stork/1/images/diagrams/plantuml-383c9b353dda.svg @@ -0,0 +1 @@ +«entry-point»StorkServiceDiscoveryServiceInstanceLoadBalancer«named»ServiceKubernetesConsulCustom Service Discovery«default»RoundRobinLeastResponseTimeCustom Load Balancer*selectServiceInstance \ No newline at end of file diff --git a/content/docs/smallrye-stork/1/images/diagrams/plantuml-a0003ba546d7.svg b/content/docs/smallrye-stork/1/images/diagrams/plantuml-a0003ba546d7.svg new file mode 100644 index 000000000..9c1a99915 --- /dev/null +++ b/content/docs/smallrye-stork/1/images/diagrams/plantuml-a0003ba546d7.svg @@ -0,0 +1 @@ +ApplicationApplicationApplicationStorkStorkStorkConfigProviderConfigProviderConfigProviderServiceServiceServiceService DiscoveryService DiscoveryService DiscoveryLoad BalancerLoad BalancerLoad BalancerApplicationStorkConfigProviderServiceService DiscoveryLoad BalancerApplicationStorkConfigProviderServiceService DiscoveryLoad Balancer(1)initialize()(2)getConfig()(3): List<ServiceConfig> (4)getService("name")(5)Service  (6)selectServiceInstance()(7)getServiceInstances()(8)Uni<List<ServiceInstance>>(9)selectServiceInstance()(10)Uni<ServiceInstance>(11)Uni<ServiceInstance> \ No newline at end of file diff --git a/content/docs/smallrye-stork/1/images/problem-dark.png b/content/docs/smallrye-stork/1/images/problem-dark.png new file mode 100644 index 000000000..4c601386a Binary files /dev/null and b/content/docs/smallrye-stork/1/images/problem-dark.png differ diff --git a/content/docs/smallrye-stork/1/images/problem-light.png b/content/docs/smallrye-stork/1/images/problem-light.png new file mode 100644 index 000000000..cc0cff154 Binary files /dev/null and b/content/docs/smallrye-stork/1/images/problem-light.png differ diff --git a/content/docs/smallrye-stork/1/images/redhat_reversed.svg b/content/docs/smallrye-stork/1/images/redhat_reversed.svg new file mode 100644 index 000000000..37fe83c48 --- /dev/null +++ b/content/docs/smallrye-stork/1/images/redhat_reversed.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/content/docs/smallrye-stork/1/images/solution-dark.png b/content/docs/smallrye-stork/1/images/solution-dark.png new file mode 100644 index 000000000..800d0f978 Binary files /dev/null and b/content/docs/smallrye-stork/1/images/solution-dark.png differ diff --git a/content/docs/smallrye-stork/1/images/solution-light.png b/content/docs/smallrye-stork/1/images/solution-light.png new file mode 100644 index 000000000..044bd3786 Binary files /dev/null and b/content/docs/smallrye-stork/1/images/solution-light.png differ diff --git a/content/docs/smallrye-stork/1/images/stork-white.png b/content/docs/smallrye-stork/1/images/stork-white.png new file mode 100644 index 000000000..8ceae5659 Binary files /dev/null and b/content/docs/smallrye-stork/1/images/stork-white.png differ diff --git a/content/docs/smallrye-stork/1/index.adoc b/content/docs/smallrye-stork/1/index.adoc new file mode 100644 index 000000000..094027edf --- /dev/null +++ b/content/docs/smallrye-stork/1/index.adoc @@ -0,0 +1,45 @@ += Overview +include::_attributes.adoc[] + +SmallRye Stork `{project-version}` is a service discovery and client-side load-balancing framework. + +The essence of distributed systems resides in the interaction between _services_. +In modern architecture, you often have multiple instances of your service to share the load or improve the resilience by redundancy. +But, how do you select the _best_ instance of your service? +That's where Stork helps. +Stork is going to select the most appropriate instance. +It offers: + +* Extensible service discovery mechanisms +* Built-in support for Consul and Kubernetes +* Customizable client load-balancing strategies +* A programmatic API and a _managed_ approach +* A Quarkus integration, but Stork can be used in any environment + +== The problem + +In distributed systems, applications typically need to call one another. +In a monolithic application, components invoke one another through language-level method or procedure calls. +In a traditional distributed system deployment, services run at fixed, well-known locations (schemes, hosts, and ports) and can call one another using HTTP/REST or some RPC mechanism. The service locations are often hardcoded in the application configuration. +Nevertheless, a modern distributed system or microservice-based application typically runs in virtualized or containerized environments where the number of instances of a service and their locations change dynamically. +IPs get randomly assigned, and instances can be created or destroyed at any time. +With such dynamics, hard-coded locations are a dead-end. + +.the problem +image::images/problem-light.png[the problem] + +== The solution + +Stork handles the service lookup and selection. +It proposes an extensible set of service discovery mechanisms and load-balancing strategies. + +.the solution +image::images/solution-light.png[the solution] + +== What if the infrastructure provides such a feature? + +Some infrastructure, such as Kubernetes, provides service discoveries and load-balancing features. +However, these mechanisms often lack flexibility. You cannot influence the service instance selection, and the load-balancing strategy is generally a simple _round-robin_. + +Stork provides more flexibility in the service instance selection. For example, it can select the fastest instance (based on the previous calls) to improve the response time. +However, if you don't need that flexibility, just use the infrastructure layer. diff --git a/content/docs/smallrye-stork/1/load-balancer/custom-load-balancer.adoc b/content/docs/smallrye-stork/1/load-balancer/custom-load-balancer.adoc new file mode 100644 index 000000000..5fc9a5d07 --- /dev/null +++ b/content/docs/smallrye-stork/1/load-balancer/custom-load-balancer.adoc @@ -0,0 +1,144 @@ += Implement your own load balancer mechanism +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Stork is extensible, and you can implement your service selection (load-balancer) mechanism. + +== Dependencies + +To implement your _Load Balancer Provider_, make sure your project depends on Core and Configuration Generator. +The former brings classes necessary to implement custom load balancer, the latter contains an annotation processor that generates classes needed by Stork. + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-core + {project-version} + + + io.smallrye.stork + stork-configuration-generator + provided + + {project-version} + +---- + +== Implementing a load balancer provider + +Load balancer implementation consists of three elements: + +* `LoadBalancer` which is responsible for selecting service instances for a single Stork service, +* `LoadBalancerProvider` which creates instances of `LoadBalancer` for a given load balancer _type_, +* `$typeConfiguration` which is a configuration for the load balancer. This class is automatically generated. + +A _type_, for example `acme-load-balancer`, identifies each provider. +This _type_ is used in the configuration to reference the provider: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.load-balancer.type=acme-load-balancer +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.load-balancer.type=acme-load-balancer +---- + +-- + +==== + +A `LoadBalancerProvider` implementation needs to be annotated with `@LoadBalancerType` that defines the _type_. +Any configuration properties that the provider expects should be defined with `@LoadBalancerAttribute` annotations placed on the provider. + +A load balancer provider class should look as follows: + +[source,java] +---- +include::{examples-dir}/examples/AcmeLoadBalancerProvider.java[] +---- + +Note, that the `LoadBalancerProvider` interface takes a configuration class as a parameter. +This configuration class is generated automatically by the _Configuration Generator_. +Its name is created by appending `Configuration` to the load balancer type, like `AcmeLoadBalancerConfiguration`. + +The next step is to implement the `LoadBalancer` interface. + +The essence of load balancers' work happens in the `selectServiceInstance` method. The method returns a single `ServiceInstance` from a collection. + +[source,java] +---- +include::{examples-dir}/examples/AcmeLoadBalancer.java[] +---- + +This implementation is simplistic and just picks a random instance from the received list. + +Some load balancers make the pick based on statistics such as calls in progress or response times, or amount of errors of a service instance. +To collect this information in your load balancer, you can wrap the selected service instance into `ServiceInstanceWithStatGathering`. + +Load balancers based on statistics often expect that an operation using a selected service instance is marked as started before the next selection. +By default, Stork assumes that a `LoadBalancer` requires this and guards the calls accordingly. +If this is not the case for your implementation, override the `requiresStrictRecording()` method to return `false`. + +== Using your load balancer + +In the project using it, don't forget to add the dependency on the module providing your implementation. +Then, in the configuration, just add: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-discovery.type=... +stork.my-service.load-balancer.type=acme-load-balancer\ +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.type=... +quarkus.stork.my-service.load-balancer.type=acme-load-balancer +---- + +-- + +==== + +Then, Stork will use your implementation to select the `my-service` service instance. + +== Using your load balancer using the programmatic API + +When building your load balancer project, the configuration generator creates a configuration class. +This class can be used to configure your load balancer using the Stork programmatic API. + +[source,java] +---- +include::{examples-dir}/examples/AcmeSelectorApiUsage.java[] +---- + +Remember that attributes, like `my-attribute`, are declared using the `@LoadBalancerAttribute` annotation on the `LoadBalancerProvider` implementation. diff --git a/content/docs/smallrye-stork/1/load-balancer/least-requests.adoc b/content/docs/smallrye-stork/1/load-balancer/least-requests.adoc new file mode 100644 index 000000000..409811e9b --- /dev/null +++ b/content/docs/smallrye-stork/1/load-balancer/least-requests.adoc @@ -0,0 +1,59 @@ += Least Requests Load Balancing +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The `least-requests` load balancing strategy monitors the number of inflight calls and selects the less-used instance. + +This strategy keeps track of the inflight calls made by the application and picks the service instance with the smallest number of inflight requests: + +[arabic] +. when the selection happens, the service instance with the smallest number of inflight requests is selected, and this number is incremented +. when the operation completes, successfully or not, the number of inflight requests is decremented + +== Dependency + +First, you need to add the `least-requests` load-balancer to your project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-load-balancer-least-requests + {project-version} + +---- + +== Configuration + +For each service expected to use a least-response-time selection, configure the `load-balancer` to be `least-requests`: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-discovery.type=... +stork.my-service.service-discovery...=... +stork.my-service.load-balancer.type=least-requests +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.type=... +quarkus.stork.my-service.service-discovery...=... +quarkus.stork.my-service.load-balancer.type=least-requests +---- + +-- + +==== diff --git a/content/docs/smallrye-stork/1/load-balancer/power-of-two-choices.adoc b/content/docs/smallrye-stork/1/load-balancer/power-of-two-choices.adoc new file mode 100644 index 000000000..8eaa129ff --- /dev/null +++ b/content/docs/smallrye-stork/1/load-balancer/power-of-two-choices.adoc @@ -0,0 +1,67 @@ += Power Of Two Choices Load Balancing +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The `power-of-two-choices` load balancing selects two random service instances and then chooses the one with the least inflight requests. +It avoids the overhead of `least-requests` and the worst case for `random` where it selects a busy destination. + +This strategy acts as follows: + +[arabic] +. when the selection happens, it picks two random instances from the list, +. it returns the least loaded instance (based on the number of inflight requests), +. when the operation completes, successfully or not, the number of inflight requests for the instance is decremented. + +Check http://www.eecs.harvard.edu/~michaelm/NEWWORK/postscripts/twosurvey.pdf[The Power of Two Random Choices] paper to learn more about this pattern and the benefits. + +== Dependency + +First, you need to add the random load-balancer to your project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-load-balancer-power-of-two-choices + {project-version} + +---- + +== Configuration + +For each service expected to use a random service selection, configure the `load-balancer` to be `power-of-two-choices`: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-discovery.type=... +stork.my-service.service-discovery...=... +stork.my-service.load-balancer.type=power-of-two-choices +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.type=... +quarkus.stork.my-service.service-discovery...=... +quarkus.stork.my-service.load-balancer.type=power-of-two-choices +---- + +-- + +==== + +Supported attributes are the following: + +include::{generated-dir}/power-of-two-choices-lb-attributes.adoc[] diff --git a/content/docs/smallrye-stork/1/load-balancer/random.adoc b/content/docs/smallrye-stork/1/load-balancer/random.adoc new file mode 100644 index 000000000..515209c42 --- /dev/null +++ b/content/docs/smallrye-stork/1/load-balancer/random.adoc @@ -0,0 +1,57 @@ += Random Load Balancing +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The `random` load balancing is a straightforward service instance selection solution that picks a random instance every time. + +== Dependency + +First, you need to add the random load-balancer to your project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-load-balancer-random + {project-version} + +---- + +== Configuration + +For each service expected to use a random service selection, configure the `load-balancer` to be `random`: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-discovery.type=... +stork.my-service.service-discovery...=... +stork.my-service.load-balancer.type=random +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.type=... +quarkus.stork.my-service.service-discovery...=... +quarkus.stork.my-service.load-balancer.type=random +---- + +-- + +==== + +Supported attributes are the following: + +include::{generated-dir}/random-lb-attributes.adoc[] diff --git a/content/docs/smallrye-stork/1/load-balancer/response-time.adoc b/content/docs/smallrye-stork/1/load-balancer/response-time.adoc new file mode 100644 index 000000000..2520b7f6f --- /dev/null +++ b/content/docs/smallrye-stork/1/load-balancer/response-time.adoc @@ -0,0 +1,94 @@ += Least Response Time Load Balancing +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The `least-response-time` load balancing strategy collects response times of the calls made with service instances and picks an instance based on this information. + +Erroneous responses are treated as responses with a long response time, by default 60 seconds. This can be controlled with the `error-penalty` attribute. + +The algorithm for service instance selection is as follows: + +* if there is a service instance that wasn't used before - use it, otherwise: +* if there are any service instances with collected response times - select the one for which _score_ is the lowest, otherwise: +* select a random instance + +The _score_ for an instance decreases in time if an instance is not used. This way we ensure that instances that haven't been used in a long time, are retried. + +For the details on the _score_ calculation, see xref:#score-calculation[Score calculation] + +== Dependency + +To use this load balancer, start with adding the least-response-time load-balancer dependency to your project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-load-balancer-least-response-time + {project-version} + +---- + +== Configuration + +For each service expected to use a least-response-time selection, configure the `load-balancer` to be `least-response-time`: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-discovery.type=... +stork.my-service.service-discovery...=... +stork.my-service.load-balancer.type=least-response-time +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.type=... +quarkus.stork.my-service.service-discovery...=... +quarkus.stork.my-service.load-balancer.type=least-response-time +---- + +-- + +==== + +The following attributes are supported: + +include::{generated-dir}/least-response-time-lb-attributes.adoc[] + +== Score calculation + +The _score_ of a service instance is calculated by dividing a weighted sum of response times by sum of the weighs. The result is additionally adjusted to account for instances that haven't been used for a long time. + +Let: + +* $n$ denote how many instance selections were made so far +* $t++_++i$ denote the response time for call $i$ +* $n++_++i$ denote the number of instance selections done until the moment of recording the response time for call $i$ +* $n++_{++max}$ denote the number of instance selections at the moment of last call recorded with this instance +* $$ denote a configurable `declining-factor` + +The idea for the weight is to decrease the importance of the data collected long time (many calls) ago. For call $i$, the weight is calculated as follows: +$$ +w++_++i = ^ ++{++(n - n++_++i)} +$$ + +The score of a service instance is calculated as: +$$ +score(n) = ^++{++n - n++_{++max}} ++*++ = +^++{++n - n++_{++max}} ++*++ +$$ + +The `declining-factor` should be in $(0, 1++]++$ , the default is $0.9$. Using a lower value makes the older response times less important. diff --git a/content/docs/smallrye-stork/1/load-balancer/round-robin.adoc b/content/docs/smallrye-stork/1/load-balancer/round-robin.adoc new file mode 100644 index 000000000..ba7529ea4 --- /dev/null +++ b/content/docs/smallrye-stork/1/load-balancer/round-robin.adoc @@ -0,0 +1,47 @@ += Round-Robin Load Balancing +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The round-robin is a straightforward load-balancing solution that just iterates over the set of service instances. +While being simple, this solution shares the load among the instances and may be sufficient in many cases. + +The round-robin strategy is the default load-balancing strategy. +It is provided by Stork itself, and so does not require an additional dependency. + +== Configuration + +There is no need to configure the load-balancing strategy to be `round-robin`. +Stork automatically uses this strategy when none are configured. + +However, you can also configure it explicitly as follows: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-discovery.type=... +stork.my-service.service-discovery...=... +stork.my-service.load-balancer.type=round-robin +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.type=... +quarkus.stork.my-service.service-discovery...=... +quarkus.stork.my-service.load-balancer.type=round-robin +---- + +-- + +==== diff --git a/content/docs/smallrye-stork/1/load-balancer/sticky.adoc b/content/docs/smallrye-stork/1/load-balancer/sticky.adoc new file mode 100644 index 000000000..07432d717 --- /dev/null +++ b/content/docs/smallrye-stork/1/load-balancer/sticky.adoc @@ -0,0 +1,56 @@ += Sticky Load Balancing +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The sticky load balancer selects a single service instance and keeps using it until it fails. +Then, it selects another one. + +It is possible to configure a backoff time to specify for how long a failing service instance should not be retried. + +Precisely, the load balancer works as follows: + +* if no service instance has been selected so far, select the first instance from the collection; +* else if the previously selected service instance has not failed, and is still available, return it; +* else return the first available service instance that has no recorded failure, if one exists; +* else, find the available instance for which the time since the last failure is the longest, and +** if the backoff time since the failure passed, return it; +** or, throw an `NoAcceptableServiceInstanceFoundException` as no acceptable instances are available. + +== Configuration + +To use the `sticky` load service selection strategy, set the load balancer type to `sticky`: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-discovery.type=... +stork.my-service.service-discovery...=... +stork.my-service.load-balancer.type=sticky +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.type=... +quarkus.stork.my-service.service-discovery...=... +quarkus.stork.my-service.load-balancer.type=sticky +---- + +-- + +==== + +The following attributes are supported: + +NOTE: This table is generated from the project's build and is not available for this version. diff --git a/content/docs/smallrye-stork/1/programmatic-api.adoc b/content/docs/smallrye-stork/1/programmatic-api.adoc new file mode 100644 index 000000000..788f39819 --- /dev/null +++ b/content/docs/smallrye-stork/1/programmatic-api.adoc @@ -0,0 +1,54 @@ += Stork Programmatic API +include::_attributes.adoc[] + +Stork proposes a programmatic API that lets you register new service Definitions and do manual lookup and selection. +When using the programmatic API of Stork, you can: +Retrieve the singleton Stork instance. This instance is configured with the set of Services it manages. +Register new service definition. +Retrieve the Service you want to use. Each Service is associated with a name. +Retrieve the ServiceInstance, which will provide the metadata to access the actual instance. + +== Initializing Stork + +If your framework does not already provide a configured `Stork` instance, you need to do: + +[source,java] +---- +include::{examples-dir}/examples/InitializationExample.java[] +---- + +== Adding service dynamically + +To register a new `ServiceDefinition`, use the `defineIfAbsent` method: + +[source,java] +---- +include::{examples-dir}/examples/DefinitionExample.java[] +---- + +The `ServiceDefinition` instances can be created from: + +* A service discovery configuration - these classes are provided by the service discovery implementations, +* An optional load balancer configuration - these classes are provided by the load balancer implementations + +Attributes from the service discovery and load balancer can be configured from the `Configuration` classes. + +== Looking for service instances + +To list the service instances for a given service, or to select an instance according to the load balancer strategy, use the following code: + +[source,java] +---- +include::{examples-dir}/examples/LookupExample.java[] +---- + +The lookup and selection methods are returning Uni as these processes are asynchronous. + +== All in one example + +The following snippet provides an _all in one_ example of the Stork programmatic API: + +[source,java] +---- +include::{examples-dir}/examples/StorkApiExample.java[] +---- diff --git a/content/docs/smallrye-stork/1/quarkus.adoc b/content/docs/smallrye-stork/1/quarkus.adoc new file mode 100644 index 000000000..c17303c16 --- /dev/null +++ b/content/docs/smallrye-stork/1/quarkus.adoc @@ -0,0 +1,79 @@ += Using Stork with Quarkus +include::_attributes.adoc[] + +https://quarkus.io[Quarkus] is a Kubernetes Native Java stack tailored for OpenJDK HotSpot and GraalVM. + +Quarkus REST Client Reactive and gRPC extensions come with built-in integration with SmallRye Stork. + +This page describes how to use REST Client Reactive with Stork. Using gRPC with Stork is similar. + +We will use the Consul service discovery and the round-robin load balancer as examples. + +== The project + +You can create a Quarkus project with the REST Client Reactive extension using https://code.quarkus.io[code.quarkus.io]. + +The corresponding https://quarkus.io/guides/rest-client-reactive[Quarkus guide] describes the extension in more detail. + +== The client + +To use the REST client to communicate with a remote endpoint, you need to create an interface that describes how the communication should work. +The client requires `baseUri` (or `baseUrl`) pointing to the address of the remote endpoint. + +To use Stork to determine the actual address, set the scheme of the URI to `stork` and the hostname of the URI to the name of the Stork service. + +For example, the `HelloClient` below will use the Stork service called `hello-service` to determine the address of the destination, and `/hello` as the base path for queries: + +[source,java] +---- +include::{examples-dir}/examples/HelloClient.java[] +---- + +== The service + +In Stork, a `Service` consists of service discovery and a load balancer. +The Service discovery is responsible for determining the `ServiceInstance`s, that is, available addresses for a service. +The load balancer picks a single `ServiceInstance` for a call. + +=== Dependencies + +To use the service discovery and the load balancer of your choosing, you need to add the appropriate dependencies to your application. +For example, if you wish to use Consul and load-balance the calls with round-robin, add the following to your `pom.xml`: + +[source,xml] +---- + + io.smallrye.stork + stork-service-discovery-consul + +---- + +[NOTE] +==== + +No need to add a dependency for the round-robin load-balancer. This strategy is built-in. +Also, no need to specify the stork version, Quarkus imports the Stork _BOM_. + +==== + +When using Eureka, Kubernetes, or any other service discovery mechanism, or a different load balancer, replace the dependencies above with the ones you need. +Based on the defined dependencies, Stork automatically registers providers for service discovery mechanisms and load balancers. + +=== The config + +The last piece of the puzzle is the actual service configuration. +If your Consul instance is running on `localhost` on port `8500`, service discovery configuration should look as follows: + +[source,properties] +---- +quarkus.stork.hello-service.service-discovery.type=consul +quarkus.stork.hello-service.service-discovery.consul-host=localhost +quarkus.stork.hello-service.service-discovery.consul-port=8500 +---- + +[NOTE] +==== + +When no load-balancing is configured, Stork uses a round-robin. + +==== diff --git a/content/docs/smallrye-stork/1/service-discovery/composite.adoc b/content/docs/smallrye-stork/1/service-discovery/composite.adoc new file mode 100644 index 000000000..96b805f9d --- /dev/null +++ b/content/docs/smallrye-stork/1/service-discovery/composite.adoc @@ -0,0 +1,65 @@ += Composite Service Discovery +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Some organizations use hybrid infrastructure. In such an infrastructure, different service instances may be discoverable via different service discovery providers. E.g. when _serviceA_ is being migrated from a VM to Kubernetes, its older versions can be discovered via Consul and newer via Kubernetes. + +`composite` service discovery addresses this problem by letting you define a service that consists of multiple services. + +== Dependency + +To use the `composite` service discovery, first add the appropriate Service Discovery provider dependency to your project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-service-discovery-composite + {project-version} + +---- + +== Configuration + +For each service that should consist of multiple services, configure the service discovery `type`, and set the `services` property to a comma separated list of services: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-discovery.type=composite +stork.my-service.service-discovery.services=serviceA,serviceB + +stork.serviceA.service-discovery.type=... +stork.serviceB.service-discovery.type=... +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.type=composite +quarkus.stork.my-service.service-discovery.services=serviceA,serviceB + +quarkus.stork.serviceA.service-discovery.type=... +quarkus.stork.serviceB.service-discovery.type=... +---- + +-- + +==== + +Remember to define the services that make up your composite service. + +These are all the parameters of the composite service discovery: + +include::{generated-dir}/composite-sd-attributes.adoc[] diff --git a/content/docs/smallrye-stork/1/service-discovery/consul.adoc b/content/docs/smallrye-stork/1/service-discovery/consul.adoc new file mode 100644 index 000000000..2dca70310 --- /dev/null +++ b/content/docs/smallrye-stork/1/service-discovery/consul.adoc @@ -0,0 +1,102 @@ += Consul Service Discovery +:version-root: ../ +include::{version-root}_attributes.adoc[] + +https://www.consul.io/[Consul] is a distributed, highly available, and data center aware solution to connect and configure applications across dynamic, distributed infrastructure. +It's often used as service discovery backend to register and locate the services composing your system. +Consul makes it simple for services to register themselves and to discover other services via a DNS or HTTP interface. +External services can be registered as well. + +This page explains how Stork can use Consul to handle the service discovery and service registration. + +== Dependency + +First, you need to add the Stork Consul Service Discovery provider: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-service-discovery-consul + {project-version} + +---- + +=== Service discovery configuration + +For each service that should get the service instances from Consul, configure the service discovery `type`: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-discovery.type=consul +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.type=consul +---- + +-- + +==== + +Consul service discovery is configured with the following parameters: + +include::{generated-dir}/consul-sd-attributes.adoc[] + +== Service registration + +Stork also provides the ability to register services using Consul as backend. + +=== Service registration configuration + +For each service that should register the service instances in Consul, configure the service discovery `type`: + +[.tabs] +==== + +stork standalone:: ++ +-- + +-- + +==== + +[source,properties] +---- +stork.my-service.service-registrar.type=consul +---- + +[.tabs] +==== + +stork in quarkus:: ++ +-- + +-- + +==== + +[source,properties] +---- +quarkus.stork.my-service.service-registrar.type=consul +---- + +Consul service registrar is configured with the following parameters: + +include::{generated-dir}/consul-sr-attributes.adoc[] diff --git a/content/docs/smallrye-stork/1/service-discovery/custom-service-discovery.adoc b/content/docs/smallrye-stork/1/service-discovery/custom-service-discovery.adoc new file mode 100644 index 000000000..3fa9511d0 --- /dev/null +++ b/content/docs/smallrye-stork/1/service-discovery/custom-service-discovery.adoc @@ -0,0 +1,194 @@ += Implement your own service discovery mechanism +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Stork is extensible, and you can implement your own service discovery mechanism. + +== Dependencies + +To implement your _Service Discovery Provider_, make sure your project depends on Core and Configuration Generator. +The former brings classes necessary to implement custom discovery, the latter contains an https://docs.oracle.com/en/java/javase/11/docs/api/java.compiler/javax/annotation/processing/Processor.html[annotation processor] that generates classes needed by Stork. + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-core + {project-version} + + + io.smallrye.stork + stork-configuration-generator + provided + + {project-version} + +---- + +== Implementing a service discovery provider + +Service discovery implementation consists of three elements: + +* `ServiceDiscovery` which is responsible for locating service instances for a single Stork service. +* `ServiceDiscoveryProvider` which creates instances of `ServiceDiscovery` for a given service discovery _type_. +* `$typeConfiguration` which is a configuration for the discovery. This class is automatically generated during the compilation (using an annotation processor). + +A _type_, for example, `acme`, identifies each provider. +This _type_ is used in the configuration to reference the provider: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-discovery.type=acme +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.type=acme +---- + +-- + +==== + +A `ServiceDiscoveryProvider` implementation needs to be annotated with `@ServiceDiscoveryType` that defines the _type_. +Any configuration properties that the provider expects should be defined with `@ServiceDiscoveryAttribute` annotations placed on the provider. + +A service discovery provider class should look as follows: + +[source,java] +---- +include::{examples-dir}/examples/AcmeServiceDiscoveryProvider.java[] +---- + +Note, that the `ServiceDiscoveryProvider` interface takes a configuration class as a parameter. This configuration class +is generated automatically by the _Configuration Generator_. +Its name is created by appending `Configuration` to the service discovery type, such as `AcmeConfiguration`. + +The next step is to implement the `ServiceDiscovery` interface: + +[source,java] +---- +include::{examples-dir}/examples/AcmeServiceDiscovery.java[] +---- + +This implementation is simplistic. +Typically, instead of creating a service instance with values from the configuration, you would connect to a service discovery backend, look for the service and build the list of service instance accordingly. +That's why the method returns a `Uni`. +Most of the time, the lookup is a remote operation. + +As you can see, the `AcmeConfiguration` class gives access to the configuration attribute. + +== Using your service discovery + +In the project using it, don't forget to add the dependency on the module providing your implementation. +Then, in the configuration, just add: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-discovery.type=acme +stork.my-service.service-discovery.host=localhost +stork.my-service.service-discovery.port=1234 +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.type=acme +quarkus.stork.my-service.service-discovery.host=localhost +quarkus.stork.my-service.service-discovery.port=1234 +---- + +-- + +==== + +Then, Stork will use your implementation to locate the `my-service` service. + +== Using your service discovery using the programmatic API + +When building your service discovery project, the configuration generator creates a configuration class. +This class can be used to configure your service discovery using the Stork programmatic API. + +[source,java] +---- +include::{examples-dir}/examples/AcmeDiscoveryApiUsage.java[] +---- + +Remember that attributes, like `host`, are declared using the `@ServiceDiscoveryAttribute` annotation on the `ServiceDiscoveryProvider` implementation. + +== Caching the service instances + +Your `ServiceDiscovery` implementation can extend `io.smallrye.stork.impl.CachingServiceDiscovery` to automatically _cache_ the service instances. +In this case, the retrieved set of `ServiceInstance` is cached and only updated after some time. +This duration is an additional configuration attribute. +For homogeneity, we recommend the following attribute: + +[source,java] +---- +@ServiceDiscoveryAttribute(name = "refresh-period", description = "Service discovery cache refresh period.", + defaultValue = CachingServiceDiscovery.DEFAULT_REFRESH_INTERVAL) +---- + +The following snippet extends the _acme_ service discovery with the `refresh-period` attribute: + +[source,java] +---- +include::{examples-dir}/examples/CachedAcmeServiceDiscoveryProvider.java[] +---- + +Extending `io.smallrye.stork.impl.CachingServiceDiscovery` changes the structure of the service discovery implementation: + +[source,java] +---- +include::{examples-dir}/examples/CachedAcmeServiceDiscovery.java[] +---- + +[arabic] +. Call the `super` constructor with the `refresh-period` value +. Implement `fetchNewServiceInstances` instead of `getServiceInstances`. +The method is called periodically, and the retrieved instances are cached. +This implementation is simplistic. + +If the retrieval fails, the error is reported, and Stork keeps the previously retrieved list of instances. + +=== Customizing the caching strategy + +Sometimes it can be useful to change this behaviour and customize the cache expiration strategy. + +For example, imagine you are using a backend service discovery where service instances can change very frequently. + +Moreover, contacting the backend service discovery can be expensive in terms of computing, +thus finding a good value for the refreshing time can be mission impossible. + +For these situations, Stork allows to implement a better expiration strategy for the cache. + +If you want to customize the expiration strategy, you need: +1. Implement the `cache` method where the expiration strategy should be defined. +2. Invalidate the cache when expiration condition evaluates to true. + +Take a look to the xref:service-discovery/kubernetes.adoc#caching-the-service-instances[Kubernetes Service Discovery] for further details about this feature. diff --git a/content/docs/smallrye-stork/1/service-discovery/dns.adoc b/content/docs/smallrye-stork/1/service-discovery/dns.adoc new file mode 100644 index 000000000..b8066d5ea --- /dev/null +++ b/content/docs/smallrye-stork/1/service-discovery/dns.adoc @@ -0,0 +1,77 @@ += DNS Service Discovery +:version-root: ../ +include::{version-root}_attributes.adoc[] + +DNS is a name resolution protocol used to determine IP addresses for hostnames. +That makes it a natural fit for service discovery. +Consul and AWS Cloud Map provide DNS resolutions for service discovery. + +This page explains how Stork can use DNS to handle the service discovery. + +== DNS records + +DNS supports a https://en.wikipedia.org/wiki/List_of_DNS_record_types[variety of record types]. Stork can resolve hostnames to addresses based on https://en.wikipedia.org/wiki/SRV_record[_SRV_], A and AAAA records. +All these types of records may return multiple addresses for a single hostname. + +While _A_ and _AAAA_ records are quite similar, they just carry an IP (_v4_ for _A_ and _v6_ for _AAAA_), the _SRV_ records are different. +They contain a _weight_, a _target_ and a _port_ for a service instance. +The _target_ returned in an _SRV_ record needs to be resolved further by an _A_ or an _AAAA_ record. + +In short, it works as follows: + +image::images/diagrams/plantuml-22429aea1395.svg[srv_sequence] + +== Dependency + +To use the DNS service discovery, you need to add the Stork DNS Service Discovery provider dependency to your project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-service-discovery-dns + {project-version} + +---- + +== Configuration + +Next, set the service discovery `type` to `dns`. +Additionally, you would usually specify the DNS server, or servers, to use for the discovery. +All in all, your configuration could look as follows: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-discovery.type=dns +# optional dns servers: +stork.my-service.service-discovery.dns-servers=my-dns-server:8221,my-dns-server2 +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.type=dns + +# optional dns servers: +quarkus.stork.my-service.service-discovery.dns-servers=my-dns-server:8221,my-dns-server2 +---- + +-- + +==== + +All the available parameters are as follows: + +NOTE: This table is generated from the project's build and is not available for this version. diff --git a/content/docs/smallrye-stork/1/service-discovery/eureka.adoc b/content/docs/smallrye-stork/1/service-discovery/eureka.adoc new file mode 100644 index 000000000..00ebd7457 --- /dev/null +++ b/content/docs/smallrye-stork/1/service-discovery/eureka.adoc @@ -0,0 +1,118 @@ += Eureka Service Discovery +:version-root: ../ +include::{version-root}_attributes.adoc[] + +https://github.com/Netflix/eureka[Eureka] is a RESTful service that is primarily used in the AWS cloud for the purpose of discovery, load balancing, and failover of middle-tier servers. + +This page explains how Stork can use Eureka to handle the service discovery and service registration. + +== Dependency + +First, you need to add the Stork Eureka Service Discovery provider: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-service-discovery-eureka + {project-version} + +---- + +=== Service discovery configuration + +For each application instance expected to be registered in Eureka, configure the lookup: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-discovery.type=eureka +stork.my-service.service-discovery.eureka-host=localhost +stork.my-service.service-discovery.eureka-port=8761 +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.type=eureka +quarkus.stork.my-service.service-discovery.eureka-host=localhost +quarkus.stork.my-service.service-discovery.eureka-port=8761 +---- + +-- + +==== + +Stork looks for the service with the given name (`my-service` in the previous example). + +Supported attributes are the following: + +include::{generated-dir}/eureka-sd-attributes.adoc[] + +The `application` attribute is optional. +It uses the Stork service name (`my-service` in the previous configuration) if not set. + +The `instance` attribute allows selecting a specific instance. +Using this attribute prevents load-balancing as you will always select a single instance. + +The `secure` attribute indicates if you want the _secure virtual address_ of the application instance. +If set to `true`, unsecured instances are filtered out from the available instances. + +== Service registration + +Stork also provides the ability to register services using Eureka as backend. + +=== Service registration configuration + +For each service that should register the service instances in Eureka, configure the service discovery `type`: + +[.tabs] +==== + +stork standalone:: ++ +-- + +-- + +==== + +[source,properties] +---- +stork.my-service.service-registrar.type=eureka +stork.my-service.service-registrar.eureka-host=localhost +stork.my-service.service-registrar.eureka-port=8761 +---- + +[.tabs] +==== + +stork in quarkus:: ++ +-- + +-- + +==== + +[source,properties] +---- +quarkus.stork.my-service.service-registrar.type=eureka +quarkus.stork.my-service.service-registrar.eureka-host=localhost +quarkus.stork.my-service.service-registrar.eureka-port=8761 +---- + +Consul service registrar is configured with the following parameters: + +include::{generated-dir}/eureka-sr-attributes.adoc[] diff --git a/content/docs/smallrye-stork/1/service-discovery/knative.adoc b/content/docs/smallrye-stork/1/service-discovery/knative.adoc new file mode 100644 index 000000000..71fd20806 --- /dev/null +++ b/content/docs/smallrye-stork/1/service-discovery/knative.adoc @@ -0,0 +1,131 @@ += Knative Service Discovery +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Knative is a Kubernetes-based platform for serverless workloads. Knative provides a set of objects as Kubernetes Custom Resource Definitions (CRDs). +These resources are used to define and control how your serverless workload behaves on the cluster. +The Stork Knative service discovery implementation is very similar to the Kubernetes one. +Stork will ask for https://knative.dev/docs/serving/reference/serving-api/#serving.knative.dev/v1.Service[Knative services] to the cluster instead of vanilla https://kubernetes.io/docs/concepts/services-networking/service/#service-resource[Kubernetes services] used by the Kubernetes implementation. +To do so, Stork uses https://github.com/fabric8io/kubernetes-client/blob/master/extensions/knative/client/src/main/java/io/fabric8/knative/client/KnativeClient.java[Fabric 8 Knative Client] which is just an extension of Fabric8 Kubernetes Client. + +== Dependency + +First, you need to add the Stork Knative Service Discovery provider: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-service-discovery-knative + {project-version} + +---- + +=== A few words about server authentication. + +Stork uses Fabric8 Knative Client which is a https://github.com/fabric8io/kubernetes-client#readme[Fabric8 Kubernetes Client] extension to access the Kubernetes resources, concretely the `DefaultKnativeClient` implementation. + +Since Knative Client is just an extension of Fabric8 Kubernetes Client, it’s also possible to get an instance of KnativeClient from KubernetesClient. + +`DefaultKubernetesClient` will try to read the `~/.kube/config` file from your local machine and load the token for authenticating with the Kubernetes API server. + +The level of access (Roles) depends on the configured `ServiceAccount`. + +You can override this configuration if you want fine-grain control. + +==== Role-based access control (RBAC) + +If you're using a Kubernetes cluster with Role-Based Access Control (RBAC) enabled, the default permissions for a ServiceAccount don't allow it to list or modify any resources. +A `ServiceAccount`, a `Role` and a `RoleBinding` are needed in order to allow Stork to list the available service instances from the cluster or the namespace. + +An example that allows listing all endpoints could look something like this: + +[source,yaml] +---- +------ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: + namespace: +--- +kind: Role +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: + namespace: +rules: + - apiGroups: [""] # "" indicates the core API group + resources: ["endpoints", "pods"] # stork queries service endpoints and pods + verbs: ["get", "list"] +--- +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: RoleBinding +metadata: + name: + namespace: +subjects: + - kind: ServiceAccount + # Reference to upper's `metadata.name` + name: + # Reference to upper's `metadata.namespace` + namespace: +roleRef: + kind: Role + name: + apiGroup: rbac.authorization.k8s.io +---- + +== Configuration + +For each service expected to be exposed as _Kubernetes Service_, configure the lookup: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-knservice.service-discovery.type=knative +stork.my-knservice.service-discovery.knative-namespace=my-namespace +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-knservice.service-discovery.type=knative +quarkus.stork.my-knservice.service-discovery.knative-namespace=my-namespace +---- + +-- + +==== + +Stork looks for the _Knative Service_ with the given name (`my-knservice` in the previous example) in the specified namespace. + +Stork inspects the _Knative Service_ and retrieves the url of the service. + +Supported attributes are the following: + +NOTE: This table is generated from the project's build and is not available for this version. + +== Caching the service instances + +Contacting the cluster too much frequently can result in performance problems. It's why Knative Service discovery extends `io.smallrye.stork.impl.CachingServiceDiscovery` to automatically _cache_ the service instances. +Moreover, the caching expiration has been also improved in order to only update the retrieved set of `ServiceInstance` if some of them changes and an event is emitted. +This is done by creating an https://www.javadoc.io/static/io.fabric8/kubernetes-client-api/6.1.1/io/fabric8/kubernetes/client/informers/SharedIndexInformer.html[Informer], similar to a https://www.javadoc.io/static/io.fabric8/kubernetes-client-api/6.1.1/io/fabric8/kubernetes/client/Watch.html[Watch], able to observe the events on the Knative Service instances resources. + +include::{examples-dir}/../src/main/java/io/smallrye/stork/servicediscovery/knative/KnativeServiceDiscovery.java[opts=optional] + +Note that: +- the cache is invalidated when an event is received. +- the cache is validated once the instances are retrieved from the cluster, in the `fetchNewServiceInstances` method. +- the `cache` method is overrided to customize the expiration strategy. In this case the collection of service instances will be kept until an event occurs. diff --git a/content/docs/smallrye-stork/1/service-discovery/kubernetes.adoc b/content/docs/smallrye-stork/1/service-discovery/kubernetes.adoc new file mode 100644 index 000000000..72317ddd0 --- /dev/null +++ b/content/docs/smallrye-stork/1/service-discovery/kubernetes.adoc @@ -0,0 +1,133 @@ += Kubernetes Service Discovery +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Kubernetes has a built-in support for service discovery and load-balancing. +However, you may need more flexibility to carefully select the service instance you want. + +This page explains how Stork can use the Kubernetes API to handle the service discovery. + +== Dependency + +First, you need to add the Stork Kubernetes Service Discovery provider: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-service-discovery-kubernetes + {project-version} + +---- + +=== A few words about server authentication. + +Stork uses https://github.com/fabric8io/kubernetes-client#readme[Fabric8 Kubernetes Client] to access the Kubernetes resources, concretely the `DefaultKubernetesClient` implementation. + +It will try to read the `~/.kube/config` file from your local machine and load the token for authenticating with the Kubernetes API server. + +If you are using the Stork Kubernetes discovery provider from inside a _Pod_, it loads `~/.kube/config` from the container file system. + +This file is automatically mounted inside the Pod. + +The level of access (Roles) depends on the configured `ServiceAccount`. + +You can override this configuration if you want fine-grain control. + +==== Role-based access control (RBAC) + +If you're using a Kubernetes cluster with Role-Based Access Control (RBAC) enabled, the default permissions for a ServiceAccount don't allow it to list or modify any resources. +A `ServiceAccount`, a `Role` and a `RoleBinding` are needed in order to allow Stork to list the available service instances from the cluster or the namespace. + +An example that allows listing all endpoints could look something like this: + +[source,yaml] +---- +------ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: + namespace: +--- +kind: Role +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: + namespace: +rules: + - apiGroups: [""] # "" indicates the core API group + resources: ["endpoints", "pods"] # stork queries service endpoints and pods + verbs: ["get", "list"] +--- +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: RoleBinding +metadata: + name: + namespace: +subjects: + - kind: ServiceAccount + # Reference to upper's `metadata.name` + name: + # Reference to upper's `metadata.namespace` + namespace: +roleRef: + kind: Role + name: + apiGroup: rbac.authorization.k8s.io +---- + +== Configuration + +For each service expected to be exposed as _Kubernetes Service_, configure the lookup: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-discovery.type=kubernetes +stork.my-service.service-discovery.k8s-namespace=my-namespace +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.type=kubernetes +quarkus.stork.my-service.service-discovery.k8s-namespace=my-namespace +---- + +-- + +==== + +Stork looks for the _Kubernetes Service_ with the given name (`my-service` in the previous example) in the specified namespace. + +Instead of using the _Kubernetes Service_ IP directly, and let Kubernetes handle the selection and balancing, Stork inspects the service and retrieves the list of _pods_ providing the service. +Then, it can select the instance. + +Supported attributes are the following: + +include::{generated-dir}/kubernetes-sd-attributes.adoc[] + +== Caching the service instances + +Contacting the cluster too much frequently can result in performance problems. It's why Kubernetes Service discovery extends `io.smallrye.stork.impl.CachingServiceDiscovery` to automatically _cache_ the service instances. +Moreover, the caching expiration has been also improved in order to only update the retrieved set of `ServiceInstance` if some of them changes and an event is emitted. +This is done by creating an https://www.javadoc.io/static/io.fabric8/kubernetes-client-api/6.1.1/io/fabric8/kubernetes/client/informers/SharedIndexInformer.html[Informer], similar to a https://www.javadoc.io/static/io.fabric8/kubernetes-client-api/6.1.1/io/fabric8/kubernetes/client/Watch.html[Watch], able to observe the events on the service instances resources. + +include::{examples-dir}/../src/main/java/io/smallrye/stork/servicediscovery/kubernetes/KubernetesServiceDiscovery.java[opts=optional] + +Note that: +- the cache is invalidated when an event is received. +- the cache is validated once the instances are retrieved from the cluster, in the `fetchNewServiceInstances` method. +- the `cache` method is overrided to customize the expiration strategy. In this case the collection of service instances will be kept until an event occurs. diff --git a/content/docs/smallrye-stork/1/service-discovery/static-list.adoc b/content/docs/smallrye-stork/1/service-discovery/static-list.adoc new file mode 100644 index 000000000..6954f0756 --- /dev/null +++ b/content/docs/smallrye-stork/1/service-discovery/static-list.adoc @@ -0,0 +1,56 @@ += Static List Service Discovery +:version-root: ../ +include::{version-root}_attributes.adoc[] + +In some situations, such as demos, development, or testing, you may want to mock the service discovery by providing a predefined list of service instances. +For this purpose, Stork comes with a `static` service discovery type. + +== Dependency + +To use the `static` service discovery, first add the appropriate Service Discovery provider dependency to your project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-service-discovery-static-list + {project-version} + +---- + +== Configuration + +For each service that should use the static list of service instances configure the service discovery `type`: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-discovery.type=static +stork.my-service.service-discovery.address-list=localhost:8080,localhost:8081 +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.type=static +quarkus.stork.my-service.service-discovery.address-list=localhost:8080,localhost:8081 +---- + +-- + +==== + +These are all the static service discovery parameters: + +include::{generated-dir}/static-sd-attributes.adoc[] diff --git a/content/docs/smallrye-stork/2/_attributes.adoc b/content/docs/smallrye-stork/2/_attributes.adoc new file mode 100644 index 000000000..ab473c9c6 --- /dev/null +++ b/content/docs/smallrye-stork/2/_attributes.adoc @@ -0,0 +1,20 @@ +:project-version: 2.7.11 +// Attributes shared by every page. smallrye.io overrides the ones it needs when it publishes. +// Pages in subdirectories set :version-root: (../ per level) before including this file. +ifndef::version-root[:version-root:] +:idprefix: +:idseparator: - +:sectanchors: +:examples-dir: {version-root}_examples +:partialsdir: {version-root}_partials +:project-name: SmallRye Stork +:javadoc-version: latest +:generated-dir: {version-root}_generated + +// Added by smallrye.io (tools/docs/Docs.java sync) +:javadoc-version: 2.7.11 +:relfileprefix: /docs/smallrye-stork/2/ +:imagesdir: /docs/smallrye-stork/2 +:docs-project: smallrye-stork +:docs-version: 2 +:docs-ref: 2.7.11 diff --git a/content/docs/smallrye-stork/2/_examples/examples/AcmeDiscoveryApiUsage.java b/content/docs/smallrye-stork/2/_examples/examples/AcmeDiscoveryApiUsage.java new file mode 100644 index 000000000..1487b571a --- /dev/null +++ b/content/docs/smallrye-stork/2/_examples/examples/AcmeDiscoveryApiUsage.java @@ -0,0 +1,20 @@ +package examples; + +import io.smallrye.mutiny.Uni; +import io.smallrye.stork.api.ServiceDefinition; +import io.smallrye.stork.api.ServiceInstance; +import io.smallrye.stork.api.StorkServiceRegistry; + +import java.util.List; + +public class AcmeDiscoveryApiUsage { + + public void example(StorkServiceRegistry stork) { + stork.defineIfAbsent("my-service", ServiceDefinition.of( + new AcmeConfiguration().withHost("my-host")) + ); + + Uni> serviceInstances = stork.getService("my-service").getInstances(); + } + +} diff --git a/content/docs/smallrye-stork/2/_examples/examples/AcmeLoadBalancer.java b/content/docs/smallrye-stork/2/_examples/examples/AcmeLoadBalancer.java new file mode 100644 index 000000000..6fb0b1be1 --- /dev/null +++ b/content/docs/smallrye-stork/2/_examples/examples/AcmeLoadBalancer.java @@ -0,0 +1,32 @@ +package examples; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Random; + +import io.smallrye.stork.api.LoadBalancer; +import io.smallrye.stork.api.NoServiceInstanceFoundException; +import io.smallrye.stork.api.ServiceInstance; + +public class AcmeLoadBalancer implements LoadBalancer { + + private final Random random; + + public AcmeLoadBalancer(AcmeLoadBalancerConfiguration config) { + random = new Random(); + } + + @Override + public ServiceInstance selectServiceInstance(Collection serviceInstances) { + if (serviceInstances.isEmpty()) { + throw new NoServiceInstanceFoundException("No services found."); + } + int index = random.nextInt(serviceInstances.size()); + return new ArrayList<>(serviceInstances).get(index); + } + + @Override + public boolean requiresStrictRecording() { + return false; + } +} diff --git a/content/docs/smallrye-stork/2/_examples/examples/AcmeLoadBalancerProvider.java b/content/docs/smallrye-stork/2/_examples/examples/AcmeLoadBalancerProvider.java new file mode 100644 index 000000000..3640322ee --- /dev/null +++ b/content/docs/smallrye-stork/2/_examples/examples/AcmeLoadBalancerProvider.java @@ -0,0 +1,22 @@ +package examples; + +import io.smallrye.stork.api.LoadBalancer; +import io.smallrye.stork.api.ServiceDiscovery; +import io.smallrye.stork.api.config.LoadBalancerAttribute; +import io.smallrye.stork.api.config.LoadBalancerType; +import io.smallrye.stork.spi.LoadBalancerProvider; +import jakarta.enterprise.context.ApplicationScoped; + +@LoadBalancerType("acme-load-balancer") +@LoadBalancerAttribute(name = "my-attribute", + description = "Attribute that alters the behavior of the LoadBalancer") +@ApplicationScoped +public class AcmeLoadBalancerProvider implements + LoadBalancerProvider { + + @Override + public LoadBalancer createLoadBalancer(AcmeLoadBalancerConfiguration config, + ServiceDiscovery serviceDiscovery) { + return new AcmeLoadBalancer(config); + } +} diff --git a/content/docs/smallrye-stork/2/_examples/examples/AcmeObservationCollector.java b/content/docs/smallrye-stork/2/_examples/examples/AcmeObservationCollector.java new file mode 100644 index 000000000..ea3378927 --- /dev/null +++ b/content/docs/smallrye-stork/2/_examples/examples/AcmeObservationCollector.java @@ -0,0 +1,36 @@ +package examples; + +import io.smallrye.stork.Stork; +import io.smallrye.stork.api.observability.ObservationCollector; +import io.smallrye.stork.api.observability.StorkEventHandler; +import io.smallrye.stork.api.observability.StorkObservation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class AcmeObservationCollector implements ObservationCollector { + + private static final Logger LOGGER = LoggerFactory.getLogger(AcmeObservationCollector.class); + + private static final StorkEventHandler ACME_HANDLER = event -> { + //This is the terminal event. Put here your custom logic to extend the metrics collection. + + //E.g. Expose metrics to Micrometer, additional logs.... + LOGGER.info( "Service discovery took " + event.getServiceDiscoveryDuration() + "."); + LOGGER.info( event.getDiscoveredInstancesCount() + " have been discovered for " + event.getServiceName() + "."); + LOGGER.info( "Service selection took " + event.getServiceSelectionDuration() + "."); + + // ... + + }; + + public static StorkObservation ACME_STORK_EVENT; + + @Override + public StorkObservation create(String serviceName, String serviceDiscoveryType, + String serviceSelectionType) { + ACME_STORK_EVENT = new StorkObservation( + serviceName, serviceDiscoveryType, serviceSelectionType, + ACME_HANDLER); + return ACME_STORK_EVENT; + } +} diff --git a/content/docs/smallrye-stork/2/_examples/examples/AcmeRegistrarApiUsage.java b/content/docs/smallrye-stork/2/_examples/examples/AcmeRegistrarApiUsage.java new file mode 100644 index 000000000..1d69dc207 --- /dev/null +++ b/content/docs/smallrye-stork/2/_examples/examples/AcmeRegistrarApiUsage.java @@ -0,0 +1,21 @@ +package examples; + +import io.smallrye.stork.api.ServiceDefinition; +import io.smallrye.stork.api.StorkServiceRegistry; +import io.smallrye.stork.servicediscovery.staticlist.StaticConfiguration; + +public class AcmeRegistrarApiUsage { + + public void example(StorkServiceRegistry stork) { + String list = "localhost:8080, localhost:8081"; + + stork.defineIfAbsent("my-service", ServiceDefinition.of( + new StaticConfiguration().withAddressList(list), + new AcmeLoadBalancerConfiguration().withMyAttribute("my-value"),new AcmeRegistrarConfiguration()) + ); + + stork.getService("my-service").registerInstance("my-service", "localhost", + 9000); + } + +} diff --git a/content/docs/smallrye-stork/2/_examples/examples/AcmeSelectorApiUsage.java b/content/docs/smallrye-stork/2/_examples/examples/AcmeSelectorApiUsage.java new file mode 100644 index 000000000..67440ddf9 --- /dev/null +++ b/content/docs/smallrye-stork/2/_examples/examples/AcmeSelectorApiUsage.java @@ -0,0 +1,22 @@ +package examples; + +import io.smallrye.mutiny.Uni; +import io.smallrye.stork.api.ServiceDefinition; +import io.smallrye.stork.api.ServiceInstance; +import io.smallrye.stork.api.StorkServiceRegistry; +import io.smallrye.stork.servicediscovery.staticlist.StaticConfiguration; +import io.smallrye.stork.serviceregistration.staticlist.StaticRegistrarConfiguration; + +public class AcmeSelectorApiUsage { + + public void example(StorkServiceRegistry stork) { + String list = "localhost:8080, localhost:8081"; + stork.defineIfAbsent("my-service", ServiceDefinition.of( + new StaticConfiguration().withAddressList(list), + new AcmeLoadBalancerConfiguration().withMyAttribute("my-value"),new StaticRegistrarConfiguration()) + ); + + Uni uni = stork.getService("my-service").selectInstance(); + } + +} diff --git a/content/docs/smallrye-stork/2/_examples/examples/AcmeServiceDiscovery.java b/content/docs/smallrye-stork/2/_examples/examples/AcmeServiceDiscovery.java new file mode 100644 index 000000000..b9368c4b4 --- /dev/null +++ b/content/docs/smallrye-stork/2/_examples/examples/AcmeServiceDiscovery.java @@ -0,0 +1,32 @@ +package examples; + +import java.util.Collections; +import java.util.List; + +import io.smallrye.mutiny.Uni; +import io.smallrye.stork.api.ServiceDiscovery; +import io.smallrye.stork.api.ServiceInstance; +import io.smallrye.stork.impl.DefaultServiceInstance; +import io.smallrye.stork.utils.ServiceInstanceIds; + +public class AcmeServiceDiscovery implements ServiceDiscovery { + + private final String host; + private final int port; + + public AcmeServiceDiscovery(AcmeConfiguration configuration) { + this.host = configuration.getHost(); + this.port = Integer.parseInt(configuration.getPort()); + } + + @Override + public Uni> getServiceInstances() { + // Proceed to the lookup... + // Here, we just return a DefaultServiceInstance with the configured host and port + // The last parameter specifies whether the communication with the instance should + // happen over a secure connection + DefaultServiceInstance instance = + new DefaultServiceInstance(ServiceInstanceIds.next(), host, port, false); + return Uni.createFrom().item(() -> Collections.singletonList(instance)); + } +} diff --git a/content/docs/smallrye-stork/2/_examples/examples/AcmeServiceDiscoveryProvider.java b/content/docs/smallrye-stork/2/_examples/examples/AcmeServiceDiscoveryProvider.java new file mode 100644 index 000000000..ca39d5289 --- /dev/null +++ b/content/docs/smallrye-stork/2/_examples/examples/AcmeServiceDiscoveryProvider.java @@ -0,0 +1,28 @@ +package examples; + +import io.smallrye.stork.api.ServiceDiscovery; +import io.smallrye.stork.api.config.ServiceConfig; +import io.smallrye.stork.api.config.ServiceDiscoveryAttribute; +import io.smallrye.stork.api.config.ServiceDiscoveryType; +import io.smallrye.stork.spi.StorkInfrastructure; +import io.smallrye.stork.spi.ServiceDiscoveryProvider; +import jakarta.enterprise.context.ApplicationScoped; + +@ServiceDiscoveryType("acme") +@ServiceDiscoveryAttribute(name = "host", + description = "Host name of the service discovery server.", required = true) +@ServiceDiscoveryAttribute(name = "port", + description = "Hort of the service discovery server.", required = false) +@ApplicationScoped +public class AcmeServiceDiscoveryProvider + implements ServiceDiscoveryProvider { + + @Override + public ServiceDiscovery createServiceDiscovery( + AcmeConfiguration config, + String serviceName, + ServiceConfig serviceConfig, + StorkInfrastructure storkInfrastructure) { + return new AcmeServiceDiscovery(config); + } +} diff --git a/content/docs/smallrye-stork/2/_examples/examples/AcmeServiceRegistrar.java b/content/docs/smallrye-stork/2/_examples/examples/AcmeServiceRegistrar.java new file mode 100644 index 000000000..41ce5db4c --- /dev/null +++ b/content/docs/smallrye-stork/2/_examples/examples/AcmeServiceRegistrar.java @@ -0,0 +1,30 @@ +package examples; + +import io.smallrye.mutiny.Uni; +import io.smallrye.stork.api.Metadata; +import io.smallrye.stork.api.ServiceRegistrar; + +public class AcmeServiceRegistrar implements ServiceRegistrar { + + private final String backendHost; + private final int backendPort; + + public AcmeServiceRegistrar(AcmeRegistrarConfiguration configuration) { + this.backendHost = configuration.getHost(); + this.backendPort = Integer.parseInt(configuration.getPort()); + } + + + @Override + public Uni registerServiceInstance(String serviceName, String instanceName, Metadata metadata, String ipAddress, int defaultPort) { + //instanceName is an optional identifier for this specific instance; use it as the registration ID when provided + //do whatever is needed for registering service instance + return Uni.createFrom().voidItem(); + } + + @Override + public Uni deregisterServiceInstance(String serviceName) { + //do whatever is needed for deregistering service instance + return Uni.createFrom().voidItem(); + } +} diff --git a/content/docs/smallrye-stork/2/_examples/examples/AcmeServiceRegistrarProvider.java b/content/docs/smallrye-stork/2/_examples/examples/AcmeServiceRegistrarProvider.java new file mode 100644 index 000000000..52e028cfb --- /dev/null +++ b/content/docs/smallrye-stork/2/_examples/examples/AcmeServiceRegistrarProvider.java @@ -0,0 +1,28 @@ +package examples; + +import io.smallrye.stork.api.Metadata; +import io.smallrye.stork.api.ServiceRegistrar; +import io.smallrye.stork.api.config.ServiceRegistrarAttribute; +import io.smallrye.stork.api.config.ServiceRegistrarType; +import io.smallrye.stork.spi.ServiceRegistrarProvider; +import io.smallrye.stork.spi.StorkInfrastructure; +import jakarta.enterprise.context.ApplicationScoped; + +@ServiceRegistrarType(value = "acme", metadataKey = Metadata.DefaultMetadataKey.class) +@ServiceRegistrarAttribute(name = "host", + description = "Host name of the service registration server.", required = true) +@ServiceRegistrarAttribute(name = "port", + description = "Port of the service registration server.", required = false) +@ApplicationScoped +public class AcmeServiceRegistrarProvider + implements ServiceRegistrarProvider { + + @Override + public ServiceRegistrar createServiceRegistrar( + AcmeRegistrarConfiguration config, + String serviceName, + StorkInfrastructure storkInfrastructure) { + return new AcmeServiceRegistrar(config); + } + +} diff --git a/content/docs/smallrye-stork/2/_examples/examples/CachedAcmeServiceDiscovery.java b/content/docs/smallrye-stork/2/_examples/examples/CachedAcmeServiceDiscovery.java new file mode 100644 index 000000000..204aa78c9 --- /dev/null +++ b/content/docs/smallrye-stork/2/_examples/examples/CachedAcmeServiceDiscovery.java @@ -0,0 +1,31 @@ +package examples; + +import io.smallrye.mutiny.Uni; +import io.smallrye.stork.api.ServiceDiscovery; +import io.smallrye.stork.api.ServiceInstance; +import io.smallrye.stork.impl.CachingServiceDiscovery; +import io.smallrye.stork.impl.DefaultServiceInstance; +import io.smallrye.stork.utils.ServiceInstanceIds; + +import java.util.Collections; +import java.util.List; + +public class CachedAcmeServiceDiscovery extends CachingServiceDiscovery { + + private final String host; + private final int port; + + public CachedAcmeServiceDiscovery(CachedAcmeConfiguration configuration) { + super(configuration.getRefreshPeriod()); // (1) + this.host = configuration.getHost(); + this.port = Integer.parseInt(configuration.getPort()); + } + + @Override // (2) + public Uni> fetchNewServiceInstances(List previousInstances) { + // Retrieve services... + DefaultServiceInstance instance = + new DefaultServiceInstance(ServiceInstanceIds.next(), host, port, false); + return Uni.createFrom().item(() -> Collections.singletonList(instance)); + } +} diff --git a/content/docs/smallrye-stork/2/_examples/examples/CachedAcmeServiceDiscoveryProvider.java b/content/docs/smallrye-stork/2/_examples/examples/CachedAcmeServiceDiscoveryProvider.java new file mode 100644 index 000000000..56d627671 --- /dev/null +++ b/content/docs/smallrye-stork/2/_examples/examples/CachedAcmeServiceDiscoveryProvider.java @@ -0,0 +1,32 @@ +package examples; + +import io.smallrye.stork.api.ServiceDiscovery; +import io.smallrye.stork.api.config.ServiceConfig; +import io.smallrye.stork.api.config.ServiceDiscoveryAttribute; +import io.smallrye.stork.api.config.ServiceDiscoveryType; +import io.smallrye.stork.impl.CachingServiceDiscovery; +import io.smallrye.stork.spi.ServiceDiscoveryProvider; +import io.smallrye.stork.spi.StorkInfrastructure; +import jakarta.enterprise.context.ApplicationScoped; + +@ServiceDiscoveryType("cached-acme") +@ServiceDiscoveryAttribute(name = "host", + description = "Host name of the service discovery server.", required = true) +@ServiceDiscoveryAttribute(name = "port", + description = "Hort of the service discovery server.", required = false) +@ServiceDiscoveryAttribute(name = "refresh-period", + description = "Service discovery cache refresh period.", + defaultValue = CachingServiceDiscovery.DEFAULT_REFRESH_INTERVAL) +@ApplicationScoped +public class CachedAcmeServiceDiscoveryProvider + implements ServiceDiscoveryProvider { + + @Override + public ServiceDiscovery createServiceDiscovery( + CachedAcmeConfiguration config, + String serviceName, + ServiceConfig serviceConfig, + StorkInfrastructure storkInfrastructure) { + return new CachedAcmeServiceDiscovery(config); + } +} diff --git a/content/docs/smallrye-stork/2/_examples/examples/CustomExpirationCachedAcmeServiceDiscovery.java b/content/docs/smallrye-stork/2/_examples/examples/CustomExpirationCachedAcmeServiceDiscovery.java new file mode 100644 index 000000000..b2e3823c9 --- /dev/null +++ b/content/docs/smallrye-stork/2/_examples/examples/CustomExpirationCachedAcmeServiceDiscovery.java @@ -0,0 +1,44 @@ +package examples; + +import io.smallrye.mutiny.Uni; +import io.smallrye.stork.api.ServiceInstance; +import io.smallrye.stork.impl.CachingServiceDiscovery; +import io.smallrye.stork.impl.DefaultServiceInstance; +import io.smallrye.stork.utils.ServiceInstanceIds; + +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +public class CustomExpirationCachedAcmeServiceDiscovery extends CachingServiceDiscovery { + + private final String host; + private final int port; + + private AtomicBoolean invalidated = new AtomicBoolean(); + + public CustomExpirationCachedAcmeServiceDiscovery(CachedAcmeConfiguration configuration) { + super(configuration.getRefreshPeriod()); + this.host = configuration.getHost(); + this.port = Integer.parseInt(configuration.getPort()); + } + + @Override + public Uni> fetchNewServiceInstances(List previousInstances) { + // Retrieve services... + DefaultServiceInstance instance = + new DefaultServiceInstance(ServiceInstanceIds.next(), host, port, false); + return Uni.createFrom().item(() -> Collections.singletonList(instance)); + } + + @Override + public Uni> cache(Uni> uni) { + return uni.memoize().until(() -> invalidated.get()); + } + + @Override + public void invalidate() { + invalidated.set(true); + } + +} \ No newline at end of file diff --git a/content/docs/smallrye-stork/2/_examples/examples/DefinitionExample.java b/content/docs/smallrye-stork/2/_examples/examples/DefinitionExample.java new file mode 100644 index 000000000..aab2dc3d6 --- /dev/null +++ b/content/docs/smallrye-stork/2/_examples/examples/DefinitionExample.java @@ -0,0 +1,30 @@ +package examples; + +import io.smallrye.stork.Stork; +import io.smallrye.stork.api.ServiceDefinition; +import io.smallrye.stork.loadbalancer.random.RandomConfiguration; +import io.smallrye.stork.servicediscovery.staticlist.StaticConfiguration; +import io.smallrye.stork.serviceregistration.staticlist.StaticRegistrarConfiguration; + +public class DefinitionExample { + + public static void example(Stork stork) { + String example = "localhost:8080, localhost:8081"; + + // A service using a static list of locations as discovery + // As not set, it defaults to round-robin to select the instance. + stork.defineIfAbsent("my-service", + ServiceDefinition.of(new StaticConfiguration().withAddressList(example))); + + // Another service using the random selection strategy, instead of round-robin + stork.defineIfAbsent("my-second-service", + ServiceDefinition.of(new StaticConfiguration().withAddressList(example), + new RandomConfiguration())); + + // Another service using the random selection strategy, instead of round-robin + // and a static service registrar + stork.defineIfAbsent("my-second-service", + ServiceDefinition.of(new StaticConfiguration().withAddressList(example), + new RandomConfiguration(), new StaticRegistrarConfiguration())); + } +} diff --git a/content/docs/smallrye-stork/2/_examples/examples/HelloClient.java b/content/docs/smallrye-stork/2/_examples/examples/HelloClient.java new file mode 100644 index 000000000..eec581608 --- /dev/null +++ b/content/docs/smallrye-stork/2/_examples/examples/HelloClient.java @@ -0,0 +1,20 @@ +package examples; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + + + +@Path("/") +@RegisterRestClient(baseUri = "stork://hello-service/hello") +public interface HelloClient { + @POST + @Produces(MediaType.TEXT_PLAIN) + @Consumes(MediaType.TEXT_PLAIN) + String echo(String name); +} + diff --git a/content/docs/smallrye-stork/2/_examples/examples/InitializationExample.java b/content/docs/smallrye-stork/2/_examples/examples/InitializationExample.java new file mode 100644 index 000000000..cb93ca0bd --- /dev/null +++ b/content/docs/smallrye-stork/2/_examples/examples/InitializationExample.java @@ -0,0 +1,18 @@ +package examples; + +import io.smallrye.stork.Stork; +import io.smallrye.stork.api.ServiceDefinition; +import io.smallrye.stork.api.ServiceInstance; +import io.smallrye.stork.loadbalancer.random.RandomConfiguration; +import io.smallrye.stork.servicediscovery.staticlist.StaticConfiguration; + +import java.time.Duration; + +public class InitializationExample { + + public static void main(String[] args) { + Stork.initialize(); + Stork stork = Stork.getInstance(); + // ... + } +} diff --git a/content/docs/smallrye-stork/2/_examples/examples/LookupExample.java b/content/docs/smallrye-stork/2/_examples/examples/LookupExample.java new file mode 100644 index 000000000..71e232d12 --- /dev/null +++ b/content/docs/smallrye-stork/2/_examples/examples/LookupExample.java @@ -0,0 +1,27 @@ +package examples; + +import io.smallrye.mutiny.Uni; +import io.smallrye.stork.Stork; +import io.smallrye.stork.api.Service; +import io.smallrye.stork.api.ServiceDefinition; +import io.smallrye.stork.api.ServiceInstance; +import io.smallrye.stork.loadbalancer.random.RandomConfiguration; +import io.smallrye.stork.servicediscovery.staticlist.StaticConfiguration; + +import java.util.List; +import java.util.Map; + +public class LookupExample { + + public static void example(Stork stork) { + Service service = stork.getService("my-service"); + + // Gets all the available instances: + Uni> instances = service.getInstances(); + // Select one instance using the load balancing strategy + Uni instance = service.selectInstance(); + + // Gets all the managed services: + Map services = stork.getServices(); + } +} diff --git a/content/docs/smallrye-stork/2/_examples/examples/ObservableInitializationExample.java b/content/docs/smallrye-stork/2/_examples/examples/ObservableInitializationExample.java new file mode 100644 index 000000000..d18854291 --- /dev/null +++ b/content/docs/smallrye-stork/2/_examples/examples/ObservableInitializationExample.java @@ -0,0 +1,13 @@ +package examples; + +import io.smallrye.stork.Stork; +import io.smallrye.stork.integration.ObservableStorkInfrastructure; + +public class ObservableInitializationExample { + + public static void main(String[] args) { + Stork.initialize(new ObservableStorkInfrastructure(new AcmeObservationCollector())); + Stork stork = Stork.getInstance(); + // ... + } +} diff --git a/content/docs/smallrye-stork/2/_examples/examples/ObservationExample.java b/content/docs/smallrye-stork/2/_examples/examples/ObservationExample.java new file mode 100644 index 000000000..299fc5b4e --- /dev/null +++ b/content/docs/smallrye-stork/2/_examples/examples/ObservationExample.java @@ -0,0 +1,35 @@ +package examples; + +import io.smallrye.mutiny.Uni; +import io.smallrye.stork.Stork; +import io.smallrye.stork.api.Service; +import io.smallrye.stork.api.ServiceInstance; +import io.smallrye.stork.api.observability.ObservationCollector; +import io.smallrye.stork.api.observability.StorkObservation; + +import java.time.Duration; +import java.util.List; +import java.util.Map; + +import static examples.AcmeObservationCollector.*; + +public class ObservationExample { + + public static void example(Stork stork) { + Service service = stork.getService("my-service"); + + ObservationCollector observations = service.getObservations(); + + // Gets the time spent in service discovery and service selection even if any error happens + Duration overallDuration = ACME_STORK_EVENT.getOverallDuration(); + + // Gets the total number of instances discovered + int discoveredInstancesCount = ACME_STORK_EVENT.getDiscoveredInstancesCount(); + + // Gets the error raised during the process + Throwable failure = ACME_STORK_EVENT.failure(); + + // ... + + } +} diff --git a/content/docs/smallrye-stork/2/_examples/examples/SpringBootInitializationExample.java b/content/docs/smallrye-stork/2/_examples/examples/SpringBootInitializationExample.java new file mode 100644 index 000000000..d21242584 --- /dev/null +++ b/content/docs/smallrye-stork/2/_examples/examples/SpringBootInitializationExample.java @@ -0,0 +1,29 @@ +package examples; + +import io.smallrye.stork.Stork; +import io.smallrye.stork.springboot.SpringBootApplicationContextProvider; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.DependsOn; + +@SpringBootApplication +public class SpringBootInitializationExample { + + public static void main(String[] args) { + SpringApplication.run(SpringBootInitializationExample.class, args); + } + + @Bean + @DependsOn("springBootApplicationContextProvider") + public Stork stork() { + Stork.initialize(); + Stork stork = Stork.getInstance(); + return stork; + } + + @Bean + public SpringBootApplicationContextProvider springBootApplicationContextProvider() { + return new SpringBootApplicationContextProvider(); + } +} diff --git a/content/docs/smallrye-stork/2/_examples/examples/StorkApiExample.java b/content/docs/smallrye-stork/2/_examples/examples/StorkApiExample.java new file mode 100644 index 000000000..d8713eb9f --- /dev/null +++ b/content/docs/smallrye-stork/2/_examples/examples/StorkApiExample.java @@ -0,0 +1,40 @@ +package examples; + +import io.smallrye.stork.Stork; +import io.smallrye.stork.api.ServiceDefinition; +import io.smallrye.stork.api.ServiceInstance; +import io.smallrye.stork.loadbalancer.random.RandomConfiguration; +import io.smallrye.stork.servicediscovery.staticlist.StaticConfiguration; +import io.smallrye.stork.serviceregistration.staticlist.StaticRegistrarConfiguration; + +import java.time.Duration; + +public class StorkApiExample { + + public static void main(String[] args) { + Stork.initialize(); + Stork stork = Stork.getInstance(); + + String example = "localhost:8080, localhost:8082"; + + // A service using a static list of locations as discovery + // As not set, it defaults to round-robin to select the instance. + stork.defineIfAbsent("my-service", + ServiceDefinition.of(new StaticConfiguration().withAddressList(example))); + + // Another service using the random selection strategy, instead of round-robin + stork.defineIfAbsent("my-second-service", + ServiceDefinition.of(new StaticConfiguration().withAddressList(example), + new RandomConfiguration())); + + ServiceInstance instance = stork.getService("my-second-service").selectInstance() + .await().atMost(Duration.ofSeconds(1)); + System.out.println(instance.getHost() + ":" + instance.getPort()); + + // Another service using the random selection strategy, instead of round-robin + // and a static service registrar + stork.defineIfAbsent("my-third-service", + ServiceDefinition.of(new StaticConfiguration().withAddressList(example), + new RandomConfiguration(), new StaticRegistrarConfiguration())); + } +} diff --git a/content/docs/smallrye-stork/2/_examples/examples/StorkEntryPointExample.java b/content/docs/smallrye-stork/2/_examples/examples/StorkEntryPointExample.java new file mode 100644 index 000000000..99343bd51 --- /dev/null +++ b/content/docs/smallrye-stork/2/_examples/examples/StorkEntryPointExample.java @@ -0,0 +1,14 @@ +package examples; + +import io.smallrye.stork.Stork; + +public class StorkEntryPointExample { + + public static void main(String[] args) { + Stork.initialize(); + Stork stork = Stork.getInstance(); + // ... + Stork.shutdown(); + } + +} diff --git a/content/docs/smallrye-stork/2/_examples/examples/StorkServiceExample.java b/content/docs/smallrye-stork/2/_examples/examples/StorkServiceExample.java new file mode 100644 index 000000000..c0b7dbb06 --- /dev/null +++ b/content/docs/smallrye-stork/2/_examples/examples/StorkServiceExample.java @@ -0,0 +1,18 @@ +package examples; + +import io.smallrye.stork.api.Service; +import io.smallrye.stork.Stork; + +public class StorkServiceExample { + + public static void main(String[] args) { + Stork.initialize(); + Stork stork = Stork.getInstance(); + + Service service = stork.getService("my-service"); + + // ... + Stork.shutdown(); + } + +} diff --git a/content/docs/smallrye-stork/2/_examples/examples/StorkServiceLookupExample.java b/content/docs/smallrye-stork/2/_examples/examples/StorkServiceLookupExample.java new file mode 100644 index 000000000..70c03cdc1 --- /dev/null +++ b/content/docs/smallrye-stork/2/_examples/examples/StorkServiceLookupExample.java @@ -0,0 +1,24 @@ +package examples; + +import java.time.Duration; +import java.util.List; + +import io.smallrye.stork.Stork; +import io.smallrye.stork.api.Service; +import io.smallrye.stork.api.ServiceInstance; + +public class StorkServiceLookupExample { + + public static void main(String[] args) { + Stork.initialize(); + Stork stork = Stork.getInstance(); + + Service service = stork.getService("my-service"); + List instances = service.getInstances() + .await().atMost(Duration.ofSeconds(5)); + + // ... + Stork.shutdown(); + } + +} diff --git a/content/docs/smallrye-stork/2/_examples/examples/StorkServiceSelectionExample.java b/content/docs/smallrye-stork/2/_examples/examples/StorkServiceSelectionExample.java new file mode 100644 index 000000000..a9e3dcc85 --- /dev/null +++ b/content/docs/smallrye-stork/2/_examples/examples/StorkServiceSelectionExample.java @@ -0,0 +1,25 @@ +package examples; + +import java.time.Duration; + +import io.smallrye.stork.Stork; +import io.smallrye.stork.api.Service; +import io.smallrye.stork.api.ServiceInstance; + +public class StorkServiceSelectionExample { + + public static void main(String[] args) { + Stork.initialize(); + Stork stork = Stork.getInstance(); + + Service service = stork.getService("my-service"); + ServiceInstance instance = service.selectInstance() + .await().atMost(Duration.ofSeconds(5)); + + System.out.println(instance.getHost() + ":" + instance.getPort()); + + // ... + Stork.shutdown(); + } + +} diff --git a/content/docs/smallrye-stork/2/_generated/composite-sd-attributes.adoc b/content/docs/smallrye-stork/2/_generated/composite-sd-attributes.adoc new file mode 100644 index 000000000..ae556fc30 --- /dev/null +++ b/content/docs/smallrye-stork/2/_generated/composite-sd-attributes.adoc @@ -0,0 +1,5 @@ +[cols=",,,",options="header",] +|=== +|Attribute |Mandatory |Default Value |Description +|`services` |Yes | |A comma-separated list of services that this services consists of. +|=== diff --git a/content/docs/smallrye-stork/2/_generated/consul-sd-attributes.adoc b/content/docs/smallrye-stork/2/_generated/consul-sd-attributes.adoc new file mode 100644 index 000000000..007fb5d01 --- /dev/null +++ b/content/docs/smallrye-stork/2/_generated/consul-sd-attributes.adoc @@ -0,0 +1,17 @@ +[cols=",,,",options="header",] +|=== +|Attribute |Mandatory |Default Value |Description +|`consul-host` |No |`localhost` |The Consul host. +|`consul-port` |No |`8500` |The Consul port. +|`use-health-checks` |No |`true` |Whether to use health check. +|`application` |No | |The application name; if not defined Stork service name will be used. +|`refresh-period` |No |`5M` |Service discovery cache refresh period. +|`secure` |No | |whether the connection with the service should be encrypted with TLS. +|`ssl` |No |`false` |Whether to enable TLS/SSL when connecting to Consul (default: false) +|`trust-store-path` |No |++``++ |Path to the trust store file used to verify the Consul server certificate +|`trust-store-password` |No |++``++ |Password of the trust store +|`key-store-path` |No |++``++ |Path to the key store file containing the client certificate and private key +|`key-store-password` |No |++``++ |Password of the key store +|`verify-host` |No |`false` |Whether to enable hostname verification for the Consul TLS connection (default: false) +|`acl-token` |No |++``++ |Consul ACL token used for authentication when accessing the Consul API +|=== diff --git a/content/docs/smallrye-stork/2/_generated/consul-sr-attributes.adoc b/content/docs/smallrye-stork/2/_generated/consul-sr-attributes.adoc new file mode 100644 index 000000000..861f4a93b --- /dev/null +++ b/content/docs/smallrye-stork/2/_generated/consul-sr-attributes.adoc @@ -0,0 +1,16 @@ +[cols=",,,",options="header",] +|=== +|Attribute |Mandatory |Default Value |Description +|`consul-host` |No |`localhost` |The Consul host. +|`consul-port` |No |`8500` |The Consul port. +|`health-check-url` |No |++``++ |The liveness http address. +|`health-check-interval` |No |`30s` |How often Consul performs the health check +|`health-check-deregister-after` |No |`1m` |How long after the check is in critical status Consul will remove the service from the catalogue. +|`ssl` |No |`false` |Whether to enable TLS/SSL when connecting to Consul (default: false) +|`trust-store-path` |No |++``++ |Path to the trust store file used to verify the Consul server certificate +|`trust-store-password` |No |++``++ |Password of the trust store +|`key-store-path` |No |++``++ |Path to the key store file containing the client certificate and private key +|`key-store-password` |No |++``++ |Password of the key store +|`verify-host` |No |`false` |Whether to enable hostname verification for the Consul TLS connection (default: false) +|`acl-token` |No |++``++ |Consul ACL token used for authentication when accessing the Consul API +|=== diff --git a/content/docs/smallrye-stork/2/_generated/dns-sd-attributes.adoc b/content/docs/smallrye-stork/2/_generated/dns-sd-attributes.adoc new file mode 100644 index 000000000..d1e538a95 --- /dev/null +++ b/content/docs/smallrye-stork/2/_generated/dns-sd-attributes.adoc @@ -0,0 +1,14 @@ +[cols=",,,",options="header",] +|=== +|Attribute |Mandatory |Default Value |Description +|`dns-servers` |No |`none` |Comma separated list of dns servers. Servers can either be in the `server:port` or just `server` form. Use `none` to use the system resolver. +|`hostname` |No | |The hostname to look up; if not defined Stork service name will be used. +|`record-type` |No |`SRV` |Type of the DNS record. A, AAAA and SRV records are supported +|`port` |No | |Port of the service instances. Required if the record type is other than SRV. +|`refresh-period` |No |`5M` |Service discovery cache refresh period. +|`secure` |No | |Whether the connection with the service should be encrypted with TLS. +|`recursion-desired` |No |`true` |Whether DNS recursion is desired +|`resolve-srv` |No |`true` |Whether DNS resolution for SRV records is desired +|`dns-timeout` |No |`5s` |Timeout for DNS queries +|`fail-on-error` |No |`false` |Whether an error in retrieving service instances from one of the DNS servers should cause a failure of the discovery attempt. +|=== diff --git a/content/docs/smallrye-stork/2/_generated/eureka-sd-attributes.adoc b/content/docs/smallrye-stork/2/_generated/eureka-sd-attributes.adoc new file mode 100644 index 000000000..2a2581bcb --- /dev/null +++ b/content/docs/smallrye-stork/2/_generated/eureka-sd-attributes.adoc @@ -0,0 +1,13 @@ +[cols=",,,",options="header",] +|=== +|Attribute |Mandatory |Default Value |Description +|`eureka-host` |Yes | |The Eureka server host. +|`eureka-port` |No |`8761` |The Eureka server port. +|`eureka-context-path` |No |`/` |The Eureka server root context path. +|`application` |No | |The Eureka application Id; if not defined Stork service name will be used +|`eureka-trust-all` |No |`false` |Enable/Disable the TLS certificate verification +|`eureka-tls` |No |`false` |Use TLS to connect to the Eureka server +|`instance` |No | |The Eureka application instance Id +|`refresh-period` |No |`5M` |Service discovery cache refresh period. +|`secure` |No |`false` |Whether is should select the secured endpoint of the retrieved services. +|=== diff --git a/content/docs/smallrye-stork/2/_generated/eureka-sr-attributes.adoc b/content/docs/smallrye-stork/2/_generated/eureka-sr-attributes.adoc new file mode 100644 index 000000000..e18d0ba66 --- /dev/null +++ b/content/docs/smallrye-stork/2/_generated/eureka-sr-attributes.adoc @@ -0,0 +1,11 @@ +[cols=",,,",options="header",] +|=== +|Attribute |Mandatory |Default Value |Description +|`eureka-host` |No |`localhost` |The Eureka host. +|`eureka-port` |No |`8761` |The Eureka port. +|`eureka-context-path` |No |`/` |The Eureka server root context path. +|`eureka-trust-all` |No |`false` |Enable/Disable the TLS certificate verification +|`eureka-tls` |No |`false` |Use TLS to connect to the Eureka server +|`health-check-url` |No |++``++ |The liveness http address. +|`host-name` |No |++``++ |The host name to register in Eureka. If not set, the ipAddress is used. +|=== diff --git a/content/docs/smallrye-stork/2/_generated/knative-sd-attributes.adoc b/content/docs/smallrye-stork/2/_generated/knative-sd-attributes.adoc new file mode 100644 index 000000000..3b3fe0eb3 --- /dev/null +++ b/content/docs/smallrye-stork/2/_generated/knative-sd-attributes.adoc @@ -0,0 +1,9 @@ +[cols=",,,",options="header",] +|=== +|Attribute |Mandatory |Default Value |Description +|`knative-host` |No | |The Knative API host. +|`knative-namespace` |No | |The namespace of the service. Use all to discover all namespaces. +|`application` |No | |The Knative application Id; if not defined Stork service name will be used. +|`refresh-period` |No |`5M` |Service discovery cache refresh period. +|`secure` |No | |Whether the connection with the service should be encrypted with TLS. +|=== diff --git a/content/docs/smallrye-stork/2/_generated/kubernetes-sd-attributes.adoc b/content/docs/smallrye-stork/2/_generated/kubernetes-sd-attributes.adoc new file mode 100644 index 000000000..d0bbb6980 --- /dev/null +++ b/content/docs/smallrye-stork/2/_generated/kubernetes-sd-attributes.adoc @@ -0,0 +1,14 @@ +[cols=",,,",options="header",] +|=== +|Attribute |Mandatory |Default Value |Description +|`k8s-host` |No | |The Kubernetes API host. +|`k8s-namespace` |No | |The namespace of the service. Use all to discover all namespaces. +|`application` |No | |The Kubernetes application Id; if not defined Stork service name will be used. +|`port-name` |No | |The Kubernetes application port name. If not defined, when exposing multiple ports, Stork will use the first one. +|`refresh-period` |No |`5M` |Service discovery cache refresh period. +|`secure` |No | |Whether the connection with the service should be encrypted with TLS. +|`request-retry-backoff-limit` |No | |Maximum number of retry attempts allowed after a request failure. +|`request-retry-backoff-interval` |No | |Amount of time to wait between retry attempts after a request fails. +|`use-endpoint-slices` |No | |Use the EndpointSlice API instead +|`use-cluster-ip` |No |`false` |Use the Kubernetes Service ClusterIP instead of pod IPs. Stork returns a single instance with the ClusterIP and resolved port. +|=== diff --git a/content/docs/smallrye-stork/2/_generated/least-requests-lb-attributes.adoc b/content/docs/smallrye-stork/2/_generated/least-requests-lb-attributes.adoc new file mode 100644 index 000000000..60c10c523 --- /dev/null +++ b/content/docs/smallrye-stork/2/_generated/least-requests-lb-attributes.adoc @@ -0,0 +1,4 @@ +[cols=",,,",options="header",] +|=== +|Attribute |Mandatory |Default Value |Description +|=== diff --git a/content/docs/smallrye-stork/2/_generated/least-response-time-lb-attributes.adoc b/content/docs/smallrye-stork/2/_generated/least-response-time-lb-attributes.adoc new file mode 100644 index 000000000..dd977d2d0 --- /dev/null +++ b/content/docs/smallrye-stork/2/_generated/least-response-time-lb-attributes.adoc @@ -0,0 +1,7 @@ +[cols=",,,",options="header",] +|=== +|Attribute |Mandatory |Default Value |Description +|`declining-factor` |No |`0.9` |How much _score_ should decline in time, see Score calculation in the docs for details. +|`error-penalty` |No |`60s` |This load balancer treats an erroneous response as a response after this time. +|`use-secure-random` |No |`false` |Whether the load balancer should use a SecureRandom instead of a Random (default). Check https://stackoverflow.com/questions/11051205/difference-between-java-util-random-and-java-security-securerandom[this page] to understand the difference +|=== diff --git a/content/docs/smallrye-stork/2/_generated/power-of-two-choices-lb-attributes.adoc b/content/docs/smallrye-stork/2/_generated/power-of-two-choices-lb-attributes.adoc new file mode 100644 index 000000000..95a37a4df --- /dev/null +++ b/content/docs/smallrye-stork/2/_generated/power-of-two-choices-lb-attributes.adoc @@ -0,0 +1,5 @@ +[cols=",,,",options="header",] +|=== +|Attribute |Mandatory |Default Value |Description +|`use-secure-random` |No |`false` |Whether the load balancer should use a SecureRandom instead of a Random (default). Check https://stackoverflow.com/questions/11051205/difference-between-java-util-random-and-java-security-securerandom[this page] to understand the difference +|=== diff --git a/content/docs/smallrye-stork/2/_generated/random-lb-attributes.adoc b/content/docs/smallrye-stork/2/_generated/random-lb-attributes.adoc new file mode 100644 index 000000000..95a37a4df --- /dev/null +++ b/content/docs/smallrye-stork/2/_generated/random-lb-attributes.adoc @@ -0,0 +1,5 @@ +[cols=",,,",options="header",] +|=== +|Attribute |Mandatory |Default Value |Description +|`use-secure-random` |No |`false` |Whether the load balancer should use a SecureRandom instead of a Random (default). Check https://stackoverflow.com/questions/11051205/difference-between-java-util-random-and-java-security-securerandom[this page] to understand the difference +|=== diff --git a/content/docs/smallrye-stork/2/_generated/static-sd-attributes.adoc b/content/docs/smallrye-stork/2/_generated/static-sd-attributes.adoc new file mode 100644 index 000000000..469ca6230 --- /dev/null +++ b/content/docs/smallrye-stork/2/_generated/static-sd-attributes.adoc @@ -0,0 +1,7 @@ +[cols=",,,",options="header",] +|=== +|Attribute |Mandatory |Default Value |Description +|`address-list` |Yes | |A comma-separated list of addresses (host:port). The default port is 80. +|`secure` |No | |Whether the connection with the service should be encrypted with TLS. Default is false, except if the host:port uses the port is 443. +|`shuffle` |No |`false` |Whether the list of address must be shuffled to avoid using the first address on every startup. +|=== diff --git a/content/docs/smallrye-stork/2/_generated/static-sr-attributes.adoc b/content/docs/smallrye-stork/2/_generated/static-sr-attributes.adoc new file mode 100644 index 000000000..8ec570617 --- /dev/null +++ b/content/docs/smallrye-stork/2/_generated/static-sr-attributes.adoc @@ -0,0 +1,5 @@ +[cols=",,,",options="header",] +|=== +|Attribute |Mandatory |Default Value |Description +|`address-list` |Yes | |A comma-separated list of addresses (host:port). The default port is 80. +|=== diff --git a/content/docs/smallrye-stork/2/_generated/sticky-lb-attributes.adoc b/content/docs/smallrye-stork/2/_generated/sticky-lb-attributes.adoc new file mode 100644 index 000000000..e458f6468 --- /dev/null +++ b/content/docs/smallrye-stork/2/_generated/sticky-lb-attributes.adoc @@ -0,0 +1,5 @@ +[cols=",,,",options="header",] +|=== +|Attribute |Mandatory |Default Value |Description +|`failure-backoff-time` |No |`0` |After how much time, a service instance that has failed can be reused. +|=== diff --git a/content/docs/smallrye-stork/2/_nav.yml b/content/docs/smallrye-stork/2/_nav.yml new file mode 100644 index 000000000..8975e7eed --- /dev/null +++ b/content/docs/smallrye-stork/2/_nav.yml @@ -0,0 +1,68 @@ +title: SmallRye Stork +nav: +- title: Overview + page: index +- title: Concepts + page: concepts +- title: Programmatic API + page: programmatic-api +- title: Microprofile Configuration + page: microprofile-config +- title: Spring Boot Configuration + page: springboot-config +- title: Observability + page: observability +- title: Javadoc + url: https://javadoc.io/doc/io.smallrye.stork/smallrye-stork-api/latest/index.html +- title: Using Stork with Quarkus + page: quarkus +- title: Service Discovery + items: + - title: Overview + page: service-discovery/overview + - title: Consul + page: service-discovery/consul + - title: DNS + page: service-discovery/dns + - title: Kubernetes + page: service-discovery/kubernetes + - title: KNative + page: service-discovery/knative + - title: Eureka + page: service-discovery/eureka + - title: Composite + page: service-discovery/composite + - title: Static List + page: service-discovery/static-list + - title: Custom Service Discovery + page: service-discovery/custom-service-discovery +- title: Load-Balancing + items: + - title: Overview + page: load-balancer/overview + - title: Round Robin + page: load-balancer/round-robin + - title: Random + page: load-balancer/random + - title: Least Requests + page: load-balancer/least-requests + - title: Least Response Time + page: load-balancer/least-response-time + - title: Power Of Two Choices + page: load-balancer/power-of-two-choices + - title: Sticky + page: load-balancer/sticky + - title: Custom Load Balancer + page: load-balancer/custom-load-balancer +- title: Service Registration + items: + - title: Overview + page: service-registration/overview + - title: Consul + page: service-registration/consul + - title: Eureka + page: service-registration/eureka + - title: Static List + page: service-registration/static-list + - title: Custom Service Registration + page: service-registration/custom-service-registration diff --git a/content/docs/smallrye-stork/2/concepts.adoc b/content/docs/smallrye-stork/2/concepts.adoc new file mode 100644 index 000000000..1618802c1 --- /dev/null +++ b/content/docs/smallrye-stork/2/concepts.adoc @@ -0,0 +1,108 @@ += Concepts +include::_attributes.adoc[] + +This page presents the concepts used in Stork. +When using Stork in a _managed_ environment, such as Quarkus, all these concepts are hidden, as you only configure the lookup and selection. +However, when using the programmatic API, you will use these concepts directly. + +image::images/diagrams/plantuml-383c9b353dda.svg[stork] + +== Process overview + +When using the programmatic API of Stork, you can: + +[arabic] +. Retrieve the _singleton_ `Stork` instance. This instance is configured with the set of `Service` it manages. +. Retrieve the `Service` you want to use. Each `Service` is associated with a name. +. Retrieve the `ServiceInstance` which will provide the metadata to access the actual service. + +image::images/diagrams/plantuml-a0003ba546d7.svg[sequence] + +Behind the scenes, Stork will handle the service lookup and selection. + +[NOTE] +==== + +The service lookup and selection are asynchronous operations. +Thus, the API returns instances of `Uni`. + +==== + +== Stork + +`io.smallrye.stork.Stork` is the entry-point of the API. +The `Stork` instance is a _singleton_. +It needs to be initialized once (when the application starts) and shutdown when the application stops: + +[source,java] +---- +include::{examples-dir}/examples/StorkEntryPointExample.java[indent=0] +---- + +During the initialization, Stork looks for `io.smallrye.stork.config.ConfigProvider` SPI provider and CDI beans (from 2.x version) and retrieves the list of managed services: + +* A service is identified by a *name*. +* A service has a service discovery configuration indicating how Stork will look for service instances +* A service can have a load-balancer configuration indicating how Stork can select the most appropriate instance. + +== Service + +A `io.smallrye.stork.Service` is the structure representing a _service_ used by the application. +Services are pre-configured with their name, service discovery, and optionally, their load-balancer. +You retrieve a `Service` using the `Stork#getService(String name)` method. + +[source,java] +---- +include::{examples-dir}/examples/StorkServiceExample.java[indent=0] +---- + +The `Service` lets you retrieve the list of `ServiceInstance`, or select a single one, when a load-balancer is configured. + +== Service Instance + +The `io.smallrye.stork.api.ServiceInstance` represents an actual instance of the service. +It provides the metadata to configure a _client_ to interact with that specific instance of service. + +[source,java] +---- +include::{examples-dir}/examples/StorkServiceLookupExample.java[indent=0] +---- + +The service selection is a two-steps process: + +[arabic] +. Service lookup - using the service discovery +. Service selection - using the load balancer + +[source,java] +---- +include::{examples-dir}/examples/StorkServiceSelectionExample.java[indent=0] +---- + +== Service Discovery + +The `io.smallrye.stork.api.ServiceDiscovery` represents a service discovery mechanism, such as DNS, Consul, or Eureka. + +You can implement a custom service discovery for Stork by implementing the `ServiceDiscoveryProvider` interface. +The corresponding `ServiceRegistrarProviderLoader` and `RegistrarConfiguration` classes will be automatically generated during compilation time. + +Please note that the `ServiceDiscovery` implementation must be non-blocking. + +== Load Balancer + +The `io.smallrye.stork.api.LoadBalancer` represents a load-balancer strategy, such as round-robin. + +To implement a custom load balancer for Stork, implement the `LoadBalancerProvider` interface. +The corresponding `LoadBalancerProviderLoader` and `Configuration` classes will be automatically generated during compilation time. + +Please note that the `LoadBalancer` implementation, similarly to `ServiceDiscovery` +must be non-blocking. + +== Service registration + +The `io.smallrye.stork.api.ServiceRegistrar` represents a service registration mechanism for Consul and Eureka. + +You can implement a custom service registrar for Stork by implementing the `ServiceRegistrarProvider` interface. +The corresponding `ServiceRegistrarProviderLoader` and `RegistrarConfiguration` classes will be automatically generated during compilation time. + +Please note that the `ServiceRegistrar` implementation must be non-blocking. diff --git a/content/docs/smallrye-stork/2/images/diagrams/plantuml-383c9b353dda.svg b/content/docs/smallrye-stork/2/images/diagrams/plantuml-383c9b353dda.svg new file mode 100644 index 000000000..0e4b865f3 --- /dev/null +++ b/content/docs/smallrye-stork/2/images/diagrams/plantuml-383c9b353dda.svg @@ -0,0 +1 @@ +«entry-point»StorkServiceDiscoveryServiceInstanceLoadBalancer«named»ServiceKubernetesConsulCustom Service Discovery«default»RoundRobinLeastResponseTimeCustom Load Balancer*selectServiceInstance \ No newline at end of file diff --git a/content/docs/smallrye-stork/2/images/diagrams/plantuml-3c6c7f0192c2.svg b/content/docs/smallrye-stork/2/images/diagrams/plantuml-3c6c7f0192c2.svg new file mode 100644 index 000000000..59018984f --- /dev/null +++ b/content/docs/smallrye-stork/2/images/diagrams/plantuml-3c6c7f0192c2.svg @@ -0,0 +1 @@ +ApplicationApplicationObservableStorkInfrastructureObservableStorkInfrastructureObservationCollectorObservationCollectorStorkStorkServiceServiceApplicationObservableStorkInfrastructureObservationCollectorStorkServiceApplicationObservableStorkInfrastructureObservationCollectorStorkService(1)instantiates(2)instantiates(3)ObservationCollector(4)ObservableStorkInfrastructure (5)initialize(observableInfrastructure)(6)instantiates (..., ObservationCollector, ...) \ No newline at end of file diff --git a/content/docs/smallrye-stork/2/images/diagrams/plantuml-a0003ba546d7.svg b/content/docs/smallrye-stork/2/images/diagrams/plantuml-a0003ba546d7.svg new file mode 100644 index 000000000..9c1a99915 --- /dev/null +++ b/content/docs/smallrye-stork/2/images/diagrams/plantuml-a0003ba546d7.svg @@ -0,0 +1 @@ +ApplicationApplicationApplicationStorkStorkStorkConfigProviderConfigProviderConfigProviderServiceServiceServiceService DiscoveryService DiscoveryService DiscoveryLoad BalancerLoad BalancerLoad BalancerApplicationStorkConfigProviderServiceService DiscoveryLoad BalancerApplicationStorkConfigProviderServiceService DiscoveryLoad Balancer(1)initialize()(2)getConfig()(3): List<ServiceConfig> (4)getService("name")(5)Service  (6)selectServiceInstance()(7)getServiceInstances()(8)Uni<List<ServiceInstance>>(9)selectServiceInstance()(10)Uni<ServiceInstance>(11)Uni<ServiceInstance> \ No newline at end of file diff --git a/content/docs/smallrye-stork/2/images/diagrams/plantuml-bc915c97a9fa.svg b/content/docs/smallrye-stork/2/images/diagrams/plantuml-bc915c97a9fa.svg new file mode 100644 index 000000000..39d2fba6a --- /dev/null +++ b/content/docs/smallrye-stork/2/images/diagrams/plantuml-bc915c97a9fa.svg @@ -0,0 +1 @@ +ApplicationStorkDNS ServerApplicationStorkDNS ServerApplicationStorkDNS Serverget service instancesget SRV records for hostnamelist of SRV recordslist of SRV record targets if no resolution desiredget A/AAAA records for each SRV record targetlist of A/AAAAlist of ServiceInstances \ No newline at end of file diff --git a/content/docs/smallrye-stork/2/images/diagrams/plantuml-c8137b2f4bb7.svg b/content/docs/smallrye-stork/2/images/diagrams/plantuml-c8137b2f4bb7.svg new file mode 100644 index 000000000..c2ebac7a1 --- /dev/null +++ b/content/docs/smallrye-stork/2/images/diagrams/plantuml-c8137b2f4bb7.svg @@ -0,0 +1 @@ +ServiceServiceServiceServiceServiceObservationCollectorObservationCollectorObservationCollectorObservationCollectorObservationCollectorStorkObservationStorkObservationStorkObservationStorkObservationStorkObservationStorkEventHandlerStorkEventHandlerStorkEventHandlerStorkEventHandlerStorkEventHandlerServiceObservationCollectorStorkObservationStorkEventHandlerServiceObservationCollectorStorkObservationStorkEventHandler(1)create("serviceName", sd type, ss type)(2)instantiates(3)Registers start time(4)StorkObservation (5)onServiceDiscoverySuccess(List<ServiceInstance>)(6)Registers end servicediscovery time.Registers instances count  (7)onServiceDiscoveryFailure(Throwable)(8)Registers end servicediscovery time.Registers failure cause  (9)onServiceSelectionSuccess(instanceId)(10)Registers end serviceselection time.Registers instance idRegisters overall duration (11)complete(this) (12)onServiceSelectionFailure(Throwable))(13)Registers overall duration.Registers failure cause (14)complete(this) \ No newline at end of file diff --git a/content/docs/smallrye-stork/2/images/problem-dark.png b/content/docs/smallrye-stork/2/images/problem-dark.png new file mode 100644 index 000000000..4c601386a Binary files /dev/null and b/content/docs/smallrye-stork/2/images/problem-dark.png differ diff --git a/content/docs/smallrye-stork/2/images/problem-light.png b/content/docs/smallrye-stork/2/images/problem-light.png new file mode 100644 index 000000000..cc0cff154 Binary files /dev/null and b/content/docs/smallrye-stork/2/images/problem-light.png differ diff --git a/content/docs/smallrye-stork/2/images/redhat_reversed.svg b/content/docs/smallrye-stork/2/images/redhat_reversed.svg new file mode 100644 index 000000000..37fe83c48 --- /dev/null +++ b/content/docs/smallrye-stork/2/images/redhat_reversed.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/content/docs/smallrye-stork/2/images/solution-dark.png b/content/docs/smallrye-stork/2/images/solution-dark.png new file mode 100644 index 000000000..800d0f978 Binary files /dev/null and b/content/docs/smallrye-stork/2/images/solution-dark.png differ diff --git a/content/docs/smallrye-stork/2/images/solution-light.png b/content/docs/smallrye-stork/2/images/solution-light.png new file mode 100644 index 000000000..044bd3786 Binary files /dev/null and b/content/docs/smallrye-stork/2/images/solution-light.png differ diff --git a/content/docs/smallrye-stork/2/images/stork-white.png b/content/docs/smallrye-stork/2/images/stork-white.png new file mode 100644 index 000000000..8ceae5659 Binary files /dev/null and b/content/docs/smallrye-stork/2/images/stork-white.png differ diff --git a/content/docs/smallrye-stork/2/index.adoc b/content/docs/smallrye-stork/2/index.adoc new file mode 100644 index 000000000..094027edf --- /dev/null +++ b/content/docs/smallrye-stork/2/index.adoc @@ -0,0 +1,45 @@ += Overview +include::_attributes.adoc[] + +SmallRye Stork `{project-version}` is a service discovery and client-side load-balancing framework. + +The essence of distributed systems resides in the interaction between _services_. +In modern architecture, you often have multiple instances of your service to share the load or improve the resilience by redundancy. +But, how do you select the _best_ instance of your service? +That's where Stork helps. +Stork is going to select the most appropriate instance. +It offers: + +* Extensible service discovery mechanisms +* Built-in support for Consul and Kubernetes +* Customizable client load-balancing strategies +* A programmatic API and a _managed_ approach +* A Quarkus integration, but Stork can be used in any environment + +== The problem + +In distributed systems, applications typically need to call one another. +In a monolithic application, components invoke one another through language-level method or procedure calls. +In a traditional distributed system deployment, services run at fixed, well-known locations (schemes, hosts, and ports) and can call one another using HTTP/REST or some RPC mechanism. The service locations are often hardcoded in the application configuration. +Nevertheless, a modern distributed system or microservice-based application typically runs in virtualized or containerized environments where the number of instances of a service and their locations change dynamically. +IPs get randomly assigned, and instances can be created or destroyed at any time. +With such dynamics, hard-coded locations are a dead-end. + +.the problem +image::images/problem-light.png[the problem] + +== The solution + +Stork handles the service lookup and selection. +It proposes an extensible set of service discovery mechanisms and load-balancing strategies. + +.the solution +image::images/solution-light.png[the solution] + +== What if the infrastructure provides such a feature? + +Some infrastructure, such as Kubernetes, provides service discoveries and load-balancing features. +However, these mechanisms often lack flexibility. You cannot influence the service instance selection, and the load-balancing strategy is generally a simple _round-robin_. + +Stork provides more flexibility in the service instance selection. For example, it can select the fastest instance (based on the previous calls) to improve the response time. +However, if you don't need that flexibility, just use the infrastructure layer. diff --git a/content/docs/smallrye-stork/2/load-balancer/custom-load-balancer.adoc b/content/docs/smallrye-stork/2/load-balancer/custom-load-balancer.adoc new file mode 100644 index 000000000..c8d02933b --- /dev/null +++ b/content/docs/smallrye-stork/2/load-balancer/custom-load-balancer.adoc @@ -0,0 +1,145 @@ += Implement your own load balancer mechanism +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Stork is extensible, and you can implement your service selection (load-balancer) mechanism. + +== Dependencies + +To implement your _Load Balancer Provider_, make sure your project depends on Core and Configuration Generator. +The former brings classes necessary to implement custom load balancer, the latter contains an annotation processor that generates classes needed by Stork. + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-core + {project-version} + + + io.smallrye.stork + stork-configuration-generator + provided + + {project-version} + +---- + +== Implementing a load balancer provider + +Load balancer implementation consists of three elements: + +* `LoadBalancer` which is responsible for selecting service instances for a single Stork service, +* `LoadBalancerProvider` which creates instances of `LoadBalancer` for a given load balancer _type_, +* `$typeConfiguration` which is a configuration for the load balancer. This class is automatically generated. + +A _type_, for example `acme-load-balancer`, identifies each provider. +This _type_ is used in the configuration to reference the provider: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.load-balancer.type=acme-load-balancer +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.load-balancer.type=acme-load-balancer +---- + +-- + +==== + +A `LoadBalancerProvider` implementation needs to be annotated with `@LoadBalancerType` that defines the _type_. +Any configuration properties that the provider expects should be defined with `@LoadBalancerAttribute` annotations placed on the provider. +Optionally, you can also add `@ApplicationScoped` annotation in order to provide the load balancer implementation as CDI bean. + +A load balancer provider class should look as follows: + +[source,java] +---- +include::{examples-dir}/examples/AcmeLoadBalancerProvider.java[indent=0] +---- + +Note, that the `LoadBalancerProvider` interface takes a configuration class as a parameter. +This configuration class is generated automatically by the _Configuration Generator_. +Its name is created by appending `Configuration` to the load balancer type, like `AcmeLoadBalancerConfiguration`. + +The next step is to implement the `LoadBalancer` interface. + +The essence of load balancers' work happens in the `selectServiceInstance` method. The method returns a single `ServiceInstance` from a collection. + +[source,java] +---- +include::{examples-dir}/examples/AcmeLoadBalancer.java[indent=0] +---- + +This implementation is simplistic and just picks a random instance from the received list. + +Some load balancers make the pick based on statistics such as calls in progress or response times, or amount of errors of a service instance. +To collect this information in your load balancer, you can wrap the selected service instance into `ServiceInstanceWithStatGathering`. + +Load balancers based on statistics often expect that an operation using a selected service instance is marked as started before the next selection. +By default, Stork assumes that a `LoadBalancer` requires this and guards the calls accordingly. +If this is not the case for your implementation, override the `requiresStrictRecording()` method to return `false`. + +== Using your load balancer + +In the project using it, don't forget to add the dependency on the module providing your implementation. +Then, in the configuration, just add: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-discovery.type=... +stork.my-service.load-balancer.type=acme-load-balancer\ +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.type=... +quarkus.stork.my-service.load-balancer.type=acme-load-balancer +---- + +-- + +==== + +Then, Stork will use your implementation to select the `my-service` service instance. + +== Using your load balancer using the programmatic API + +When building your load balancer project, the configuration generator creates a configuration class. +This class can be used to configure your load balancer using the Stork programmatic API. + +[source,java] +---- +include::{examples-dir}/examples/AcmeSelectorApiUsage.java[indent=0] +---- + +Remember that attributes, like `my-attribute`, are declared using the `@LoadBalancerAttribute` annotation on the `LoadBalancerProvider` implementation. diff --git a/content/docs/smallrye-stork/2/load-balancer/least-requests.adoc b/content/docs/smallrye-stork/2/load-balancer/least-requests.adoc new file mode 100644 index 000000000..6a8babbc6 --- /dev/null +++ b/content/docs/smallrye-stork/2/load-balancer/least-requests.adoc @@ -0,0 +1,59 @@ += Least Requests Load Balancing +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The `least-requests` load balancing strategy monitors the number of inflight calls and selects the less-used instance. + +This strategy keeps track of the inflight calls made by the application and picks the service instance with the smallest number of inflight requests: + +[arabic] +. when the selection happens, the service instance with the smallest number of inflight requests is selected, and this number is incremented +. when the operation completes, successfully or not, the number of inflight requests is decremented + +== Dependency + +First, you need to add the `least-requests` load-balancer to your project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-load-balancer-least-requests + {project-version} + +---- + +== Configuration + +For each service expected to use a least-response-time selection, configure the `load-balancer` to be `least-requests`: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.load-balancer.type=least-requests +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.load-balancer.type=least-requests +---- + +-- + +==== + +Supported configuration properties are the following: + +include::{generated-dir}/least-requests-lb-attributes.adoc[] diff --git a/content/docs/smallrye-stork/2/load-balancer/least-response-time.adoc b/content/docs/smallrye-stork/2/load-balancer/least-response-time.adoc new file mode 100644 index 000000000..9742a0b23 --- /dev/null +++ b/content/docs/smallrye-stork/2/load-balancer/least-response-time.adoc @@ -0,0 +1,94 @@ += Least Response Time Load Balancing +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The `least-response-time` load balancing strategy collects response times of the calls made with service instances and picks an instance based on this information. + +Erroneous responses are treated as responses with a long response time, by default 60 seconds. This can be controlled with the `error-penalty` attribute. + +The algorithm for service instance selection is as follows: + +* if there is a service instance that wasn't used before - use it, otherwise: +* if there are any service instances with collected response times - select the one for which _score_ is the lowest, otherwise: +* select a random instance + +The _score_ for an instance decreases in time if an instance is not used. This way we ensure that instances that haven't been used in a long time, are retried. + +For the details on the _score_ calculation, see xref:#score-calculation[Score calculation] + +== Dependency + +To use this load balancer, start with adding the least-response-time load-balancer dependency to your project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-load-balancer-least-response-time + {project-version} + +---- + +== Configuration + +For each service expected to use a least-response-time selection, configure the `load-balancer` to be `least-response-time`: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.load-balancer.type=least-response-time +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.load-balancer.type=least-response-time +---- + +-- + +==== + +The following attributes are supported: + +include::{generated-dir}/least-response-time-lb-attributes.adoc[] + +== Score calculation + +The _score_ of a service instance is calculated by dividing a weighted sum of response times by sum of the weighs. The result is additionally adjusted to account for instances that haven't been used for a long time. + +Let: + +* $n$ denote how many instance selections were made so far +* $t++_++i$ denote the response time for call $i$ +* $n++_++i$ denote the number of instance selections done until the moment of recording the response time for call $i$ +* $n++_{++max}$ denote the number of instance selections at the moment of last call recorded with this instance +* $$ denote a configurable `declining-factor` + +The idea for the weight is to decrease the importance of the data collected long time (many calls) ago. For call $i$, the weight is calculated as follows: +$$ +w++_++i = ^ ++{++(n - n++_++i)} +$$ + +The score of a service instance is calculated as: +$$ +score(n) = ^++{++n - n++_{++max}} ++*++ = +^++{++n - n++_{++max}} ++*++ +$$ + +The `declining-factor` should be in $(0, 1++]++$ , the default is $0.9$. Using a lower value makes the older response times less important. + +Supported configuration properties are the following: + +include::{generated-dir}/least-response-time-lb-attributes.adoc[] diff --git a/content/docs/smallrye-stork/2/load-balancer/overview.adoc b/content/docs/smallrye-stork/2/load-balancer/overview.adoc new file mode 100644 index 000000000..62c2f482c --- /dev/null +++ b/content/docs/smallrye-stork/2/load-balancer/overview.adoc @@ -0,0 +1,22 @@ += Overview +:version-root: ../ +include::{version-root}_attributes.adoc[] + +== Load Balancer / Service Selection in SmallRye Stork + +Once services are registered and discovered, the next critical step is selecting which service instance will handle a given request. +SmallRye Stork provides flexible load balancing strategies to efficiently distribute requests across multiple instances of a service. +This ensures optimal resource usage, improved performance, and high availability. + +=== Key Features: + +* *Multiple Load Balancing Strategies*: SmallRye Stork supports several built-in strategies for selecting service instances. +Check them out in the following dedicated sections. +* *Customizable Strategies*: You can define custom service selection strategies based on your unique use case or performance requirements, ensuring that the load balancer can adapt to specific needs. + +==== How it Works: + +Once a service has been registered and discovered, the load balancer comes into play when a client makes a request to that service. +Stork applies the configured load balancing strategy to select an instance from the available pool of discovered services. + +This feature ensures that your services remain responsive, scalable, and resilient, providing a smooth experience for both users and developers. diff --git a/content/docs/smallrye-stork/2/load-balancer/power-of-two-choices.adoc b/content/docs/smallrye-stork/2/load-balancer/power-of-two-choices.adoc new file mode 100644 index 000000000..9e73e9897 --- /dev/null +++ b/content/docs/smallrye-stork/2/load-balancer/power-of-two-choices.adoc @@ -0,0 +1,63 @@ += Power Of Two Choices Load Balancing +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The `power-of-two-choices` load balancing selects two random service instances and then chooses the one with the least inflight requests. +It avoids the overhead of `least-requests` and the worst case for `random` where it selects a busy destination. + +This strategy acts as follows: + +[arabic] +. when the selection happens, it picks two random instances from the list, +. it returns the least loaded instance (based on the number of inflight requests), +. when the operation completes, successfully or not, the number of inflight requests for the instance is decremented. + +Check http://www.eecs.harvard.edu/~michaelm/NEWWORK/postscripts/twosurvey.pdf[The Power of Two Random Choices] paper to learn more about this pattern and the benefits. + +== Dependency + +First, you need to add the random load-balancer to your project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-load-balancer-power-of-two-choices + {project-version} + +---- + +== Configuration + +For each service expected to use a random service selection, configure the `load-balancer` to be `power-of-two-choices`: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.load-balancer.type=power-of-two-choices +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.load-balancer.type=power-of-two-choices +---- + +-- + +==== + +Supported configuration properties are the following: + +include::{generated-dir}/power-of-two-choices-lb-attributes.adoc[] diff --git a/content/docs/smallrye-stork/2/load-balancer/random.adoc b/content/docs/smallrye-stork/2/load-balancer/random.adoc new file mode 100644 index 000000000..f721585d7 --- /dev/null +++ b/content/docs/smallrye-stork/2/load-balancer/random.adoc @@ -0,0 +1,53 @@ += Random Load Balancing +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The `random` load balancing is a straightforward service instance selection solution that picks a random instance every time. + +== Dependency + +First, you need to add the random load-balancer to your project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-load-balancer-random + {project-version} + +---- + +== Configuration + +For each service expected to use a random service selection, configure the `load-balancer` to be `random`: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.load-balancer.type=random +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.load-balancer.type=random +---- + +-- + +==== + +Supported attributes are the following: + +include::{generated-dir}/random-lb-attributes.adoc[] diff --git a/content/docs/smallrye-stork/2/load-balancer/round-robin.adoc b/content/docs/smallrye-stork/2/load-balancer/round-robin.adoc new file mode 100644 index 000000000..e1f768f10 --- /dev/null +++ b/content/docs/smallrye-stork/2/load-balancer/round-robin.adoc @@ -0,0 +1,43 @@ += Round-Robin Load Balancing +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The round-robin is a straightforward load-balancing solution that just iterates over the set of service instances. +While being simple, this solution shares the load among the instances and may be sufficient in many cases. + +The round-robin strategy is the default load-balancing strategy. +It is provided by Stork itself, and so does not require an additional dependency. + +== Configuration + +There is no need to configure the load-balancing strategy to be `round-robin`. +Stork automatically uses this strategy when none are configured. + +However, you can also configure it explicitly as follows: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.load-balancer.type=round-robin +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.load-balancer.type=round-robin +---- + +-- + +==== diff --git a/content/docs/smallrye-stork/2/load-balancer/sticky.adoc b/content/docs/smallrye-stork/2/load-balancer/sticky.adoc new file mode 100644 index 000000000..8af192d26 --- /dev/null +++ b/content/docs/smallrye-stork/2/load-balancer/sticky.adoc @@ -0,0 +1,52 @@ += Sticky Load Balancing +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The sticky load balancer selects a single service instance and keeps using it until it fails. +Then, it selects another one. + +It is possible to configure a backoff time to specify for how long a failing service instance should not be retried. + +Precisely, the load balancer works as follows: + +* if no service instance has been selected so far, select the first instance from the collection; +* else if the previously selected service instance has not failed, and is still available, return it; +* else return the first available service instance that has no recorded failure, if one exists; +* else, find the available instance for which the time since the last failure is the longest, and +** if the backoff time since the failure passed, return it; +** or, throw an `NoAcceptableServiceInstanceFoundException` as no acceptable instances are available. + +== Configuration + +To use the `sticky` load service selection strategy, set the load balancer type to `sticky`: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.load-balancer.type=sticky +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.load-balancer.type=sticky +---- + +-- + +==== + +The following attributes are supported: + +include::{generated-dir}/sticky-lb-attributes.adoc[] diff --git a/content/docs/smallrye-stork/2/microprofile-config.adoc b/content/docs/smallrye-stork/2/microprofile-config.adoc new file mode 100644 index 000000000..48ba3a487 --- /dev/null +++ b/content/docs/smallrye-stork/2/microprofile-config.adoc @@ -0,0 +1,30 @@ += Stork MicroProfile Config +include::_attributes.adoc[] + +Stork integrates with MicroProfile Configuration out of the box, enabling seamless access to configuration properties. +This documentation explains how Stork can retrieve configuration details from the MicroProfile Config file present in the classpath. +Quarkus uses this approach for reading configuration details from the MicroProfile Config file located within the classpath. + +== Dependency setup + +To enable MicroProfile Config integration in Stork, you need to include the following dependency: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + smallrye-stork-microprofile + {project-version} + +---- + +== Initializing Stork + +If your framework lacks a pre-configured Stork instance, you'll need to perform initialization: + +[source,java] +---- +include::{examples-dir}/examples/InitializationExample.java[indent=0] +---- + +Upon initialization, Stork scans for the `io.smallrye.stork.config.MicroProfileConfigProvider` SPI provider and CDI beans (from version 2.x onwards). It then builds a comprehensive list of managed services by parsing the properties configuration files. diff --git a/content/docs/smallrye-stork/2/observability.adoc b/content/docs/smallrye-stork/2/observability.adoc new file mode 100644 index 000000000..3409d5a75 --- /dev/null +++ b/content/docs/smallrye-stork/2/observability.adoc @@ -0,0 +1,112 @@ += Stork Observability API +include::_attributes.adoc[] + +Stork proposes an observability API that automatically observes some parameters to show how the Stork service discovery and selection are behaving. + +For any _observation_ to happen, you need to provide your own implementation of an `ObservationCollector.` By default, Stork provides a no-op implementation. + +The `ObservationCollector` is responsible for instantiating the `StorkObservation`. + +The `StorkObservation` reacts to Stork events thanks to a `StorkEventHandler`. + +You can extend the metrics collection by extending the `StorkEventHandler` interface. + +The following sequence diagram shows how the observability is initialized : + +image::images/diagrams/plantuml-3c6c7f0192c2.svg[observability_sequence] + +The `StorkObservation` registers times, number of discovered instances, the selected instance and failures by reacting to the lifecycle of a Stork event such as: + +* start : Observation has been started. +The beginning time is registered. +It happens when the `ObservationCollector#create()` method gets called. +* service discovery success: a collection of instances has been successfully discovered for a service. +The end discovery time and number of instances are recorded. +It happens when the `StorkObservation#onServiceDiscoverySuccess` gets called. +* service discovery error: an error occurs when discovering a service. +The end discovery time and failure cause are captured. +It happens when the `StorkObservation#onServiceDiscoveryFailure` gets called. +* service selection success: an instance has been successfully selected from the collection. +The end selection time and selected instance ID are registered. +It happens when the `StorkObservation#onServiceSelectionSuccess` gets called. +* service selection error: an error occurred during selecting the instance. +End selection time and failure cause are registered. +It happens when the `StorkObservation#onServiceSelectionFailure` gets called. +* end: Observation has finished. Overall duration is registered. +It happens when the `StorkObservation#onServiceSelectionSuccess` gets called. + +The following sequence diagram represents the described observation process above: + +image::images/diagrams/plantuml-c8137b2f4bb7.svg[observation_sequence] + +== Implementing an observation collector + +An `ObservationCollector` implementation must override the `create` method to provide an instance of StorkObservation. +In addition, the user can access and enrich the observation data through the `StorkEventHandler`. + +A custom observation collector class should look as follows: + +[source,java] +---- +include::{examples-dir}/examples/AcmeObservationCollector.java[indent=0] +---- + +The next step is to initialize Stork with an `ObservableStorkInfrastructure`, taking an instance of your `ObservationCollector` as parameter. + +[source,java] +---- +include::{examples-dir}/examples/ObservableInitializationExample.java[indent=0] +---- + +Then, Stork uses your implementation to register metrics. + +== Observing service discovery and selection behaviours + +To access metrics registered by `StorkObservation`, use the following code: + +[source,java] +---- +include::{examples-dir}/examples/ObservationExample.java[indent=0] +---- + +Stork Observability with Quarkus + +Stork metrics are automatically enabled when using Stork together with the Micrometer extension in a Quarkus application. + +Micrometer collects the metrics of the rest and grpc client using Stork, as well as when using the Stork API. + +As an example, if you export the metrics to Prometheus, you will get: + +[source,text] +---- +# HELP stork_load_balancer_failures_total The number of failures during service selection. +# TYPE stork_load_balancer_failures_total counter +stork_load_balancer_failures_total{service_name="hello-service",} 0.0 +# HELP stork_service_selection_duration_seconds The duration of the selection operation +# TYPE stork_service_selection_duration_seconds summary +stork_service_selection_duration_seconds_count{service_name="hello-service",} 13.0 +stork_service_selection_duration_seconds_sum{service_name="hello-service",} 0.001049291 +# HELP stork_service_selection_duration_seconds_max The duration of the selection operation +# TYPE stork_service_selection_duration_seconds_max gauge +stork_service_selection_duration_seconds_max{service_name="hello-service",} 0.0 +# HELP stork_overall_duration_seconds_max The total duration of the Stork service discovery and selection operations +# TYPE stork_overall_duration_seconds_max gauge +stork_overall_duration_seconds_max{service_name="hello-service",} 0.0 +# HELP stork_overall_duration_seconds The total duration of the Stork service discovery and selection operations +# TYPE stork_overall_duration_seconds summary +stork_overall_duration_seconds_count{service_name="hello-service",} 13.0 +stork_overall_duration_seconds_sum{service_name="hello-service",} 0.001049291 +# HELP stork_service_discovery_failures_total The number of failures during service discovery +# TYPE stork_service_discovery_failures_total counter +stork_service_discovery_failures_total{service_name="hello-service",} 0.0 +# HELP stork_service_discovery_duration_seconds_max The duration of the discovery operation +# TYPE stork_service_discovery_duration_seconds_max gauge +stork_service_discovery_duration_seconds_max{service_name="hello-service",} 0.0 +# HELP stork_service_discovery_duration_seconds The duration of the discovery operation +# TYPE stork_service_discovery_duration_seconds summary +stork_service_discovery_duration_seconds_count{service_name="hello-service",} 13.0 +stork_service_discovery_duration_seconds_sum{service_name="hello-service",} 6.585046209 +# HELP stork_instances_count_total The number of service instances discovered +# TYPE stork_instances_count_total counter +stork_instances_count_total{service_name="hello-service",} 26.0 +---- diff --git a/content/docs/smallrye-stork/2/programmatic-api.adoc b/content/docs/smallrye-stork/2/programmatic-api.adoc new file mode 100644 index 000000000..32f17e23b --- /dev/null +++ b/content/docs/smallrye-stork/2/programmatic-api.adoc @@ -0,0 +1,54 @@ += Stork Programmatic API +include::_attributes.adoc[] + +Stork proposes a programmatic API that lets you register new service Definitions and do manual lookup and selection. +When using the programmatic API of Stork, you can: +Retrieve the singleton Stork instance. This instance is configured with the set of Services it manages. +Register new service definition. +Retrieve the Service you want to use. Each Service is associated with a name. +Retrieve the ServiceInstance, which will provide the metadata to access the actual instance. + +== Initializing Stork + +If your framework does not already provide a configured `Stork` instance, you need to do: + +[source,java] +---- +include::{examples-dir}/examples/InitializationExample.java[indent=0] +---- + +== Adding service dynamically + +To register a new `ServiceDefinition`, use the `defineIfAbsent` method: + +[source,java] +---- +include::{examples-dir}/examples/DefinitionExample.java[indent=0] +---- + +The `ServiceDefinition` instances can be created from: + +* A service discovery configuration - these classes are provided by the service discovery implementations, +* An optional load balancer configuration - these classes are provided by the load balancer implementations + +Attributes from the service discovery and load balancer can be configured from the `Configuration` classes. + +== Looking for service instances + +To list the service instances for a given service, or to select an instance according to the load balancer strategy, use the following code: + +[source,java] +---- +include::{examples-dir}/examples/LookupExample.java[indent=0] +---- + +The lookup and selection methods are returning Uni as these processes are asynchronous. + +== All in one example + +The following snippet provides an _all in one_ example of the Stork programmatic API: + +[source,java] +---- +include::{examples-dir}/examples/StorkApiExample.java[indent=0] +---- diff --git a/content/docs/smallrye-stork/2/quarkus.adoc b/content/docs/smallrye-stork/2/quarkus.adoc new file mode 100644 index 000000000..08c9b3186 --- /dev/null +++ b/content/docs/smallrye-stork/2/quarkus.adoc @@ -0,0 +1,79 @@ += Using Stork with Quarkus +include::_attributes.adoc[] + +https://quarkus.io[Quarkus] is a Kubernetes Native Java stack tailored for OpenJDK HotSpot and GraalVM. + +Quarkus REST Client Reactive and gRPC extensions come with built-in integration with SmallRye Stork. + +This page describes how to use REST Client Reactive with Stork. Using gRPC with Stork is similar. + +We will use the Consul service discovery and the round-robin load balancer as examples. + +== The project + +You can create a Quarkus project with the REST Client Reactive extension using https://code.quarkus.io[code.quarkus.io]. + +The corresponding https://quarkus.io/guides/rest-client-reactive[Quarkus guide] describes the extension in more detail. + +== The client + +To use the REST client to communicate with a remote endpoint, you need to create an interface that describes how the communication should work. +The client requires `baseUri` (or `baseUrl`) pointing to the address of the remote endpoint. + +To use Stork to determine the actual address, set the scheme of the URI to `stork` and the hostname of the URI to the name of the Stork service. + +For example, the `HelloClient` below will use the Stork service called `hello-service` to determine the address of the destination, and `/hello` as the base path for queries: + +[source,java] +---- +include::{examples-dir}/examples/HelloClient.java[indent=0] +---- + +== The service + +In Stork, a `Service` consists of service discovery and a load balancer. +The Service discovery is responsible for determining the `ServiceInstance`s, that is, available addresses for a service. +The load balancer picks a single `ServiceInstance` for a call. + +=== Dependencies + +To use the service discovery and the load balancer of your choosing, you need to add the appropriate dependencies to your application. +For example, if you wish to use Consul and load-balance the calls with round-robin, add the following to your `pom.xml`: + +[source,xml] +---- + + io.smallrye.stork + stork-service-discovery-consul + +---- + +[NOTE] +==== + +No need to add a dependency for the round-robin load-balancer. This strategy is built-in. +Also, no need to specify the stork version, Quarkus imports the Stork _BOM_. + +==== + +When using Eureka, Kubernetes, or any other service discovery mechanism, or a different load balancer, replace the dependencies above with the ones you need. +Based on the defined dependencies, Stork automatically registers providers for service discovery mechanisms and load balancers. + +=== The config + +The last piece of the puzzle is the actual service configuration. +If your Consul instance is running on `localhost` on port `8500`, service discovery configuration should look as follows: + +[source,properties] +---- +quarkus.stork.hello-service.service-discovery.type=consul +quarkus.stork.hello-service.service-discovery.consul-host=localhost +quarkus.stork.hello-service.service-discovery.consul-port=8500 +---- + +[NOTE] +==== + +When no load-balancing is configured, Stork uses a round-robin. + +==== diff --git a/content/docs/smallrye-stork/2/service-discovery/composite.adoc b/content/docs/smallrye-stork/2/service-discovery/composite.adoc new file mode 100644 index 000000000..7be362c60 --- /dev/null +++ b/content/docs/smallrye-stork/2/service-discovery/composite.adoc @@ -0,0 +1,71 @@ += Composite Service Discovery +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Some organizations use hybrid infrastructure. In such an infrastructure, different service instances may be discoverable via different service discovery providers. E.g. when _serviceA_ is being migrated from a VM to Kubernetes, its older versions can be discovered via Consul and newer via Kubernetes. + +`composite` service discovery addresses this problem by letting you define a service that consists of multiple services. + +== Dependency + +To use the `composite` service discovery, first add the appropriate Service Discovery provider dependency to your project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-service-discovery-composite + {project-version} + +---- + +== Configuration + +For each service that should consist of multiple services, configure the service discovery `type`, and set the `services` property to a comma separated list of services: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-discovery.type=composite +stork.my-service.service-discovery.services=serviceA,serviceB + +stork.serviceA.service-discovery.type=... +stork.serviceB.service-discovery.type=... +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.type=composite +quarkus.stork.my-service.service-discovery.services=serviceA,serviceB + +quarkus.stork.serviceA.service-discovery.type=... +quarkus.stork.serviceB.service-discovery.type=... +---- + +-- + +==== + +Remember to define the services that make up your composite service. + +Be aware that Stork doesn't work as a standalone service discovery cluster. +Instead, it processes composite configurations—meaning it handles multiple configurations that may use different service discovery implementation. +For each specific configuration, Stork delegates the service discovery task to the appropriate service discovery provider. +If one of these providers doesn't respond or fails, it affects Stork's ability to resolve that specific configuration, but it doesn't mean Stork itself is faulty; +it relies on the performance of the service discovery systems it’s configured to work with. + +These are all the parameters of the composite service discovery: + +include::{generated-dir}/composite-sd-attributes.adoc[] diff --git a/content/docs/smallrye-stork/2/service-discovery/consul.adoc b/content/docs/smallrye-stork/2/service-discovery/consul.adoc new file mode 100644 index 000000000..210076247 --- /dev/null +++ b/content/docs/smallrye-stork/2/service-discovery/consul.adoc @@ -0,0 +1,167 @@ += Consul Service Discovery +:version-root: ../ +include::{version-root}_attributes.adoc[] + +https://www.consul.io/[Consul] is a distributed, highly available, and data center aware solution to connect and configure applications across dynamic, distributed infrastructure. +It's often used as service discovery backend to register and locate the services composing your system. +Consul makes it simple for services to register themselves and to discover other services via a DNS or HTTP interface. +External services can be registered as well. + +As https://developer.hashicorp.com/consul/api-docs/agent/service#address[specified] in the Consul documentation, if the host address is not provided, Stork will automatically use the Consul node address for the instance. + +This page explains how Stork can use Consul to handle the service discovery and service registration. + +== Dependency + +First, you need to add the Stork Consul Service Discovery provider: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-service-discovery-consul + {project-version} + +---- + +=== Service discovery configuration + +For each service that should get the service instances from Consul, configure the service discovery `type`: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-discovery.type=consul +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.type=consul +---- + +-- + +==== + +== Secure communication with Consul + +Stork supports SSL/TLS encryption when connecting to a Consul server. This is useful when Consul is configured with HTTPS or when you need to authenticate using client certificates. + +To enable SSL/TLS, set the `ssl` attribute to `true` and configure the trust store and optionally the key store: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-discovery.type=consul +stork.my-service.service-discovery.consul-host=consul.example.com +stork.my-service.service-discovery.consul-port=8501 +stork.my-service.service-discovery.ssl=true +stork.my-service.service-discovery.trust-store-path=/path/to/truststore.jks +stork.my-service.service-discovery.trust-store-password=changeit +stork.my-service.service-discovery.verify-host=true +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.type=consul +quarkus.stork.my-service.service-discovery.consul-host=consul.example.com +quarkus.stork.my-service.service-discovery.consul-port=8501 +quarkus.stork.my-service.service-discovery.ssl=true +quarkus.stork.my-service.service-discovery.trust-store-path=/path/to/truststore.jks +quarkus.stork.my-service.service-discovery.trust-store-password=changeit +quarkus.stork.my-service.service-discovery.verify-host=true +---- + +-- + +==== + +For mutual TLS (mTLS) authentication, also configure the key store containing the client certificate: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-discovery.key-store-path=/path/to/keystore.jks +stork.my-service.service-discovery.key-store-password=changeit +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.key-store-path=/path/to/keystore.jks +quarkus.stork.my-service.service-discovery.key-store-password=changeit +---- + +-- + +==== + +== ACL Token Authentication + +If your Consul cluster has ACL enabled, you can provide an ACL token for authentication: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-discovery.acl-token=your-consul-acl-token +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.acl-token=your-consul-acl-token +---- + +-- + +==== + +Consul service discovery is configured with the following parameters: + +include::{generated-dir}/consul-sd-attributes.adoc[] diff --git a/content/docs/smallrye-stork/2/service-discovery/custom-service-discovery.adoc b/content/docs/smallrye-stork/2/service-discovery/custom-service-discovery.adoc new file mode 100644 index 000000000..04945b53b --- /dev/null +++ b/content/docs/smallrye-stork/2/service-discovery/custom-service-discovery.adoc @@ -0,0 +1,202 @@ += Implement your own service discovery mechanism +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Stork is extensible, and you can implement your own service discovery mechanism. + +== Dependencies + +To implement your _Service Discovery Provider_, make sure your project depends on Core and Configuration Generator. +The former brings classes necessary to implement custom discovery, the latter contains an https://docs.oracle.com/en/java/javase/11/docs/api/java.compiler/javax/annotation/processing/Processor.html[annotation processor] that generates classes needed by Stork. + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-core + {project-version} + + + io.smallrye.stork + stork-configuration-generator + provided + + {project-version} + +---- + +== Implementing a service discovery provider + +Service discovery implementation consists of three elements: + +* `ServiceDiscovery` which is responsible for locating service instances for a single Stork service. +* `ServiceDiscoveryProvider` which creates instances of `ServiceDiscovery` for a given service discovery _type_. +* `$typeConfiguration` which is a configuration for the discovery. This class is automatically generated during the compilation (using an annotation processor). + +A _type_, for example, `acme`, identifies each provider. +This _type_ is used in the configuration to reference the provider: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-discovery.type=acme +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.type=acme +---- + +-- + +==== + +A `ServiceDiscoveryProvider` implementation needs to be annotated with `@ServiceDiscoveryType` that defines the _type_. +Any configuration properties that the provider expects should be defined with `@ServiceDiscoveryAttribute` annotations placed on the provider. +Optionally, you can also add `@ApplicationScoped` annotation in order to provide the service discovery implementation as CDI bean. + +A service discovery provider class should look as follows: + +[source,java] +---- +include::{examples-dir}/examples/AcmeServiceDiscoveryProvider.java[indent=0] +---- + +Note, that the `ServiceDiscoveryProvider` interface takes a configuration class as a parameter. This configuration class +is generated automatically by the _Configuration Generator_. +Its name is created by appending `Configuration` to the service discovery type, such as `AcmeConfiguration`. + +The next step is to implement the `ServiceDiscovery` interface: + +[source,java] +---- +include::{examples-dir}/examples/AcmeServiceDiscovery.java[indent=0] +---- + +This implementation is simplistic. +Typically, instead of creating a service instance with values from the configuration, you would connect to a service discovery backend, look for the service and build the list of service instance accordingly. +That's why the method returns a `Uni`. +Most of the time, the lookup is a remote operation. + +As you can see, the `AcmeConfiguration` class gives access to the configuration attribute. + +== Using your service discovery + +In the project using it, don't forget to add the dependency on the module providing your implementation. +Then, in the configuration, just add: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-discovery.type=acme +stork.my-service.service-discovery.host=localhost +stork.my-service.service-discovery.port=1234 +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.type=acme +quarkus.stork.my-service.service-discovery.host=localhost +quarkus.stork.my-service.service-discovery.port=1234 +---- + +-- + +==== + +Then, Stork will use your implementation to locate the `my-service` service. + +== Using your service discovery using the programmatic API + +When building your service discovery project, the configuration generator creates a configuration class. +This class can be used to configure your service discovery using the Stork programmatic API. + +[source,java] +---- +include::{examples-dir}/examples/AcmeDiscoveryApiUsage.java[indent=0] +---- + +Remember that attributes, like `host`, are declared using the `@ServiceDiscoveryAttribute` annotation on the `ServiceDiscoveryProvider` implementation. + +== Caching the service instances + +Your `ServiceDiscovery` implementation can extend `io.smallrye.stork.impl.CachingServiceDiscovery` to automatically _cache_ the service instances. +In this case, the retrieved set of `ServiceInstance` is cached and only updated after some time. +This duration is an additional configuration attribute. +For homogeneity, we recommend the following attribute: + +[source,java] +---- +@ServiceDiscoveryAttribute(name = "refresh-period", description = "Service discovery cache refresh period.", + defaultValue = CachingServiceDiscovery.DEFAULT_REFRESH_INTERVAL) +---- + +The following snippet extends the _acme_ service discovery with the `refresh-period` attribute: + +[source,java] +---- +include::{examples-dir}/examples/CachedAcmeServiceDiscoveryProvider.java[indent=0] +---- + +Extending `io.smallrye.stork.impl.CachingServiceDiscovery` changes the structure of the service discovery implementation: + +[source,java] +---- +include::{examples-dir}/examples/CachedAcmeServiceDiscovery.java[indent=0] +---- + +[arabic] +. Call the `super` constructor with the `refresh-period` value +. Implement `fetchNewServiceInstances` instead of `getServiceInstances`. +The method is called periodically, and the retrieved instances are cached. +This implementation is simplistic. + +If the retrieval fails, the error is reported, and Stork keeps the previously retrieved list of instances. + +=== Customizing the caching strategy + +Sometimes it can be useful to change this behaviour and customize the cache expiration strategy. + +For example, imagine you are using a backend service discovery where service instances can change very frequently. + +Moreover, contacting the backend service discovery can be expensive in terms of computing, +thus finding a good value for the refreshing time can be mission impossible. + +For these situations, Stork allows to implement a better expiration strategy for the cache. + +If you want to customize the expiration strategy, you need: +1. Implement the `cache` method where the expiration strategy should be defined. +2. Invalidate the cache when expiration condition evaluates to true. + +Look at the example bellow: + +[source,java] +---- +include::{examples-dir}/examples/CustomExpirationCachedAcmeServiceDiscovery.java[indent=0] +---- + +Additionally, you can check the xref:service-discovery/kubernetes.adoc#caching-the-service-instances[Kubernetes Service Discovery] for further details about an event-based invalidation example. diff --git a/content/docs/smallrye-stork/2/service-discovery/dns.adoc b/content/docs/smallrye-stork/2/service-discovery/dns.adoc new file mode 100644 index 000000000..7e32934bc --- /dev/null +++ b/content/docs/smallrye-stork/2/service-discovery/dns.adoc @@ -0,0 +1,76 @@ += DNS Service Discovery +:version-root: ../ +include::{version-root}_attributes.adoc[] + +DNS is a name resolution protocol used to determine IP addresses for hostnames. +That makes it a natural fit for service discovery. +Consul and AWS Cloud Map provide DNS resolutions for service discovery. + +This page explains how Stork can use DNS to handle the service discovery. + +== DNS records + +DNS supports a https://en.wikipedia.org/wiki/List_of_DNS_record_types[variety of record types]. Stork can resolve hostnames to addresses based on https://en.wikipedia.org/wiki/SRV_record[_SRV_], A and AAAA records. All these types of records may return multiple addresses for a single hostname. You may choose to not resolve target hostnames. + +While _A_ and _AAAA_ records are quite similar, they just carry an IP (_v4_ for _A_ and _v6_ for _AAAA_), the _SRV_ records are different. +They contain a _weight_, a _target_ and a _port_ for a service instance. +The _target_ returned in an _SRV_ record needs to be resolved further by an _A_ or an _AAAA_ record. + +In short, it works as follows: + +image::images/diagrams/plantuml-bc915c97a9fa.svg[srv_sequence] + +== Dependency + +To use the DNS service discovery, you need to add the Stork DNS Service Discovery provider dependency to your project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-service-discovery-dns + {project-version} + +---- + +== Configuration + +Next, set the service discovery `type` to `dns`. +Additionally, you would usually specify the DNS server, or servers, to use for the discovery. +All in all, your configuration could look as follows: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-discovery.type=dns +# optional dns servers: +stork.my-service.service-discovery.dns-servers=my-dns-server:8221,my-dns-server2 +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.type=dns + +# optional dns servers: +quarkus.stork.my-service.service-discovery.dns-servers=my-dns-server:8221,my-dns-server2 +---- + +-- + +==== + +All the available parameters are as follows: + +include::{generated-dir}/dns-sd-attributes.adoc[] diff --git a/content/docs/smallrye-stork/2/service-discovery/eureka.adoc b/content/docs/smallrye-stork/2/service-discovery/eureka.adoc new file mode 100644 index 000000000..87b5c9208 --- /dev/null +++ b/content/docs/smallrye-stork/2/service-discovery/eureka.adoc @@ -0,0 +1,71 @@ += Eureka Service Discovery +:version-root: ../ +include::{version-root}_attributes.adoc[] + +https://github.com/Netflix/eureka[Eureka] is a RESTful service that is primarily used in the AWS cloud for the purpose of discovery, load balancing, and failover of middle-tier servers. + +This page explains how Stork can use Eureka to handle the service discovery and service registration. + +== Dependency + +First, you need to add the Stork Eureka Service Discovery provider: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-service-discovery-eureka + {project-version} + +---- + +=== Service discovery configuration + +For each application instance expected to be registered in Eureka, configure the lookup: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-discovery.type=eureka +stork.my-service.service-discovery.eureka-host=localhost +stork.my-service.service-discovery.eureka-port=8761 +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.type=eureka +quarkus.stork.my-service.service-discovery.eureka-host=localhost +quarkus.stork.my-service.service-discovery.eureka-port=8761 +---- + +-- + +==== + +Stork looks for the service with the given name (`my-service` in the previous example). + +Supported attributes are the following: + +include::{generated-dir}/eureka-sd-attributes.adoc[] + +The `application` attribute is optional. +It uses the Stork service name (`my-service` in the previous configuration) if not set. + +The `instance` attribute allows selecting a specific instance. +Using this attribute prevents load-balancing as you will always select a single instance. + +The `secure` attribute indicates whether to connect to instances over TLS. +If set to `true`, instances without a secure port enabled are filtered out. +The connection host is always resolved from the `hostName` field of the Eureka instance (falling back to `ipAddr`), regardless of this setting. diff --git a/content/docs/smallrye-stork/2/service-discovery/knative.adoc b/content/docs/smallrye-stork/2/service-discovery/knative.adoc new file mode 100644 index 000000000..539fcdb5b --- /dev/null +++ b/content/docs/smallrye-stork/2/service-discovery/knative.adoc @@ -0,0 +1,129 @@ += Knative Service Discovery +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Knative is a Kubernetes-based platform for serverless workloads. Knative provides a set of objects as Kubernetes Custom Resource Definitions (CRDs). +These resources are used to define and control how your serverless workload behaves on the cluster. +The Stork Knative service discovery implementation is very similar to the Kubernetes one. +Stork will ask for https://knative.dev/docs/serving/reference/serving-api/#serving.knative.dev/v1.Service[Knative services] to the cluster instead of vanilla https://kubernetes.io/docs/concepts/services-networking/service/#service-resource[Kubernetes services] used by the Kubernetes implementation. +To do so, Stork uses https://github.com/fabric8io/kubernetes-client/blob/master/extensions/knative/client/src/main/java/io/fabric8/knative/client/KnativeClient.java[Fabric 8 Knative Client] which is just an extension of Fabric8 Kubernetes Client. + +== Dependency + +First, you need to add the Stork Knative Service Discovery provider: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-service-discovery-knative + {project-version} + +---- + +=== A few words about server authentication. + +Stork uses Fabric8 Knative Client which is a https://github.com/fabric8io/kubernetes-client#readme[Fabric8 Kubernetes Client] extension to access the Kubernetes resources, concretely the `DefaultKnativeClient` implementation. + +Since Knative Client is just an extension of Fabric8 Kubernetes Client, it’s also possible to get an instance of KnativeClient from KubernetesClient. + +`DefaultKubernetesClient` will try to read the `~/.kube/config` file from your local machine and load the token for authenticating with the Kubernetes API server. + +The level of access (Roles) depends on the configured `ServiceAccount`. + +You can override this configuration if you want fine-grain control. + +==== Role-based access control (RBAC) + +If you're using a Kubernetes cluster with Role-Based Access Control (RBAC) enabled, the default permissions for a ServiceAccount don't allow it to list or modify any resources. +A `ServiceAccount`, a `Role` and a `RoleBinding` are needed in order to allow Stork to list the available service instances from the cluster or the namespace. + +An example that allows listing all endpoints could look something like this: + +[source,yaml] +---- +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: + namespace: +--- +kind: Role +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: + namespace: +rules: + - apiGroups: [""] # "" indicates the core API group + resources: ["endpoints", "pods"] # stork queries service endpoints and pods + verbs: ["get", "list"] +--- +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: RoleBinding +metadata: + name: + namespace: +subjects: + - kind: ServiceAccount + # Reference to upper's `metadata.name` + name: + # Reference to upper's `metadata.namespace` + namespace: +roleRef: + kind: Role + name: + apiGroup: rbac.authorization.k8s.io +---- + +== Configuration + +For each service expected to be exposed as _Kubernetes Service_, configure the lookup: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-knservice.service-discovery.type=knative +stork.my-knservice.service-discovery.knative-namespace=my-namespace +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-knservice.service-discovery.type=knative +quarkus.stork.my-knservice.service-discovery.knative-namespace=my-namespace +---- + +-- + +==== + +Stork looks for the _Knative Service_ with the given name (`my-knservice` in the previous example) in the specified namespace. + +Stork inspects the _Knative Service_ and retrieves the url of the service. + +Supported attributes are the following: + +include::{generated-dir}/knative-sd-attributes.adoc[] + +== Caching the service instances + +Contacting the cluster too much frequently can result in performance problems. It's why Knative Service discovery extends `io.smallrye.stork.impl.CachingServiceDiscovery` to automatically _cache_ the service instances. +Moreover, the caching expiration has been also improved in order to only update the retrieved set of `ServiceInstance` if some of them changes and an event is emitted. +This is done by creating an https://www.javadoc.io/static/io.fabric8/kubernetes-client-api/6.1.1/io/fabric8/kubernetes/client/informers/SharedIndexInformer.html[Informer], similar to a https://www.javadoc.io/static/io.fabric8/kubernetes-client-api/6.1.1/io/fabric8/kubernetes/client/Watch.html[Watch], able to observe the events on the Knative Service instances resources. + +Note that: +- the cache is invalidated when an event is received. +- the cache is validated once the instances are retrieved from the cluster, in the `fetchNewServiceInstances` method. +- the `cache` method is overrided to customize the expiration strategy. In this case the collection of service instances will be kept until an event occurs. diff --git a/content/docs/smallrye-stork/2/service-discovery/kubernetes.adoc b/content/docs/smallrye-stork/2/service-discovery/kubernetes.adoc new file mode 100644 index 000000000..1bc26e0a4 --- /dev/null +++ b/content/docs/smallrye-stork/2/service-discovery/kubernetes.adoc @@ -0,0 +1,335 @@ += Kubernetes Service Discovery +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Kubernetes has a built-in support for service discovery and load-balancing. +However, you may need more flexibility to carefully select the service instance you want. + +This page explains how Stork can use the Kubernetes API to handle the service discovery. + +== Dependency + +First, you need to add the Stork Kubernetes Service Discovery provider: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-service-discovery-kubernetes + {project-version} + +---- + +=== A few words about server authentication. + +Stork uses https://github.com/fabric8io/kubernetes-client#readme[Fabric8 Kubernetes Client] to access the Kubernetes resources, concretely the `DefaultKubernetesClient` implementation. + +It will try to read the `~/.kube/config` file from your local machine and load the token for authenticating with the Kubernetes API server. + +If you are using the Stork Kubernetes discovery provider from inside a _Pod_, it loads `~/.kube/config` from the container file system. + +This file is automatically mounted inside the Pod. + +The level of access (Roles) depends on the configured `ServiceAccount`. + +You can override this configuration if you want fine-grain control. + +==== Role-based access control (RBAC) + +If you're using a Kubernetes cluster with Role-Based Access Control (RBAC) enabled, the default permissions for a ServiceAccount don't allow it to list or modify any resources. +A `ServiceAccount`, a `Role` and a `RoleBinding` are needed in order to allow Stork to list the available service instances from the cluster or the namespace. + +An example that allows listing all endpoints could look something like this: + +[source,yaml] +---- +------ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: + namespace: +--- +kind: Role +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: + namespace: +rules: + - apiGroups: [""] # "" indicates the core API group + resources: ["endpoints", "pods", "services"] # stork queries service endpoints, pods and services + verbs: ["get", "list"] +--- +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: RoleBinding +metadata: + name: + namespace: +subjects: + - kind: ServiceAccount + # Reference to upper's `metadata.name` + name: + # Reference to upper's `metadata.namespace` + namespace: +roleRef: + kind: Role + name: + apiGroup: rbac.authorization.k8s.io +---- + +== Configuration + +For each service expected to be exposed as _Kubernetes Service_, configure the lookup: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-discovery.type=kubernetes +stork.my-service.service-discovery.k8s-namespace=my-namespace +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.type=kubernetes +quarkus.stork.my-service.service-discovery.k8s-namespace=my-namespace +---- + +-- + +==== + +Stork looks for the _Kubernetes Service_ with the given name (`my-service` in the previous example) in the specified namespace. + +Instead of using the _Kubernetes Service_ IP directly, and let Kubernetes handle the selection and balancing, Stork inspects the service and retrieves the list of _pods_ providing the service. +Then, it can select the instance. + +== EndpointSlice support (Experimental) + +This feature is experimental. It is opt-in, and the API or behavior may change based on user feedback and Kubernetes evolution. +Recent Kubernetes versions expose service endpoints using *EndpointSlices* (`discovery.k8s.io/v1`), +which are the recommended and scalable replacement for classic `Endpoints`. + +Stork supports EndpointSlices and can use them transparently for service discovery. + +=== Why EndpointSlices + +Compared to classic `Endpoints`, EndpointSlices: + +* Scale better for services with many pods. +* Reduce load on the Kubernetes API server. +* Produce smaller and more frequent updates. +* Are the preferred API in modern Kubernetes clusters. + +When available, EndpointSlices are the recommended source for service discovery. + +== Configuration + +EndpointSlices can be enabled or disabled explicitly. By default, Stork uses automatic detection. + +=== Explicit configuration + +You can force the use of EndpointSlices per service (note that it will still fallback to Endpoints if EndpointSlices are not available for the service): + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.use-endpoint-slices=true +---- + +Or explicitly disable them: + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.use-endpoint-slices=false +---- + +=== Automatic detection (default behavior) + +If `use-endpoint-slices` is not configured, Stork applies the following logic: + +[arabic] +. If the Kubernetes cluster exposes the `discovery.k8s.io/v1` API *and* +. EndpointSlices exist for the target service + +→ Stork uses EndpointSlices. + +Otherwise, it falls back to classic Endpoints. + +This ensures backward compatibility with older clusters and services while automatically benefiting from EndpointSlices when available. + +The detection is performed *asynchronously* (off the event loop) and its result is cached for the lifetime of the service discovery instance. +If the cluster API is temporarily unreachable during detection, Stork retries a few times before falling back to classic Endpoints. +This means a transient connectivity issue at startup does not permanently disable EndpointSlice discovery, but it may cause Stork to fall back to Endpoints for the lifetime of that instance. + +Falling back to classic Endpoints is safe even on modern clusters: Kubernetes maintains `Endpoints` objects alongside `EndpointSlices` for backward compatibility, so both APIs reflect the same service instances. + +== Service instance resolution with EndpointSlices + +When using EndpointSlices, Stork does *not* resolve Pods. + +Instead, service instances are derived directly from the EndpointSlice objects: + +* `endpoint.addresses` → instance host +* `endpointSlice.ports` → instance port +* `endpointSlice.labels` → instance metadata + +Each EndpointSlice represents a homogeneous group of endpoints. +All addresses in a slice share the same set of ports. +If different port combinations exist, Kubernetes exposes them as separate EndpointSlices. + +This guarantees that all `(address, port)` combinations returned by Stork are valid. + +== ClusterIP mode + +By default, Stork resolves individual *pod IPs* from Kubernetes Endpoints (or EndpointSlices). +In ClusterIP mode, Stork resolves to the *ClusterIP and port* of the Kubernetes Service instead, without inspecting the backing pods. + +The `use-cluster-ip` attribute enables this mode. + +=== When to use ClusterIP mode + +ClusterIP mode is useful when: + +* You have a *multi-port Service* and want Stork to resolve the port by name, without needing to know actual port numbers. +* You don't need client-side load balancing — for example, because `kube-proxy`, a service mesh, or another network-level mechanism already handles traffic distribution. +* You want to *control the resolution strategy through configuration*, without changing application code. + +=== Configuration + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-discovery.type=kubernetes +stork.my-service.service-discovery.k8s-namespace=my-namespace +stork.my-service.service-discovery.use-cluster-ip=true +stork.my-service.service-discovery.port-name=http +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.type=kubernetes +quarkus.stork.my-service.service-discovery.k8s-namespace=my-namespace +quarkus.stork.my-service.service-discovery.use-cluster-ip=true +quarkus.stork.my-service.service-discovery.port-name=http +---- + +-- + +==== + +In this mode, Stork queries the Kubernetes `Service` resource directly and returns a single `ServiceInstance` with: + +* *Host* — the Service's ClusterIP (e.g., `10.96.0.15`) +* *Port* — resolved from the Service spec, matching `port-name` when multiple ports are defined + +Since only one instance is returned, any configured Stork load balancer becomes a passthrough. + +[NOTE] +==== + +ClusterIP mode does not support headless Services (`clusterIP: None`). +If the target Service is headless, Stork returns an empty list of service instances. + +==== + +=== Multi-port Services + +ClusterIP mode works well with multi-port Services. +Use the `port-name` attribute to select the desired port: + +[source,yaml] +---- +apiVersion: v1 +kind: Service +metadata: + name: my-service +spec: + ports: + - name: http + port: 8080 + - name: grpc + port: 50051 +---- + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-discovery.type=kubernetes +stork.my-service.service-discovery.use-cluster-ip=true +stork.my-service.service-discovery.port-name=grpc +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.type=kubernetes +quarkus.stork.my-service.service-discovery.use-cluster-ip=true +quarkus.stork.my-service.service-discovery.port-name=grpc +---- + +-- + +==== + +This resolves to `10.96.x.x:50051` — the ClusterIP with the `grpc` port without the application needing to know the actual port number. +If `port-name` is not set, Stork uses the first port defined in the Service spec. + +Supported attributes are the following: + +include::{generated-dir}/kubernetes-sd-attributes.adoc[] + +== Caching the service instances + +Contacting the cluster too much frequently can result in performance problems. It's why Kubernetes Service discovery extends `io.smallrye.stork.impl.CachingServiceDiscovery` to automatically _cache_ the service instances. +Moreover, the caching expiration has been also improved in order to only update the retrieved set of `ServiceInstance` if some of them changes and an event is emitted. +This is done by creating an https://www.javadoc.io/static/io.fabric8/kubernetes-client-api/6.1.1/io/fabric8/kubernetes/client/informers/SharedIndexInformer.html[Informer], similar to a https://www.javadoc.io/static/io.fabric8/kubernetes-client-api/6.1.1/io/fabric8/kubernetes/client/Watch.html[Watch], able to observe the events on the service instances resources. + +Note that: +- the cache is invalidated when an event is received. +- the cache is validated, if and only if, the instances are retrieved successfully from the cluster, in the `fetchNewServiceInstances` method. +- In case of an error contacting the cluster (e.g., network failure, API server unreachable), a retry mechanism is triggered. +The system will attempt to contact the cluster up to the number of times specified by request-retry-backoff-limit, waiting request-retry-backoff-interval milliseconds between each attempt. +By default, retries are disabled to prevent the system from entering an infinite loop of calls to an unresponsive cluster. +- If no service instances are found (e.g., no matching endpoints, headless service in ClusterIP mode), Stork returns an empty list. +- the `cache` method is overridden to customize the expiration strategy. In this case the collection of service instances will be kept until an event occurs. + +When EndpointSlice are selected, Stork creates informers on the `EndpointSlice` resources instead of classic `Endpoints`. +The caching behavior remains unchanged. + +In ClusterIP mode, Stork creates an Informer on the Kubernetes `Service` resource (instead of Endpoints or EndpointSlices). +The cache is invalidated when the Service is added, updated, or deleted. +The same caching behavior described above applies. diff --git a/content/docs/smallrye-stork/2/service-discovery/overview.adoc b/content/docs/smallrye-stork/2/service-discovery/overview.adoc new file mode 100644 index 000000000..42531f0d8 --- /dev/null +++ b/content/docs/smallrye-stork/2/service-discovery/overview.adoc @@ -0,0 +1,34 @@ += Overview +:version-root: ../ +include::{version-root}_attributes.adoc[] + +== Service Discovery in SmallRye Stork + +As already introduced, service discovery is a crucial part of modern microservices architectures. +It allows services to dynamically discover the location of other services at runtime, which is particularly useful in distributed systems where services may scale up or down, +or change their network addresses. + +SmallRye Stork provides a flexible and extensible mechanism for service discovery. +It supports out of the box some service discovery such as Kubernetes or Consul but the main strength of it is customization so you can easily create your own implementation related on your business for example. +Stork allows services to communicate with each other without requiring hardcoded addresses, making it an ideal solution for microservices deployments. +SmallRye Stork brings this capability to clients for Quarkus applications but it's vendor agnostic so you easily use it with other solutions and even in standalone mode. + +== Service discovery caching overview + +Stork provides a built-in caching mechanism for service discovery results. +By default, each service discovery implementation may repeatedly contact its backend (e.g., Consul, Kubernetes, Eureka) to retrieve the current list of service instances. +Since these lookups can be expensive or frequent, Stork allows service discovery providers to cache the retrieved instances for a configurable period of time. + +* Cached results are reused for subsequent lookups, avoiding unnecessary backend calls. +* The cache is automatically refreshed after the configured `refresh-period`. +* If a refresh fails, Stork keeps serving the previously retrieved list of instances until a new successful refresh occurs. + +In addition to the time-based refresh mechanism, Stork also exposes an *cache invalidation API* . +This API can be used to explicitly clear the cached results, forcing the next lookup to perform a new service discovery call. +This is especially useful when the service discovery backend supports event-based notifications (for example, Consul blocking queries), allowing the cache to be invalidated as soon as a change is detected. + +____ +For more details about how to enable caching in a custom service discovery implementation, see the xref:service-discovery/custom-service-discovery.adoc#caching-the-service-instances[Caching the service instances] section. +____ + +You can explore the different implementations and learn how to create your own in the following sections. diff --git a/content/docs/smallrye-stork/2/service-discovery/static-list.adoc b/content/docs/smallrye-stork/2/service-discovery/static-list.adoc new file mode 100644 index 000000000..4e2b066f3 --- /dev/null +++ b/content/docs/smallrye-stork/2/service-discovery/static-list.adoc @@ -0,0 +1,56 @@ += Static List Service Discovery +:version-root: ../ +include::{version-root}_attributes.adoc[] + +In some situations, such as demos, development, or testing, you may want to mock the service discovery by providing a predefined list of service instances. +For this purpose, Stork comes with a `static` service discovery type. + +== Dependency + +To use the `static` service discovery, first add the appropriate Service Discovery provider dependency to your project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-service-discovery-static-list + {project-version} + +---- + +== Configuration + +For each service that should use the static list of service instances configure the service registrar `type`: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-discovery.type=static +stork.my-service.service-discovery.address-list=localhost:8080,localhost:8081 +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.type=static +quarkus.stork.my-service.service-discovery.address-list=localhost:8080,localhost:8081 +---- + +-- + +==== + +These are all the static service discovery parameters: + +include::{generated-dir}/static-sd-attributes.adoc[] diff --git a/content/docs/smallrye-stork/2/service-registration/consul.adoc b/content/docs/smallrye-stork/2/service-registration/consul.adoc new file mode 100644 index 000000000..10fe6d6ea --- /dev/null +++ b/content/docs/smallrye-stork/2/service-registration/consul.adoc @@ -0,0 +1,228 @@ += Consul Service Registration +:version-root: ../ +include::{version-root}_attributes.adoc[] + +https://www.consul.io/[Consul] is a distributed, highly available, and data center aware solution to connect and configure applications across dynamic, distributed infrastructure. +It's often used as service discovery backend to register and locate the services composing your system. +Consul makes it simple for services to register themselves and to discover other services via a DNS or HTTP interface. +External services can be registered as well. + +This page explains how Stork can use Consul to handle the service registration. + +== Dependency + +First, you need to add the Stork Consul Service Registration provider: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-service-registration-consul + {project-version} + +---- + +== Service registration configuration + +For each service that should register the service instances in Consul, configure the service registrar `type`: + +[.tabs] +==== + +stork standalone:: ++ +-- + +-- + +==== + +[source,properties] +---- +stork.my-service.service-registrar.type=consul +---- + +[.tabs] +==== + +stork in quarkus:: ++ +-- + +-- + +==== + +[source,properties] +---- +quarkus.stork.my-service.service-registrar.type=consul +---- + +Consul service registrar is configured with the following parameters: + +include::{generated-dir}/consul-sr-attributes.adoc[] + +== Service deregistration configuration + +There is no specific configuration required to enable deregistration; however, you must ensure that a consul service registrar is configured for the service: + +[.tabs] +==== + +stork standalone:: ++ +-- + +-- + +==== + +[source,properties] +---- +stork.my-service.service-registrar.type=consul +---- + +[.tabs] +==== + +stork in quarkus:: ++ +-- + +-- + +==== + +[source,properties] +---- +quarkus.stork.my-service.service-registrar.type=consul +---- + +As with registration, deregistration relies on the service name. + +== Secure communication with Consul + +When your Consul cluster uses TLS/SSL encryption, you can configure Stork to establish secure connections. Enable SSL and provide the necessary trust store configuration: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-registrar.type=consul +stork.my-service.service-registrar.consul-host=localhost +stork.my-service.service-registrar.consul-port=8501 +stork.my-service.service-registrar.ssl=true +stork.my-service.service-registrar.trust-store-path=/path/to/truststore.jks +stork.my-service.service-registrar.trust-store-password=changeit +stork.my-service.service-registrar.verify-host=true +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-registrar.type=consul +quarkus.stork.my-service.service-registrar.consul-host=localhost +quarkus.stork.my-service.service-registrar.consul-port=8501 +quarkus.stork.my-service.service-registrar.ssl=true +quarkus.stork.my-service.service-registrar.trust-store-path=/path/to/truststore.jks +quarkus.stork.my-service.service-registrar.trust-store-password=changeit +quarkus.stork.my-service.service-registrar.verify-host=true +---- + +-- + +==== + +For mutual TLS (mTLS) authentication where the client must also present a certificate, provide both trust store and key store configuration: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-registrar.type=consul +stork.my-service.service-registrar.consul-host=localhost +stork.my-service.service-registrar.consul-port=8501 +stork.my-service.service-registrar.ssl=true +stork.my-service.service-registrar.trust-store-path=/path/to/truststore.jks +stork.my-service.service-registrar.trust-store-password=changeit +stork.my-service.service-registrar.key-store-path=/path/to/keystore.jks +stork.my-service.service-registrar.key-store-password=changeit +stork.my-service.service-registrar.verify-host=true +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-registrar.type=consul +quarkus.stork.my-service.service-registrar.consul-host=localhost +quarkus.stork.my-service.service-registrar.consul-port=8501 +quarkus.stork.my-service.service-registrar.ssl=true +quarkus.stork.my-service.service-registrar.trust-store-path=/path/to/truststore.jks +quarkus.stork.my-service.service-registrar.trust-store-password=changeit +quarkus.stork.my-service.service-registrar.key-store-path=/path/to/keystore.jks +quarkus.stork.my-service.service-registrar.key-store-password=changeit +quarkus.stork.my-service.service-registrar.verify-host=true +---- + +-- + +==== + +== ACL Token Authentication + +When your Consul cluster has ACL (Access Control List) enabled, you need to provide an ACL token for authentication: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-registrar.type=consul +stork.my-service.service-registrar.consul-host=localhost +stork.my-service.service-registrar.consul-port=8500 +stork.my-service.service-registrar.acl-token=your-acl-token +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-registrar.type=consul +quarkus.stork.my-service.service-registrar.consul-host=localhost +quarkus.stork.my-service.service-registrar.consul-port=8500 +quarkus.stork.my-service.service-registrar.acl-token=your-acl-token +---- + +-- + +==== + +The ACL token can be combined with SSL/TLS configuration for secure, authenticated access to Consul. diff --git a/content/docs/smallrye-stork/2/service-registration/custom-service-registration.adoc b/content/docs/smallrye-stork/2/service-registration/custom-service-registration.adoc new file mode 100644 index 000000000..efd60d49b --- /dev/null +++ b/content/docs/smallrye-stork/2/service-registration/custom-service-registration.adoc @@ -0,0 +1,162 @@ += Implement your own service registration mechanism +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Stork is extensible, and you can implement your own service registrar mechanism. + +== Dependencies + +To implement your _Service Registration Provider_, make sure your project depends on Core and Configuration Generator. +The former brings classes necessary to implement custom registrar, the latter contains an https://docs.oracle.com/en/java/javase/11/docs/api/java.compiler/javax/annotation/processing/Processor.html[annotation processor] that generates classes needed by Stork. + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-core + {project-version} + + + io.smallrye.stork + stork-configuration-generator + provided + + {project-version} + +---- + +== Implementing a service registrar provider + +Service registration implementation consists of three elements: + +* `ServiceRegistrar` which is responsible for registering service instances for a single Stork service. +* `ServiceRegistrarProvider` which creates instances of `ServiceRegistrar` for a given service registrar _type_. +* `$typeConfiguration` which is a configuration for the registrar. This class is automatically generated during the compilation (using an annotation processor). + +A _type_, for example, `acme`, identifies each provider. +This _type_ is used in the configuration to reference the provider: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-registrar.type=acme +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-registrar.type=acme +---- + +-- + +==== + +A `ServiceRegistrarProvider` implementation needs to be annotated with `@ServiceRegistrarType` that defines the _type_. +Any configuration properties that the provider expects should be defined with `@ServiceRegistrarAttribute` annotations placed on the provider. +Optionally, you can also add `@ApplicationScoped` annotation in order to provide the service registrar implementation as CDI bean. + +A service registrar provider class should look as follows: + +[source,java] +---- +include::{examples-dir}/examples/AcmeServiceRegistrarProvider.java[indent=0] +---- + +Note, that the `ServiceRegistrarProvider` interface takes a configuration class as a parameter. This configuration class +is generated automatically by the _Configuration Generator_. +Its name is created by appending `RegistrarConfiguration` to the service registrar type, such as `AcmeRegistrarConfiguration`. + +The next step is to implement the `ServiceRegistrar` interface: + +[source,java] +---- +include::{examples-dir}/examples/AcmeServiceRegistrar.java[indent=0] +---- + +This implementation is simplistic. +Typically, instead of creating a service instance with values from the configuration, you would connect to a service registration backend, look for the service and build the list of service instance accordingly. +That's why the method returns a `Uni`. +Most of the time, the lookup is a remote operation. + +As you can see, the `AcmeConfiguration` class gives access to the configuration attribute. + +=== Implement your own service deregistration mechanism + +The `registerServiceInstance` method receives an optional `instanceName` parameter that acts as an explicit identifier for the registered instance. +When provided, it should be used as the registration ID so that the same instance can be precisely targeted during deregistration. +If `instanceName` is `null` or empty, implementations should fall back to a deterministic ID derived from the service name, IP address, and port. + +Finally, you can implement your own service deregistration mechanism in `deregisterServiceInstance` method. + +[WARNING] +.Override the specific-instance deregistration methods +==== + +The `ServiceRegistrar` interface provides several deregistration methods: + +* `deregisterServiceInstance(String serviceName)` — deregisters *all* instances of a service. +* `deregisterServiceInstance(String serviceName, String instanceName)` — deregisters the instance registered under the given name. +* `deregisterServiceInstance(String serviceName, String ipAddress, int port)` — deregisters the instance identified by IP and port. + +The default implementations of the second and third methods fall back to the first, which may deregister *all* instances. +*Custom registrar implementations should override at least one of the specific-instance methods* to provide precise, instance-level deregistration and avoid unintended side effects. + +==== + +== Using your service registrar using the programmatic API + +In the project using it, don't forget to add the dependency on the module providing your implementation. +Then, in the configuration, just add: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-registrar.type=acme +stork.my-service.service-registrar.host=localhost +stork.my-service.service-registrar.port=1234 +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-registrar.type=acme +quarkus.stork.my-service.service-registrar.host=localhost +quarkus.stork.my-service.service-registrar.port=1234 +---- + +-- + +==== + +When building your service registrar project project, the configuration generator creates a configuration class. +Then, Stork will use your implementation to register/deregister the service instances using the `acme` backend. + +This class can be used to configure your service registrar using the Stork programmatic API. + +[source,java] +---- +include::{examples-dir}/examples/AcmeRegistrarApiUsage.java[indent=0] +---- diff --git a/content/docs/smallrye-stork/2/service-registration/eureka.adoc b/content/docs/smallrye-stork/2/service-registration/eureka.adoc new file mode 100644 index 000000000..8e327934d --- /dev/null +++ b/content/docs/smallrye-stork/2/service-registration/eureka.adoc @@ -0,0 +1,105 @@ += Eureka Service Registration +:version-root: ../ +include::{version-root}_attributes.adoc[] + +https://github.com/Netflix/eureka[Eureka] is a RESTful service that is primarily used in the AWS cloud for the purpose of discovery, load balancing, and failover of middle-tier servers. + +This page explains how Stork can use Eureka to handle the service registration. + +== Dependency + +First, you need to add the Stork Eureka Service Registration provider: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-service-registration-eureka + {project-version} + +---- + +== Service registration configuration + +For each service that should register the service instances in Eureka, configure the service registrar `type`: + +[.tabs] +==== + +stork standalone:: ++ +-- + +-- + +==== + +[source,properties] +---- +stork.my-service.service-registrar.type=eureka +stork.my-service.service-registrar.eureka-host=localhost +stork.my-service.service-registrar.eureka-port=8761 +---- + +[.tabs] +==== + +stork in quarkus:: ++ +-- + +-- + +==== + +[source,properties] +---- +quarkus.stork.my-service.service-registrar.type=eureka +quarkus.stork.my-service.service-registrar.eureka-host=localhost +quarkus.stork.my-service.service-registrar.eureka-port=8761 +---- + +Eureka service registrar is configured with the following parameters: + +include::{generated-dir}/eureka-sr-attributes.adoc[] + +The `ipAddress` passed to `registerServiceInstance()` is used as both the IP address (`ipAddr`) and the hostname (`hostName`) registered in Eureka. +If the instance has a distinct DNS hostname, set `host-name` explicitly to override this default. + +== Service deregistration configuration + +There is no specific configuration required to enable deregistration; however, you must ensure that a consul service registrar is configured for the service: + +[.tabs] +==== + +stork standalone:: ++ +-- + +-- + +==== + +[source,properties] +---- +stork.my-service.service-registrar.type=eureka +---- + +[.tabs] +==== + +stork in quarkus:: ++ +-- + +-- + +==== + +[source,properties] +---- +quarkus.stork.my-service.service-registrar.type=eureka +---- + +As with registration, deregistration relies on the service name. diff --git a/content/docs/smallrye-stork/2/service-registration/overview.adoc b/content/docs/smallrye-stork/2/service-registration/overview.adoc new file mode 100644 index 000000000..082df0e9f --- /dev/null +++ b/content/docs/smallrye-stork/2/service-registration/overview.adoc @@ -0,0 +1,82 @@ += Overview +:version-root: ../ +include::{version-root}_attributes.adoc[] + +== Service Registration in SmallRye Stork + +Service registration is the process by which services announce their availability to a central registry, allowing other services to discover and communicate with them. +In SmallRye Stork, service registration is automated and integrated with supported registries like *Consul*. +This ensures that services can dynamically join and leave the network. + +=== Key Features: + +* *Automatic Registration*: For *Quarkus applications*, SmallRye Stork automatically registers the application with the configured service registry — but *only for built-in registrars such as Consul, Eureka, and Static*. +If you're using a custom registrar, registration must still be handled programmatically, as before this feature. + +Registration is enabled by default. If you want to disable registration, you can do it per service using the following property: + +[.tabs] +==== + +stork standalone:: ++ +-- + +-- + +==== + +[source,properties] +---- +stork.my-service.service-registrar.enable=false +---- + +[.tabs] +==== + +stork in quarkus:: ++ +-- + +-- + +==== + +[source,properties] +---- +quarkus.stork.my-service.service-registrar.enable=false +---- + +*IMPORTANT* Public IP address needs to be provided. Smallrye Stork will fail if the service IP address is not provided during registration. + +==== Supported Registries: + +Currently, Smallrye Stork provides seamless integration with Consul, Eureka and a Static registry. +This integration simplifies the management of dynamic environments where services are frequently added or removed. + +==== Custom Registration: + +In addition to the default mechanisms, SmallRye Stork allows you to implement custom service registration strategies, +providing flexibility for different infrastructures or custom service registration needs. + +In the following sections you can have more details about each specific implementation. + +=== Deregister Services + +SmallRye Stork now supports deregistration of service instances from a central registry when explicitly requested. +This feature complements the registration mechanism. + +This operation is typically should be triggered when a service instance is shut down or no longer available, helping maintain a consistent and accurate service registry. +To enable service deregistration, you can invoke the `deregisterServiceInstance` method on the `ServiceRegistrar` implementation programmatically. +To target a specific instance, prefer one of these overloads: + +* `deregisterServiceInstance(serviceName, instanceName)` — deregisters the instance registered under the given name. +* `deregisterServiceInstance(serviceName, ipAddress, port)` — deregisters the instance identified by IP and port. + +The single-argument overload `deregisterServiceInstance(serviceName)` deregisters *all* instances of the service. + +*Note*: When used in standalone mode, Stork does *not* automatically handle service instance registration or deregistration. +However, when using the quarkus-stork-registration extension within a Quarkus application, +service instances are automatically registered at application startup and deregistered upon shutdown, unless this behavior is explicitly disabled. + +In the following sections you can have more details about each specific implementation. diff --git a/content/docs/smallrye-stork/2/service-registration/static-list.adoc b/content/docs/smallrye-stork/2/service-registration/static-list.adoc new file mode 100644 index 000000000..87e3eb570 --- /dev/null +++ b/content/docs/smallrye-stork/2/service-registration/static-list.adoc @@ -0,0 +1,96 @@ += Static List Service Registration +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Stork provides the ability to register services using Static list as backend. + +== Dependency + +To use the `static` service registrar, first add the appropriate Service Registration provider dependency to your project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-service-registration-static-list + {project-version} + +---- + +== Service registration configuration + +For each service that should register the service instances in a static list, configure the service registrar `type`: + +[.tabs] +==== + +stork standalone:: ++ +-- + +-- + +==== + +[source,properties] +---- +stork.my-service.service-registrar.type=static +---- + +[.tabs] +==== + +stork in quarkus:: ++ +-- + +-- + +==== + +[source,properties] +---- +quarkus.stork.my-service.service-registrar.type=static +---- + +Static service registrar is configured with the following parameters: + +include::{generated-dir}/static-sr-attributes.adoc[] + +== Service deregistration configuration + +There is no specific configuration required to enable deregistration; however, you must ensure that a consul service registrar is configured for the service: + +[.tabs] +==== + +stork standalone:: ++ +-- + +-- + +==== + +[source,properties] +---- +stork.my-service.service-registrar.type=static +---- + +[.tabs] +==== + +stork in quarkus:: ++ +-- + +-- + +==== + +[source,properties] +---- +quarkus.stork.my-service.service-registrar.type=static +---- + +As with registration, deregistration relies on the service name. diff --git a/content/docs/smallrye-stork/2/springboot-config.adoc b/content/docs/smallrye-stork/2/springboot-config.adoc new file mode 100644 index 000000000..05d2f1a3b --- /dev/null +++ b/content/docs/smallrye-stork/2/springboot-config.adoc @@ -0,0 +1,40 @@ += Stork Spring Boot Config +include::_attributes.adoc[] + +Stork seamlessly supports Spring Boot configuration, facilitating access to configuration properties. +This documentation elaborates on how Spring Boot developers can use Stork in their Spring Boot applications and configure it using the application.properties file. + +== Dependency setup + +To enable Spring Boot configuration integration in Stork, you need to include the following dependency: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-spring-boot-config + {project-version} + +---- + +== Initializing Stork + +Since Spring Boot lacks a pre-configured Stork instance, you'll need create one. It can be done by providing a Spring bean performing Stork initialization: + +[source,java] +---- +include::{examples-dir}/examples/SpringBootInitializationExample.java[indent=0] +---- + +Upon initialization, Stork scans for the `io.smallrye.stork.springboot.SpringBootConfigProvider` SPI provider and CDI beans (from version 2.x onwards). +It then builds a comprehensive list of managed services by parsing the properties configuration files. + +Please note the importance of the `io.smallrye.stork.springboot.SpringBootApplicationContextProvider` bean in our setup. +This bean has a critical role by granting Stork access to the current `org.springframework.context.ApplicationContext`. +It enables it to retrieve configuration details effectively. Consequently, it's imperative that this bean is instantiated prior to initiating the Stork initialization process. +In this case, we utilize the `@DependsOn` annotation for that. +It allows us controlling the bean creation order. + +== Comprehensive Example + +You can check our https://github.com/aureamunoz/spring-stork-guitar-hero/[Guitar Hero Application] showcasing the seamless integration of Stork with Spring Boot Configuration. diff --git a/content/docs/smallrye-stork/latest/_attributes.adoc b/content/docs/smallrye-stork/latest/_attributes.adoc new file mode 100644 index 000000000..2c34041a9 --- /dev/null +++ b/content/docs/smallrye-stork/latest/_attributes.adoc @@ -0,0 +1,20 @@ +:project-version: 3.0.3 +// Attributes shared by every page. smallrye.io overrides the ones it needs when it publishes. +// Pages in subdirectories set :version-root: (../ per level) before including this file. +ifndef::version-root[:version-root:] +:idprefix: +:idseparator: - +:sectanchors: +:examples-dir: {version-root}_examples +:partialsdir: {version-root}_partials +:project-name: SmallRye Stork +:javadoc-version: latest +:generated-dir: {version-root}_generated + +// Added by smallrye.io (tools/docs/Docs.java sync) +:javadoc-version: 3.0.3 +:relfileprefix: /docs/smallrye-stork/latest/ +:imagesdir: /docs/smallrye-stork/latest +:docs-project: smallrye-stork +:docs-version: latest +:docs-ref: 3.0.3 diff --git a/content/docs/smallrye-stork/latest/_examples/examples/AcmeDiscoveryApiUsage.java b/content/docs/smallrye-stork/latest/_examples/examples/AcmeDiscoveryApiUsage.java new file mode 100644 index 000000000..1487b571a --- /dev/null +++ b/content/docs/smallrye-stork/latest/_examples/examples/AcmeDiscoveryApiUsage.java @@ -0,0 +1,20 @@ +package examples; + +import io.smallrye.mutiny.Uni; +import io.smallrye.stork.api.ServiceDefinition; +import io.smallrye.stork.api.ServiceInstance; +import io.smallrye.stork.api.StorkServiceRegistry; + +import java.util.List; + +public class AcmeDiscoveryApiUsage { + + public void example(StorkServiceRegistry stork) { + stork.defineIfAbsent("my-service", ServiceDefinition.of( + new AcmeConfiguration().withHost("my-host")) + ); + + Uni> serviceInstances = stork.getService("my-service").getInstances(); + } + +} diff --git a/content/docs/smallrye-stork/latest/_examples/examples/AcmeLoadBalancer.java b/content/docs/smallrye-stork/latest/_examples/examples/AcmeLoadBalancer.java new file mode 100644 index 000000000..6fb0b1be1 --- /dev/null +++ b/content/docs/smallrye-stork/latest/_examples/examples/AcmeLoadBalancer.java @@ -0,0 +1,32 @@ +package examples; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Random; + +import io.smallrye.stork.api.LoadBalancer; +import io.smallrye.stork.api.NoServiceInstanceFoundException; +import io.smallrye.stork.api.ServiceInstance; + +public class AcmeLoadBalancer implements LoadBalancer { + + private final Random random; + + public AcmeLoadBalancer(AcmeLoadBalancerConfiguration config) { + random = new Random(); + } + + @Override + public ServiceInstance selectServiceInstance(Collection serviceInstances) { + if (serviceInstances.isEmpty()) { + throw new NoServiceInstanceFoundException("No services found."); + } + int index = random.nextInt(serviceInstances.size()); + return new ArrayList<>(serviceInstances).get(index); + } + + @Override + public boolean requiresStrictRecording() { + return false; + } +} diff --git a/content/docs/smallrye-stork/latest/_examples/examples/AcmeLoadBalancerProvider.java b/content/docs/smallrye-stork/latest/_examples/examples/AcmeLoadBalancerProvider.java new file mode 100644 index 000000000..3640322ee --- /dev/null +++ b/content/docs/smallrye-stork/latest/_examples/examples/AcmeLoadBalancerProvider.java @@ -0,0 +1,22 @@ +package examples; + +import io.smallrye.stork.api.LoadBalancer; +import io.smallrye.stork.api.ServiceDiscovery; +import io.smallrye.stork.api.config.LoadBalancerAttribute; +import io.smallrye.stork.api.config.LoadBalancerType; +import io.smallrye.stork.spi.LoadBalancerProvider; +import jakarta.enterprise.context.ApplicationScoped; + +@LoadBalancerType("acme-load-balancer") +@LoadBalancerAttribute(name = "my-attribute", + description = "Attribute that alters the behavior of the LoadBalancer") +@ApplicationScoped +public class AcmeLoadBalancerProvider implements + LoadBalancerProvider { + + @Override + public LoadBalancer createLoadBalancer(AcmeLoadBalancerConfiguration config, + ServiceDiscovery serviceDiscovery) { + return new AcmeLoadBalancer(config); + } +} diff --git a/content/docs/smallrye-stork/latest/_examples/examples/AcmeObservationCollector.java b/content/docs/smallrye-stork/latest/_examples/examples/AcmeObservationCollector.java new file mode 100644 index 000000000..3c4655e7d --- /dev/null +++ b/content/docs/smallrye-stork/latest/_examples/examples/AcmeObservationCollector.java @@ -0,0 +1,35 @@ +package examples; + +import io.smallrye.stork.Stork; +import io.smallrye.stork.api.observability.ObservationCollector; +import io.smallrye.stork.api.observability.StorkEventHandler; +import io.smallrye.stork.api.observability.StorkObservation; +import org.jboss.logging.Logger; + +public class AcmeObservationCollector implements ObservationCollector { + + private static final Logger LOGGER = Logger.getLogger(AcmeObservationCollector.class); + + private static final StorkEventHandler ACME_HANDLER = event -> { + //This is the terminal event. Put here your custom logic to extend the metrics collection. + + //E.g. Expose metrics to Micrometer, additional logs.... + LOGGER.info( "Service discovery took " + event.getServiceDiscoveryDuration() + "."); + LOGGER.info( event.getDiscoveredInstancesCount() + " have been discovered for " + event.getServiceName() + "."); + LOGGER.info( "Service selection took " + event.getServiceSelectionDuration() + "."); + + // ... + + }; + + public static StorkObservation ACME_STORK_EVENT; + + @Override + public StorkObservation create(String serviceName, String serviceDiscoveryType, + String serviceSelectionType) { + ACME_STORK_EVENT = new StorkObservation( + serviceName, serviceDiscoveryType, serviceSelectionType, + ACME_HANDLER); + return ACME_STORK_EVENT; + } +} diff --git a/content/docs/smallrye-stork/latest/_examples/examples/AcmeRegistrarApiUsage.java b/content/docs/smallrye-stork/latest/_examples/examples/AcmeRegistrarApiUsage.java new file mode 100644 index 000000000..2c4b52cef --- /dev/null +++ b/content/docs/smallrye-stork/latest/_examples/examples/AcmeRegistrarApiUsage.java @@ -0,0 +1,21 @@ +package examples; + +import io.smallrye.stork.api.ServiceDefinition; +import io.smallrye.stork.api.StorkServiceRegistry; +import io.smallrye.stork.servicediscovery.staticlist.StaticConfiguration; + +public class AcmeRegistrarApiUsage { + + public void example(StorkServiceRegistry stork) { + String list = "localhost:8080, localhost:8081"; + + stork.defineIfAbsent("my-service", ServiceDefinition.of( + new StaticConfiguration().withAddressList(list), + new AcmeLoadBalancerConfiguration().withMyAttribute("my-value"),new AcmeRegistrarConfiguration()) + ); + + stork.getService("my-service").registerNamedInstance("my-instance", "localhost", + 9000); + } + +} diff --git a/content/docs/smallrye-stork/latest/_examples/examples/AcmeSelectorApiUsage.java b/content/docs/smallrye-stork/latest/_examples/examples/AcmeSelectorApiUsage.java new file mode 100644 index 000000000..67440ddf9 --- /dev/null +++ b/content/docs/smallrye-stork/latest/_examples/examples/AcmeSelectorApiUsage.java @@ -0,0 +1,22 @@ +package examples; + +import io.smallrye.mutiny.Uni; +import io.smallrye.stork.api.ServiceDefinition; +import io.smallrye.stork.api.ServiceInstance; +import io.smallrye.stork.api.StorkServiceRegistry; +import io.smallrye.stork.servicediscovery.staticlist.StaticConfiguration; +import io.smallrye.stork.serviceregistration.staticlist.StaticRegistrarConfiguration; + +public class AcmeSelectorApiUsage { + + public void example(StorkServiceRegistry stork) { + String list = "localhost:8080, localhost:8081"; + stork.defineIfAbsent("my-service", ServiceDefinition.of( + new StaticConfiguration().withAddressList(list), + new AcmeLoadBalancerConfiguration().withMyAttribute("my-value"),new StaticRegistrarConfiguration()) + ); + + Uni uni = stork.getService("my-service").selectInstance(); + } + +} diff --git a/content/docs/smallrye-stork/latest/_examples/examples/AcmeServiceDiscovery.java b/content/docs/smallrye-stork/latest/_examples/examples/AcmeServiceDiscovery.java new file mode 100644 index 000000000..b9368c4b4 --- /dev/null +++ b/content/docs/smallrye-stork/latest/_examples/examples/AcmeServiceDiscovery.java @@ -0,0 +1,32 @@ +package examples; + +import java.util.Collections; +import java.util.List; + +import io.smallrye.mutiny.Uni; +import io.smallrye.stork.api.ServiceDiscovery; +import io.smallrye.stork.api.ServiceInstance; +import io.smallrye.stork.impl.DefaultServiceInstance; +import io.smallrye.stork.utils.ServiceInstanceIds; + +public class AcmeServiceDiscovery implements ServiceDiscovery { + + private final String host; + private final int port; + + public AcmeServiceDiscovery(AcmeConfiguration configuration) { + this.host = configuration.getHost(); + this.port = Integer.parseInt(configuration.getPort()); + } + + @Override + public Uni> getServiceInstances() { + // Proceed to the lookup... + // Here, we just return a DefaultServiceInstance with the configured host and port + // The last parameter specifies whether the communication with the instance should + // happen over a secure connection + DefaultServiceInstance instance = + new DefaultServiceInstance(ServiceInstanceIds.next(), host, port, false); + return Uni.createFrom().item(() -> Collections.singletonList(instance)); + } +} diff --git a/content/docs/smallrye-stork/latest/_examples/examples/AcmeServiceDiscoveryProvider.java b/content/docs/smallrye-stork/latest/_examples/examples/AcmeServiceDiscoveryProvider.java new file mode 100644 index 000000000..ca39d5289 --- /dev/null +++ b/content/docs/smallrye-stork/latest/_examples/examples/AcmeServiceDiscoveryProvider.java @@ -0,0 +1,28 @@ +package examples; + +import io.smallrye.stork.api.ServiceDiscovery; +import io.smallrye.stork.api.config.ServiceConfig; +import io.smallrye.stork.api.config.ServiceDiscoveryAttribute; +import io.smallrye.stork.api.config.ServiceDiscoveryType; +import io.smallrye.stork.spi.StorkInfrastructure; +import io.smallrye.stork.spi.ServiceDiscoveryProvider; +import jakarta.enterprise.context.ApplicationScoped; + +@ServiceDiscoveryType("acme") +@ServiceDiscoveryAttribute(name = "host", + description = "Host name of the service discovery server.", required = true) +@ServiceDiscoveryAttribute(name = "port", + description = "Hort of the service discovery server.", required = false) +@ApplicationScoped +public class AcmeServiceDiscoveryProvider + implements ServiceDiscoveryProvider { + + @Override + public ServiceDiscovery createServiceDiscovery( + AcmeConfiguration config, + String serviceName, + ServiceConfig serviceConfig, + StorkInfrastructure storkInfrastructure) { + return new AcmeServiceDiscovery(config); + } +} diff --git a/content/docs/smallrye-stork/latest/_examples/examples/AcmeServiceRegistrar.java b/content/docs/smallrye-stork/latest/_examples/examples/AcmeServiceRegistrar.java new file mode 100644 index 000000000..52ce8d799 --- /dev/null +++ b/content/docs/smallrye-stork/latest/_examples/examples/AcmeServiceRegistrar.java @@ -0,0 +1,38 @@ +package examples; + +import io.smallrye.mutiny.Uni; +import io.smallrye.stork.api.Metadata; +import io.smallrye.stork.api.ServiceRegistrar; + +import java.util.List; + +public class AcmeServiceRegistrar implements ServiceRegistrar { + + private final String backendHost; + private final int backendPort; + + public AcmeServiceRegistrar(AcmeRegistrarConfiguration configuration) { + this.backendHost = configuration.getHost(); + this.backendPort = Integer.parseInt(configuration.getPort()); + } + + + @Override + public Uni registerServiceInstance(String serviceName, String instanceName, Metadata metadata, String ipAddress, int defaultPort) { + //instanceName is an optional identifier for this specific instance; use it as the registration ID when provided + //do whatever is needed for registering service instance + return Uni.createFrom().voidItem(); + } + + @Override + public Uni registerServiceInstance(String serviceName, String instanceName, List tags, + Metadata metadata, String ipAddress, int defaultPort) { + return Uni.createFrom().voidItem(); + } + + @Override + public Uni deregisterServiceInstance(String serviceName) { + //do whatever is needed for deregistering service instance + return Uni.createFrom().voidItem(); + } +} \ No newline at end of file diff --git a/content/docs/smallrye-stork/latest/_examples/examples/AcmeServiceRegistrarProvider.java b/content/docs/smallrye-stork/latest/_examples/examples/AcmeServiceRegistrarProvider.java new file mode 100644 index 000000000..52e028cfb --- /dev/null +++ b/content/docs/smallrye-stork/latest/_examples/examples/AcmeServiceRegistrarProvider.java @@ -0,0 +1,28 @@ +package examples; + +import io.smallrye.stork.api.Metadata; +import io.smallrye.stork.api.ServiceRegistrar; +import io.smallrye.stork.api.config.ServiceRegistrarAttribute; +import io.smallrye.stork.api.config.ServiceRegistrarType; +import io.smallrye.stork.spi.ServiceRegistrarProvider; +import io.smallrye.stork.spi.StorkInfrastructure; +import jakarta.enterprise.context.ApplicationScoped; + +@ServiceRegistrarType(value = "acme", metadataKey = Metadata.DefaultMetadataKey.class) +@ServiceRegistrarAttribute(name = "host", + description = "Host name of the service registration server.", required = true) +@ServiceRegistrarAttribute(name = "port", + description = "Port of the service registration server.", required = false) +@ApplicationScoped +public class AcmeServiceRegistrarProvider + implements ServiceRegistrarProvider { + + @Override + public ServiceRegistrar createServiceRegistrar( + AcmeRegistrarConfiguration config, + String serviceName, + StorkInfrastructure storkInfrastructure) { + return new AcmeServiceRegistrar(config); + } + +} diff --git a/content/docs/smallrye-stork/latest/_examples/examples/CachedAcmeServiceDiscovery.java b/content/docs/smallrye-stork/latest/_examples/examples/CachedAcmeServiceDiscovery.java new file mode 100644 index 000000000..204aa78c9 --- /dev/null +++ b/content/docs/smallrye-stork/latest/_examples/examples/CachedAcmeServiceDiscovery.java @@ -0,0 +1,31 @@ +package examples; + +import io.smallrye.mutiny.Uni; +import io.smallrye.stork.api.ServiceDiscovery; +import io.smallrye.stork.api.ServiceInstance; +import io.smallrye.stork.impl.CachingServiceDiscovery; +import io.smallrye.stork.impl.DefaultServiceInstance; +import io.smallrye.stork.utils.ServiceInstanceIds; + +import java.util.Collections; +import java.util.List; + +public class CachedAcmeServiceDiscovery extends CachingServiceDiscovery { + + private final String host; + private final int port; + + public CachedAcmeServiceDiscovery(CachedAcmeConfiguration configuration) { + super(configuration.getRefreshPeriod()); // (1) + this.host = configuration.getHost(); + this.port = Integer.parseInt(configuration.getPort()); + } + + @Override // (2) + public Uni> fetchNewServiceInstances(List previousInstances) { + // Retrieve services... + DefaultServiceInstance instance = + new DefaultServiceInstance(ServiceInstanceIds.next(), host, port, false); + return Uni.createFrom().item(() -> Collections.singletonList(instance)); + } +} diff --git a/content/docs/smallrye-stork/latest/_examples/examples/CachedAcmeServiceDiscoveryProvider.java b/content/docs/smallrye-stork/latest/_examples/examples/CachedAcmeServiceDiscoveryProvider.java new file mode 100644 index 000000000..56d627671 --- /dev/null +++ b/content/docs/smallrye-stork/latest/_examples/examples/CachedAcmeServiceDiscoveryProvider.java @@ -0,0 +1,32 @@ +package examples; + +import io.smallrye.stork.api.ServiceDiscovery; +import io.smallrye.stork.api.config.ServiceConfig; +import io.smallrye.stork.api.config.ServiceDiscoveryAttribute; +import io.smallrye.stork.api.config.ServiceDiscoveryType; +import io.smallrye.stork.impl.CachingServiceDiscovery; +import io.smallrye.stork.spi.ServiceDiscoveryProvider; +import io.smallrye.stork.spi.StorkInfrastructure; +import jakarta.enterprise.context.ApplicationScoped; + +@ServiceDiscoveryType("cached-acme") +@ServiceDiscoveryAttribute(name = "host", + description = "Host name of the service discovery server.", required = true) +@ServiceDiscoveryAttribute(name = "port", + description = "Hort of the service discovery server.", required = false) +@ServiceDiscoveryAttribute(name = "refresh-period", + description = "Service discovery cache refresh period.", + defaultValue = CachingServiceDiscovery.DEFAULT_REFRESH_INTERVAL) +@ApplicationScoped +public class CachedAcmeServiceDiscoveryProvider + implements ServiceDiscoveryProvider { + + @Override + public ServiceDiscovery createServiceDiscovery( + CachedAcmeConfiguration config, + String serviceName, + ServiceConfig serviceConfig, + StorkInfrastructure storkInfrastructure) { + return new CachedAcmeServiceDiscovery(config); + } +} diff --git a/content/docs/smallrye-stork/latest/_examples/examples/CustomExpirationCachedAcmeServiceDiscovery.java b/content/docs/smallrye-stork/latest/_examples/examples/CustomExpirationCachedAcmeServiceDiscovery.java new file mode 100644 index 000000000..b2e3823c9 --- /dev/null +++ b/content/docs/smallrye-stork/latest/_examples/examples/CustomExpirationCachedAcmeServiceDiscovery.java @@ -0,0 +1,44 @@ +package examples; + +import io.smallrye.mutiny.Uni; +import io.smallrye.stork.api.ServiceInstance; +import io.smallrye.stork.impl.CachingServiceDiscovery; +import io.smallrye.stork.impl.DefaultServiceInstance; +import io.smallrye.stork.utils.ServiceInstanceIds; + +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +public class CustomExpirationCachedAcmeServiceDiscovery extends CachingServiceDiscovery { + + private final String host; + private final int port; + + private AtomicBoolean invalidated = new AtomicBoolean(); + + public CustomExpirationCachedAcmeServiceDiscovery(CachedAcmeConfiguration configuration) { + super(configuration.getRefreshPeriod()); + this.host = configuration.getHost(); + this.port = Integer.parseInt(configuration.getPort()); + } + + @Override + public Uni> fetchNewServiceInstances(List previousInstances) { + // Retrieve services... + DefaultServiceInstance instance = + new DefaultServiceInstance(ServiceInstanceIds.next(), host, port, false); + return Uni.createFrom().item(() -> Collections.singletonList(instance)); + } + + @Override + public Uni> cache(Uni> uni) { + return uni.memoize().until(() -> invalidated.get()); + } + + @Override + public void invalidate() { + invalidated.set(true); + } + +} \ No newline at end of file diff --git a/content/docs/smallrye-stork/latest/_examples/examples/DefinitionExample.java b/content/docs/smallrye-stork/latest/_examples/examples/DefinitionExample.java new file mode 100644 index 000000000..aab2dc3d6 --- /dev/null +++ b/content/docs/smallrye-stork/latest/_examples/examples/DefinitionExample.java @@ -0,0 +1,30 @@ +package examples; + +import io.smallrye.stork.Stork; +import io.smallrye.stork.api.ServiceDefinition; +import io.smallrye.stork.loadbalancer.random.RandomConfiguration; +import io.smallrye.stork.servicediscovery.staticlist.StaticConfiguration; +import io.smallrye.stork.serviceregistration.staticlist.StaticRegistrarConfiguration; + +public class DefinitionExample { + + public static void example(Stork stork) { + String example = "localhost:8080, localhost:8081"; + + // A service using a static list of locations as discovery + // As not set, it defaults to round-robin to select the instance. + stork.defineIfAbsent("my-service", + ServiceDefinition.of(new StaticConfiguration().withAddressList(example))); + + // Another service using the random selection strategy, instead of round-robin + stork.defineIfAbsent("my-second-service", + ServiceDefinition.of(new StaticConfiguration().withAddressList(example), + new RandomConfiguration())); + + // Another service using the random selection strategy, instead of round-robin + // and a static service registrar + stork.defineIfAbsent("my-second-service", + ServiceDefinition.of(new StaticConfiguration().withAddressList(example), + new RandomConfiguration(), new StaticRegistrarConfiguration())); + } +} diff --git a/content/docs/smallrye-stork/latest/_examples/examples/HelloClient.java b/content/docs/smallrye-stork/latest/_examples/examples/HelloClient.java new file mode 100644 index 000000000..eec581608 --- /dev/null +++ b/content/docs/smallrye-stork/latest/_examples/examples/HelloClient.java @@ -0,0 +1,20 @@ +package examples; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + + + +@Path("/") +@RegisterRestClient(baseUri = "stork://hello-service/hello") +public interface HelloClient { + @POST + @Produces(MediaType.TEXT_PLAIN) + @Consumes(MediaType.TEXT_PLAIN) + String echo(String name); +} + diff --git a/content/docs/smallrye-stork/latest/_examples/examples/InitializationExample.java b/content/docs/smallrye-stork/latest/_examples/examples/InitializationExample.java new file mode 100644 index 000000000..cb93ca0bd --- /dev/null +++ b/content/docs/smallrye-stork/latest/_examples/examples/InitializationExample.java @@ -0,0 +1,18 @@ +package examples; + +import io.smallrye.stork.Stork; +import io.smallrye.stork.api.ServiceDefinition; +import io.smallrye.stork.api.ServiceInstance; +import io.smallrye.stork.loadbalancer.random.RandomConfiguration; +import io.smallrye.stork.servicediscovery.staticlist.StaticConfiguration; + +import java.time.Duration; + +public class InitializationExample { + + public static void main(String[] args) { + Stork.initialize(); + Stork stork = Stork.getInstance(); + // ... + } +} diff --git a/content/docs/smallrye-stork/latest/_examples/examples/LookupExample.java b/content/docs/smallrye-stork/latest/_examples/examples/LookupExample.java new file mode 100644 index 000000000..71e232d12 --- /dev/null +++ b/content/docs/smallrye-stork/latest/_examples/examples/LookupExample.java @@ -0,0 +1,27 @@ +package examples; + +import io.smallrye.mutiny.Uni; +import io.smallrye.stork.Stork; +import io.smallrye.stork.api.Service; +import io.smallrye.stork.api.ServiceDefinition; +import io.smallrye.stork.api.ServiceInstance; +import io.smallrye.stork.loadbalancer.random.RandomConfiguration; +import io.smallrye.stork.servicediscovery.staticlist.StaticConfiguration; + +import java.util.List; +import java.util.Map; + +public class LookupExample { + + public static void example(Stork stork) { + Service service = stork.getService("my-service"); + + // Gets all the available instances: + Uni> instances = service.getInstances(); + // Select one instance using the load balancing strategy + Uni instance = service.selectInstance(); + + // Gets all the managed services: + Map services = stork.getServices(); + } +} diff --git a/content/docs/smallrye-stork/latest/_examples/examples/ObservableInitializationExample.java b/content/docs/smallrye-stork/latest/_examples/examples/ObservableInitializationExample.java new file mode 100644 index 000000000..d18854291 --- /dev/null +++ b/content/docs/smallrye-stork/latest/_examples/examples/ObservableInitializationExample.java @@ -0,0 +1,13 @@ +package examples; + +import io.smallrye.stork.Stork; +import io.smallrye.stork.integration.ObservableStorkInfrastructure; + +public class ObservableInitializationExample { + + public static void main(String[] args) { + Stork.initialize(new ObservableStorkInfrastructure(new AcmeObservationCollector())); + Stork stork = Stork.getInstance(); + // ... + } +} diff --git a/content/docs/smallrye-stork/latest/_examples/examples/ObservationExample.java b/content/docs/smallrye-stork/latest/_examples/examples/ObservationExample.java new file mode 100644 index 000000000..299fc5b4e --- /dev/null +++ b/content/docs/smallrye-stork/latest/_examples/examples/ObservationExample.java @@ -0,0 +1,35 @@ +package examples; + +import io.smallrye.mutiny.Uni; +import io.smallrye.stork.Stork; +import io.smallrye.stork.api.Service; +import io.smallrye.stork.api.ServiceInstance; +import io.smallrye.stork.api.observability.ObservationCollector; +import io.smallrye.stork.api.observability.StorkObservation; + +import java.time.Duration; +import java.util.List; +import java.util.Map; + +import static examples.AcmeObservationCollector.*; + +public class ObservationExample { + + public static void example(Stork stork) { + Service service = stork.getService("my-service"); + + ObservationCollector observations = service.getObservations(); + + // Gets the time spent in service discovery and service selection even if any error happens + Duration overallDuration = ACME_STORK_EVENT.getOverallDuration(); + + // Gets the total number of instances discovered + int discoveredInstancesCount = ACME_STORK_EVENT.getDiscoveredInstancesCount(); + + // Gets the error raised during the process + Throwable failure = ACME_STORK_EVENT.failure(); + + // ... + + } +} diff --git a/content/docs/smallrye-stork/latest/_examples/examples/SpringBootInitializationExample.java b/content/docs/smallrye-stork/latest/_examples/examples/SpringBootInitializationExample.java new file mode 100644 index 000000000..d21242584 --- /dev/null +++ b/content/docs/smallrye-stork/latest/_examples/examples/SpringBootInitializationExample.java @@ -0,0 +1,29 @@ +package examples; + +import io.smallrye.stork.Stork; +import io.smallrye.stork.springboot.SpringBootApplicationContextProvider; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.DependsOn; + +@SpringBootApplication +public class SpringBootInitializationExample { + + public static void main(String[] args) { + SpringApplication.run(SpringBootInitializationExample.class, args); + } + + @Bean + @DependsOn("springBootApplicationContextProvider") + public Stork stork() { + Stork.initialize(); + Stork stork = Stork.getInstance(); + return stork; + } + + @Bean + public SpringBootApplicationContextProvider springBootApplicationContextProvider() { + return new SpringBootApplicationContextProvider(); + } +} diff --git a/content/docs/smallrye-stork/latest/_examples/examples/StorkApiExample.java b/content/docs/smallrye-stork/latest/_examples/examples/StorkApiExample.java new file mode 100644 index 000000000..d8713eb9f --- /dev/null +++ b/content/docs/smallrye-stork/latest/_examples/examples/StorkApiExample.java @@ -0,0 +1,40 @@ +package examples; + +import io.smallrye.stork.Stork; +import io.smallrye.stork.api.ServiceDefinition; +import io.smallrye.stork.api.ServiceInstance; +import io.smallrye.stork.loadbalancer.random.RandomConfiguration; +import io.smallrye.stork.servicediscovery.staticlist.StaticConfiguration; +import io.smallrye.stork.serviceregistration.staticlist.StaticRegistrarConfiguration; + +import java.time.Duration; + +public class StorkApiExample { + + public static void main(String[] args) { + Stork.initialize(); + Stork stork = Stork.getInstance(); + + String example = "localhost:8080, localhost:8082"; + + // A service using a static list of locations as discovery + // As not set, it defaults to round-robin to select the instance. + stork.defineIfAbsent("my-service", + ServiceDefinition.of(new StaticConfiguration().withAddressList(example))); + + // Another service using the random selection strategy, instead of round-robin + stork.defineIfAbsent("my-second-service", + ServiceDefinition.of(new StaticConfiguration().withAddressList(example), + new RandomConfiguration())); + + ServiceInstance instance = stork.getService("my-second-service").selectInstance() + .await().atMost(Duration.ofSeconds(1)); + System.out.println(instance.getHost() + ":" + instance.getPort()); + + // Another service using the random selection strategy, instead of round-robin + // and a static service registrar + stork.defineIfAbsent("my-third-service", + ServiceDefinition.of(new StaticConfiguration().withAddressList(example), + new RandomConfiguration(), new StaticRegistrarConfiguration())); + } +} diff --git a/content/docs/smallrye-stork/latest/_examples/examples/StorkEntryPointExample.java b/content/docs/smallrye-stork/latest/_examples/examples/StorkEntryPointExample.java new file mode 100644 index 000000000..99343bd51 --- /dev/null +++ b/content/docs/smallrye-stork/latest/_examples/examples/StorkEntryPointExample.java @@ -0,0 +1,14 @@ +package examples; + +import io.smallrye.stork.Stork; + +public class StorkEntryPointExample { + + public static void main(String[] args) { + Stork.initialize(); + Stork stork = Stork.getInstance(); + // ... + Stork.shutdown(); + } + +} diff --git a/content/docs/smallrye-stork/latest/_examples/examples/StorkServiceExample.java b/content/docs/smallrye-stork/latest/_examples/examples/StorkServiceExample.java new file mode 100644 index 000000000..c0b7dbb06 --- /dev/null +++ b/content/docs/smallrye-stork/latest/_examples/examples/StorkServiceExample.java @@ -0,0 +1,18 @@ +package examples; + +import io.smallrye.stork.api.Service; +import io.smallrye.stork.Stork; + +public class StorkServiceExample { + + public static void main(String[] args) { + Stork.initialize(); + Stork stork = Stork.getInstance(); + + Service service = stork.getService("my-service"); + + // ... + Stork.shutdown(); + } + +} diff --git a/content/docs/smallrye-stork/latest/_examples/examples/StorkServiceLookupExample.java b/content/docs/smallrye-stork/latest/_examples/examples/StorkServiceLookupExample.java new file mode 100644 index 000000000..70c03cdc1 --- /dev/null +++ b/content/docs/smallrye-stork/latest/_examples/examples/StorkServiceLookupExample.java @@ -0,0 +1,24 @@ +package examples; + +import java.time.Duration; +import java.util.List; + +import io.smallrye.stork.Stork; +import io.smallrye.stork.api.Service; +import io.smallrye.stork.api.ServiceInstance; + +public class StorkServiceLookupExample { + + public static void main(String[] args) { + Stork.initialize(); + Stork stork = Stork.getInstance(); + + Service service = stork.getService("my-service"); + List instances = service.getInstances() + .await().atMost(Duration.ofSeconds(5)); + + // ... + Stork.shutdown(); + } + +} diff --git a/content/docs/smallrye-stork/latest/_examples/examples/StorkServiceSelectionExample.java b/content/docs/smallrye-stork/latest/_examples/examples/StorkServiceSelectionExample.java new file mode 100644 index 000000000..a9e3dcc85 --- /dev/null +++ b/content/docs/smallrye-stork/latest/_examples/examples/StorkServiceSelectionExample.java @@ -0,0 +1,25 @@ +package examples; + +import java.time.Duration; + +import io.smallrye.stork.Stork; +import io.smallrye.stork.api.Service; +import io.smallrye.stork.api.ServiceInstance; + +public class StorkServiceSelectionExample { + + public static void main(String[] args) { + Stork.initialize(); + Stork stork = Stork.getInstance(); + + Service service = stork.getService("my-service"); + ServiceInstance instance = service.selectInstance() + .await().atMost(Duration.ofSeconds(5)); + + System.out.println(instance.getHost() + ":" + instance.getPort()); + + // ... + Stork.shutdown(); + } + +} diff --git a/content/docs/smallrye-stork/latest/_generated/composite-sd-attributes.adoc b/content/docs/smallrye-stork/latest/_generated/composite-sd-attributes.adoc new file mode 100644 index 000000000..ae556fc30 --- /dev/null +++ b/content/docs/smallrye-stork/latest/_generated/composite-sd-attributes.adoc @@ -0,0 +1,5 @@ +[cols=",,,",options="header",] +|=== +|Attribute |Mandatory |Default Value |Description +|`services` |Yes | |A comma-separated list of services that this services consists of. +|=== diff --git a/content/docs/smallrye-stork/latest/_generated/consul-sd-attributes.adoc b/content/docs/smallrye-stork/latest/_generated/consul-sd-attributes.adoc new file mode 100644 index 000000000..007fb5d01 --- /dev/null +++ b/content/docs/smallrye-stork/latest/_generated/consul-sd-attributes.adoc @@ -0,0 +1,17 @@ +[cols=",,,",options="header",] +|=== +|Attribute |Mandatory |Default Value |Description +|`consul-host` |No |`localhost` |The Consul host. +|`consul-port` |No |`8500` |The Consul port. +|`use-health-checks` |No |`true` |Whether to use health check. +|`application` |No | |The application name; if not defined Stork service name will be used. +|`refresh-period` |No |`5M` |Service discovery cache refresh period. +|`secure` |No | |whether the connection with the service should be encrypted with TLS. +|`ssl` |No |`false` |Whether to enable TLS/SSL when connecting to Consul (default: false) +|`trust-store-path` |No |++``++ |Path to the trust store file used to verify the Consul server certificate +|`trust-store-password` |No |++``++ |Password of the trust store +|`key-store-path` |No |++``++ |Path to the key store file containing the client certificate and private key +|`key-store-password` |No |++``++ |Password of the key store +|`verify-host` |No |`false` |Whether to enable hostname verification for the Consul TLS connection (default: false) +|`acl-token` |No |++``++ |Consul ACL token used for authentication when accessing the Consul API +|=== diff --git a/content/docs/smallrye-stork/latest/_generated/consul-sr-attributes.adoc b/content/docs/smallrye-stork/latest/_generated/consul-sr-attributes.adoc new file mode 100644 index 000000000..861f4a93b --- /dev/null +++ b/content/docs/smallrye-stork/latest/_generated/consul-sr-attributes.adoc @@ -0,0 +1,16 @@ +[cols=",,,",options="header",] +|=== +|Attribute |Mandatory |Default Value |Description +|`consul-host` |No |`localhost` |The Consul host. +|`consul-port` |No |`8500` |The Consul port. +|`health-check-url` |No |++``++ |The liveness http address. +|`health-check-interval` |No |`30s` |How often Consul performs the health check +|`health-check-deregister-after` |No |`1m` |How long after the check is in critical status Consul will remove the service from the catalogue. +|`ssl` |No |`false` |Whether to enable TLS/SSL when connecting to Consul (default: false) +|`trust-store-path` |No |++``++ |Path to the trust store file used to verify the Consul server certificate +|`trust-store-password` |No |++``++ |Password of the trust store +|`key-store-path` |No |++``++ |Path to the key store file containing the client certificate and private key +|`key-store-password` |No |++``++ |Password of the key store +|`verify-host` |No |`false` |Whether to enable hostname verification for the Consul TLS connection (default: false) +|`acl-token` |No |++``++ |Consul ACL token used for authentication when accessing the Consul API +|=== diff --git a/content/docs/smallrye-stork/latest/_generated/dns-sd-attributes.adoc b/content/docs/smallrye-stork/latest/_generated/dns-sd-attributes.adoc new file mode 100644 index 000000000..d1e538a95 --- /dev/null +++ b/content/docs/smallrye-stork/latest/_generated/dns-sd-attributes.adoc @@ -0,0 +1,14 @@ +[cols=",,,",options="header",] +|=== +|Attribute |Mandatory |Default Value |Description +|`dns-servers` |No |`none` |Comma separated list of dns servers. Servers can either be in the `server:port` or just `server` form. Use `none` to use the system resolver. +|`hostname` |No | |The hostname to look up; if not defined Stork service name will be used. +|`record-type` |No |`SRV` |Type of the DNS record. A, AAAA and SRV records are supported +|`port` |No | |Port of the service instances. Required if the record type is other than SRV. +|`refresh-period` |No |`5M` |Service discovery cache refresh period. +|`secure` |No | |Whether the connection with the service should be encrypted with TLS. +|`recursion-desired` |No |`true` |Whether DNS recursion is desired +|`resolve-srv` |No |`true` |Whether DNS resolution for SRV records is desired +|`dns-timeout` |No |`5s` |Timeout for DNS queries +|`fail-on-error` |No |`false` |Whether an error in retrieving service instances from one of the DNS servers should cause a failure of the discovery attempt. +|=== diff --git a/content/docs/smallrye-stork/latest/_generated/eureka-sd-attributes.adoc b/content/docs/smallrye-stork/latest/_generated/eureka-sd-attributes.adoc new file mode 100644 index 000000000..2a2581bcb --- /dev/null +++ b/content/docs/smallrye-stork/latest/_generated/eureka-sd-attributes.adoc @@ -0,0 +1,13 @@ +[cols=",,,",options="header",] +|=== +|Attribute |Mandatory |Default Value |Description +|`eureka-host` |Yes | |The Eureka server host. +|`eureka-port` |No |`8761` |The Eureka server port. +|`eureka-context-path` |No |`/` |The Eureka server root context path. +|`application` |No | |The Eureka application Id; if not defined Stork service name will be used +|`eureka-trust-all` |No |`false` |Enable/Disable the TLS certificate verification +|`eureka-tls` |No |`false` |Use TLS to connect to the Eureka server +|`instance` |No | |The Eureka application instance Id +|`refresh-period` |No |`5M` |Service discovery cache refresh period. +|`secure` |No |`false` |Whether is should select the secured endpoint of the retrieved services. +|=== diff --git a/content/docs/smallrye-stork/latest/_generated/eureka-sr-attributes.adoc b/content/docs/smallrye-stork/latest/_generated/eureka-sr-attributes.adoc new file mode 100644 index 000000000..e18d0ba66 --- /dev/null +++ b/content/docs/smallrye-stork/latest/_generated/eureka-sr-attributes.adoc @@ -0,0 +1,11 @@ +[cols=",,,",options="header",] +|=== +|Attribute |Mandatory |Default Value |Description +|`eureka-host` |No |`localhost` |The Eureka host. +|`eureka-port` |No |`8761` |The Eureka port. +|`eureka-context-path` |No |`/` |The Eureka server root context path. +|`eureka-trust-all` |No |`false` |Enable/Disable the TLS certificate verification +|`eureka-tls` |No |`false` |Use TLS to connect to the Eureka server +|`health-check-url` |No |++``++ |The liveness http address. +|`host-name` |No |++``++ |The host name to register in Eureka. If not set, the ipAddress is used. +|=== diff --git a/content/docs/smallrye-stork/latest/_generated/knative-sd-attributes.adoc b/content/docs/smallrye-stork/latest/_generated/knative-sd-attributes.adoc new file mode 100644 index 000000000..3b3fe0eb3 --- /dev/null +++ b/content/docs/smallrye-stork/latest/_generated/knative-sd-attributes.adoc @@ -0,0 +1,9 @@ +[cols=",,,",options="header",] +|=== +|Attribute |Mandatory |Default Value |Description +|`knative-host` |No | |The Knative API host. +|`knative-namespace` |No | |The namespace of the service. Use all to discover all namespaces. +|`application` |No | |The Knative application Id; if not defined Stork service name will be used. +|`refresh-period` |No |`5M` |Service discovery cache refresh period. +|`secure` |No | |Whether the connection with the service should be encrypted with TLS. +|=== diff --git a/content/docs/smallrye-stork/latest/_generated/kubernetes-sd-attributes.adoc b/content/docs/smallrye-stork/latest/_generated/kubernetes-sd-attributes.adoc new file mode 100644 index 000000000..d0bbb6980 --- /dev/null +++ b/content/docs/smallrye-stork/latest/_generated/kubernetes-sd-attributes.adoc @@ -0,0 +1,14 @@ +[cols=",,,",options="header",] +|=== +|Attribute |Mandatory |Default Value |Description +|`k8s-host` |No | |The Kubernetes API host. +|`k8s-namespace` |No | |The namespace of the service. Use all to discover all namespaces. +|`application` |No | |The Kubernetes application Id; if not defined Stork service name will be used. +|`port-name` |No | |The Kubernetes application port name. If not defined, when exposing multiple ports, Stork will use the first one. +|`refresh-period` |No |`5M` |Service discovery cache refresh period. +|`secure` |No | |Whether the connection with the service should be encrypted with TLS. +|`request-retry-backoff-limit` |No | |Maximum number of retry attempts allowed after a request failure. +|`request-retry-backoff-interval` |No | |Amount of time to wait between retry attempts after a request fails. +|`use-endpoint-slices` |No | |Use the EndpointSlice API instead +|`use-cluster-ip` |No |`false` |Use the Kubernetes Service ClusterIP instead of pod IPs. Stork returns a single instance with the ClusterIP and resolved port. +|=== diff --git a/content/docs/smallrye-stork/latest/_generated/least-requests-lb-attributes.adoc b/content/docs/smallrye-stork/latest/_generated/least-requests-lb-attributes.adoc new file mode 100644 index 000000000..60c10c523 --- /dev/null +++ b/content/docs/smallrye-stork/latest/_generated/least-requests-lb-attributes.adoc @@ -0,0 +1,4 @@ +[cols=",,,",options="header",] +|=== +|Attribute |Mandatory |Default Value |Description +|=== diff --git a/content/docs/smallrye-stork/latest/_generated/least-response-time-lb-attributes.adoc b/content/docs/smallrye-stork/latest/_generated/least-response-time-lb-attributes.adoc new file mode 100644 index 000000000..dd977d2d0 --- /dev/null +++ b/content/docs/smallrye-stork/latest/_generated/least-response-time-lb-attributes.adoc @@ -0,0 +1,7 @@ +[cols=",,,",options="header",] +|=== +|Attribute |Mandatory |Default Value |Description +|`declining-factor` |No |`0.9` |How much _score_ should decline in time, see Score calculation in the docs for details. +|`error-penalty` |No |`60s` |This load balancer treats an erroneous response as a response after this time. +|`use-secure-random` |No |`false` |Whether the load balancer should use a SecureRandom instead of a Random (default). Check https://stackoverflow.com/questions/11051205/difference-between-java-util-random-and-java-security-securerandom[this page] to understand the difference +|=== diff --git a/content/docs/smallrye-stork/latest/_generated/power-of-two-choices-lb-attributes.adoc b/content/docs/smallrye-stork/latest/_generated/power-of-two-choices-lb-attributes.adoc new file mode 100644 index 000000000..95a37a4df --- /dev/null +++ b/content/docs/smallrye-stork/latest/_generated/power-of-two-choices-lb-attributes.adoc @@ -0,0 +1,5 @@ +[cols=",,,",options="header",] +|=== +|Attribute |Mandatory |Default Value |Description +|`use-secure-random` |No |`false` |Whether the load balancer should use a SecureRandom instead of a Random (default). Check https://stackoverflow.com/questions/11051205/difference-between-java-util-random-and-java-security-securerandom[this page] to understand the difference +|=== diff --git a/content/docs/smallrye-stork/latest/_generated/random-lb-attributes.adoc b/content/docs/smallrye-stork/latest/_generated/random-lb-attributes.adoc new file mode 100644 index 000000000..95a37a4df --- /dev/null +++ b/content/docs/smallrye-stork/latest/_generated/random-lb-attributes.adoc @@ -0,0 +1,5 @@ +[cols=",,,",options="header",] +|=== +|Attribute |Mandatory |Default Value |Description +|`use-secure-random` |No |`false` |Whether the load balancer should use a SecureRandom instead of a Random (default). Check https://stackoverflow.com/questions/11051205/difference-between-java-util-random-and-java-security-securerandom[this page] to understand the difference +|=== diff --git a/content/docs/smallrye-stork/latest/_generated/static-sd-attributes.adoc b/content/docs/smallrye-stork/latest/_generated/static-sd-attributes.adoc new file mode 100644 index 000000000..469ca6230 --- /dev/null +++ b/content/docs/smallrye-stork/latest/_generated/static-sd-attributes.adoc @@ -0,0 +1,7 @@ +[cols=",,,",options="header",] +|=== +|Attribute |Mandatory |Default Value |Description +|`address-list` |Yes | |A comma-separated list of addresses (host:port). The default port is 80. +|`secure` |No | |Whether the connection with the service should be encrypted with TLS. Default is false, except if the host:port uses the port is 443. +|`shuffle` |No |`false` |Whether the list of address must be shuffled to avoid using the first address on every startup. +|=== diff --git a/content/docs/smallrye-stork/latest/_generated/static-sr-attributes.adoc b/content/docs/smallrye-stork/latest/_generated/static-sr-attributes.adoc new file mode 100644 index 000000000..8ec570617 --- /dev/null +++ b/content/docs/smallrye-stork/latest/_generated/static-sr-attributes.adoc @@ -0,0 +1,5 @@ +[cols=",,,",options="header",] +|=== +|Attribute |Mandatory |Default Value |Description +|`address-list` |Yes | |A comma-separated list of addresses (host:port). The default port is 80. +|=== diff --git a/content/docs/smallrye-stork/latest/_generated/sticky-lb-attributes.adoc b/content/docs/smallrye-stork/latest/_generated/sticky-lb-attributes.adoc new file mode 100644 index 000000000..e458f6468 --- /dev/null +++ b/content/docs/smallrye-stork/latest/_generated/sticky-lb-attributes.adoc @@ -0,0 +1,5 @@ +[cols=",,,",options="header",] +|=== +|Attribute |Mandatory |Default Value |Description +|`failure-backoff-time` |No |`0` |After how much time, a service instance that has failed can be reused. +|=== diff --git a/content/docs/smallrye-stork/latest/_nav.yml b/content/docs/smallrye-stork/latest/_nav.yml new file mode 100644 index 000000000..8346a2ff0 --- /dev/null +++ b/content/docs/smallrye-stork/latest/_nav.yml @@ -0,0 +1,70 @@ +title: SmallRye Stork +nav: +- title: Overview + page: index +- title: Concepts + page: concepts +- title: Programmatic API + page: programmatic-api +- title: Microprofile Configuration + page: microprofile-config +- title: Spring Boot Configuration + page: springboot-config +- title: Observability + page: observability +- title: Logging + page: logging +- title: Javadoc + url: https://javadoc.io/doc/io.smallrye.stork/smallrye-stork-api/latest/index.html +- title: Using Stork with Quarkus + page: quarkus +- title: Service Discovery + items: + - title: Overview + page: service-discovery/overview + - title: Consul + page: service-discovery/consul + - title: DNS + page: service-discovery/dns + - title: Kubernetes + page: service-discovery/kubernetes + - title: KNative + page: service-discovery/knative + - title: Eureka + page: service-discovery/eureka + - title: Composite + page: service-discovery/composite + - title: Static List + page: service-discovery/static-list + - title: Custom Service Discovery + page: service-discovery/custom-service-discovery +- title: Load-Balancing + items: + - title: Overview + page: load-balancer/overview + - title: Round Robin + page: load-balancer/round-robin + - title: Random + page: load-balancer/random + - title: Least Requests + page: load-balancer/least-requests + - title: Least Response Time + page: load-balancer/least-response-time + - title: Power Of Two Choices + page: load-balancer/power-of-two-choices + - title: Sticky + page: load-balancer/sticky + - title: Custom Load Balancer + page: load-balancer/custom-load-balancer +- title: Service Registration + items: + - title: Overview + page: service-registration/overview + - title: Consul + page: service-registration/consul + - title: Eureka + page: service-registration/eureka + - title: Static List + page: service-registration/static-list + - title: Custom Service Registration + page: service-registration/custom-service-registration diff --git a/content/docs/smallrye-stork/latest/concepts.adoc b/content/docs/smallrye-stork/latest/concepts.adoc new file mode 100644 index 000000000..1618802c1 --- /dev/null +++ b/content/docs/smallrye-stork/latest/concepts.adoc @@ -0,0 +1,108 @@ += Concepts +include::_attributes.adoc[] + +This page presents the concepts used in Stork. +When using Stork in a _managed_ environment, such as Quarkus, all these concepts are hidden, as you only configure the lookup and selection. +However, when using the programmatic API, you will use these concepts directly. + +image::images/diagrams/plantuml-383c9b353dda.svg[stork] + +== Process overview + +When using the programmatic API of Stork, you can: + +[arabic] +. Retrieve the _singleton_ `Stork` instance. This instance is configured with the set of `Service` it manages. +. Retrieve the `Service` you want to use. Each `Service` is associated with a name. +. Retrieve the `ServiceInstance` which will provide the metadata to access the actual service. + +image::images/diagrams/plantuml-a0003ba546d7.svg[sequence] + +Behind the scenes, Stork will handle the service lookup and selection. + +[NOTE] +==== + +The service lookup and selection are asynchronous operations. +Thus, the API returns instances of `Uni`. + +==== + +== Stork + +`io.smallrye.stork.Stork` is the entry-point of the API. +The `Stork` instance is a _singleton_. +It needs to be initialized once (when the application starts) and shutdown when the application stops: + +[source,java] +---- +include::{examples-dir}/examples/StorkEntryPointExample.java[indent=0] +---- + +During the initialization, Stork looks for `io.smallrye.stork.config.ConfigProvider` SPI provider and CDI beans (from 2.x version) and retrieves the list of managed services: + +* A service is identified by a *name*. +* A service has a service discovery configuration indicating how Stork will look for service instances +* A service can have a load-balancer configuration indicating how Stork can select the most appropriate instance. + +== Service + +A `io.smallrye.stork.Service` is the structure representing a _service_ used by the application. +Services are pre-configured with their name, service discovery, and optionally, their load-balancer. +You retrieve a `Service` using the `Stork#getService(String name)` method. + +[source,java] +---- +include::{examples-dir}/examples/StorkServiceExample.java[indent=0] +---- + +The `Service` lets you retrieve the list of `ServiceInstance`, or select a single one, when a load-balancer is configured. + +== Service Instance + +The `io.smallrye.stork.api.ServiceInstance` represents an actual instance of the service. +It provides the metadata to configure a _client_ to interact with that specific instance of service. + +[source,java] +---- +include::{examples-dir}/examples/StorkServiceLookupExample.java[indent=0] +---- + +The service selection is a two-steps process: + +[arabic] +. Service lookup - using the service discovery +. Service selection - using the load balancer + +[source,java] +---- +include::{examples-dir}/examples/StorkServiceSelectionExample.java[indent=0] +---- + +== Service Discovery + +The `io.smallrye.stork.api.ServiceDiscovery` represents a service discovery mechanism, such as DNS, Consul, or Eureka. + +You can implement a custom service discovery for Stork by implementing the `ServiceDiscoveryProvider` interface. +The corresponding `ServiceRegistrarProviderLoader` and `RegistrarConfiguration` classes will be automatically generated during compilation time. + +Please note that the `ServiceDiscovery` implementation must be non-blocking. + +== Load Balancer + +The `io.smallrye.stork.api.LoadBalancer` represents a load-balancer strategy, such as round-robin. + +To implement a custom load balancer for Stork, implement the `LoadBalancerProvider` interface. +The corresponding `LoadBalancerProviderLoader` and `Configuration` classes will be automatically generated during compilation time. + +Please note that the `LoadBalancer` implementation, similarly to `ServiceDiscovery` +must be non-blocking. + +== Service registration + +The `io.smallrye.stork.api.ServiceRegistrar` represents a service registration mechanism for Consul and Eureka. + +You can implement a custom service registrar for Stork by implementing the `ServiceRegistrarProvider` interface. +The corresponding `ServiceRegistrarProviderLoader` and `RegistrarConfiguration` classes will be automatically generated during compilation time. + +Please note that the `ServiceRegistrar` implementation must be non-blocking. diff --git a/content/docs/smallrye-stork/latest/images/diagrams/plantuml-383c9b353dda.svg b/content/docs/smallrye-stork/latest/images/diagrams/plantuml-383c9b353dda.svg new file mode 100644 index 000000000..0e4b865f3 --- /dev/null +++ b/content/docs/smallrye-stork/latest/images/diagrams/plantuml-383c9b353dda.svg @@ -0,0 +1 @@ +«entry-point»StorkServiceDiscoveryServiceInstanceLoadBalancer«named»ServiceKubernetesConsulCustom Service Discovery«default»RoundRobinLeastResponseTimeCustom Load Balancer*selectServiceInstance \ No newline at end of file diff --git a/content/docs/smallrye-stork/latest/images/diagrams/plantuml-3c6c7f0192c2.svg b/content/docs/smallrye-stork/latest/images/diagrams/plantuml-3c6c7f0192c2.svg new file mode 100644 index 000000000..59018984f --- /dev/null +++ b/content/docs/smallrye-stork/latest/images/diagrams/plantuml-3c6c7f0192c2.svg @@ -0,0 +1 @@ +ApplicationApplicationObservableStorkInfrastructureObservableStorkInfrastructureObservationCollectorObservationCollectorStorkStorkServiceServiceApplicationObservableStorkInfrastructureObservationCollectorStorkServiceApplicationObservableStorkInfrastructureObservationCollectorStorkService(1)instantiates(2)instantiates(3)ObservationCollector(4)ObservableStorkInfrastructure (5)initialize(observableInfrastructure)(6)instantiates (..., ObservationCollector, ...) \ No newline at end of file diff --git a/content/docs/smallrye-stork/latest/images/diagrams/plantuml-a0003ba546d7.svg b/content/docs/smallrye-stork/latest/images/diagrams/plantuml-a0003ba546d7.svg new file mode 100644 index 000000000..9c1a99915 --- /dev/null +++ b/content/docs/smallrye-stork/latest/images/diagrams/plantuml-a0003ba546d7.svg @@ -0,0 +1 @@ +ApplicationApplicationApplicationStorkStorkStorkConfigProviderConfigProviderConfigProviderServiceServiceServiceService DiscoveryService DiscoveryService DiscoveryLoad BalancerLoad BalancerLoad BalancerApplicationStorkConfigProviderServiceService DiscoveryLoad BalancerApplicationStorkConfigProviderServiceService DiscoveryLoad Balancer(1)initialize()(2)getConfig()(3): List<ServiceConfig> (4)getService("name")(5)Service  (6)selectServiceInstance()(7)getServiceInstances()(8)Uni<List<ServiceInstance>>(9)selectServiceInstance()(10)Uni<ServiceInstance>(11)Uni<ServiceInstance> \ No newline at end of file diff --git a/content/docs/smallrye-stork/latest/images/diagrams/plantuml-bc915c97a9fa.svg b/content/docs/smallrye-stork/latest/images/diagrams/plantuml-bc915c97a9fa.svg new file mode 100644 index 000000000..39d2fba6a --- /dev/null +++ b/content/docs/smallrye-stork/latest/images/diagrams/plantuml-bc915c97a9fa.svg @@ -0,0 +1 @@ +ApplicationStorkDNS ServerApplicationStorkDNS ServerApplicationStorkDNS Serverget service instancesget SRV records for hostnamelist of SRV recordslist of SRV record targets if no resolution desiredget A/AAAA records for each SRV record targetlist of A/AAAAlist of ServiceInstances \ No newline at end of file diff --git a/content/docs/smallrye-stork/latest/images/diagrams/plantuml-c8137b2f4bb7.svg b/content/docs/smallrye-stork/latest/images/diagrams/plantuml-c8137b2f4bb7.svg new file mode 100644 index 000000000..c2ebac7a1 --- /dev/null +++ b/content/docs/smallrye-stork/latest/images/diagrams/plantuml-c8137b2f4bb7.svg @@ -0,0 +1 @@ +ServiceServiceServiceServiceServiceObservationCollectorObservationCollectorObservationCollectorObservationCollectorObservationCollectorStorkObservationStorkObservationStorkObservationStorkObservationStorkObservationStorkEventHandlerStorkEventHandlerStorkEventHandlerStorkEventHandlerStorkEventHandlerServiceObservationCollectorStorkObservationStorkEventHandlerServiceObservationCollectorStorkObservationStorkEventHandler(1)create("serviceName", sd type, ss type)(2)instantiates(3)Registers start time(4)StorkObservation (5)onServiceDiscoverySuccess(List<ServiceInstance>)(6)Registers end servicediscovery time.Registers instances count  (7)onServiceDiscoveryFailure(Throwable)(8)Registers end servicediscovery time.Registers failure cause  (9)onServiceSelectionSuccess(instanceId)(10)Registers end serviceselection time.Registers instance idRegisters overall duration (11)complete(this) (12)onServiceSelectionFailure(Throwable))(13)Registers overall duration.Registers failure cause (14)complete(this) \ No newline at end of file diff --git a/content/docs/smallrye-stork/latest/images/problem-dark.png b/content/docs/smallrye-stork/latest/images/problem-dark.png new file mode 100644 index 000000000..4c601386a Binary files /dev/null and b/content/docs/smallrye-stork/latest/images/problem-dark.png differ diff --git a/content/docs/smallrye-stork/latest/images/problem-light.png b/content/docs/smallrye-stork/latest/images/problem-light.png new file mode 100644 index 000000000..cc0cff154 Binary files /dev/null and b/content/docs/smallrye-stork/latest/images/problem-light.png differ diff --git a/content/docs/smallrye-stork/latest/images/redhat_reversed.svg b/content/docs/smallrye-stork/latest/images/redhat_reversed.svg new file mode 100644 index 000000000..37fe83c48 --- /dev/null +++ b/content/docs/smallrye-stork/latest/images/redhat_reversed.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/content/docs/smallrye-stork/latest/images/solution-dark.png b/content/docs/smallrye-stork/latest/images/solution-dark.png new file mode 100644 index 000000000..800d0f978 Binary files /dev/null and b/content/docs/smallrye-stork/latest/images/solution-dark.png differ diff --git a/content/docs/smallrye-stork/latest/images/solution-light.png b/content/docs/smallrye-stork/latest/images/solution-light.png new file mode 100644 index 000000000..044bd3786 Binary files /dev/null and b/content/docs/smallrye-stork/latest/images/solution-light.png differ diff --git a/content/docs/smallrye-stork/latest/images/stork-white.png b/content/docs/smallrye-stork/latest/images/stork-white.png new file mode 100644 index 000000000..8ceae5659 Binary files /dev/null and b/content/docs/smallrye-stork/latest/images/stork-white.png differ diff --git a/content/docs/smallrye-stork/latest/index.adoc b/content/docs/smallrye-stork/latest/index.adoc new file mode 100644 index 000000000..094027edf --- /dev/null +++ b/content/docs/smallrye-stork/latest/index.adoc @@ -0,0 +1,45 @@ += Overview +include::_attributes.adoc[] + +SmallRye Stork `{project-version}` is a service discovery and client-side load-balancing framework. + +The essence of distributed systems resides in the interaction between _services_. +In modern architecture, you often have multiple instances of your service to share the load or improve the resilience by redundancy. +But, how do you select the _best_ instance of your service? +That's where Stork helps. +Stork is going to select the most appropriate instance. +It offers: + +* Extensible service discovery mechanisms +* Built-in support for Consul and Kubernetes +* Customizable client load-balancing strategies +* A programmatic API and a _managed_ approach +* A Quarkus integration, but Stork can be used in any environment + +== The problem + +In distributed systems, applications typically need to call one another. +In a monolithic application, components invoke one another through language-level method or procedure calls. +In a traditional distributed system deployment, services run at fixed, well-known locations (schemes, hosts, and ports) and can call one another using HTTP/REST or some RPC mechanism. The service locations are often hardcoded in the application configuration. +Nevertheless, a modern distributed system or microservice-based application typically runs in virtualized or containerized environments where the number of instances of a service and their locations change dynamically. +IPs get randomly assigned, and instances can be created or destroyed at any time. +With such dynamics, hard-coded locations are a dead-end. + +.the problem +image::images/problem-light.png[the problem] + +== The solution + +Stork handles the service lookup and selection. +It proposes an extensible set of service discovery mechanisms and load-balancing strategies. + +.the solution +image::images/solution-light.png[the solution] + +== What if the infrastructure provides such a feature? + +Some infrastructure, such as Kubernetes, provides service discoveries and load-balancing features. +However, these mechanisms often lack flexibility. You cannot influence the service instance selection, and the load-balancing strategy is generally a simple _round-robin_. + +Stork provides more flexibility in the service instance selection. For example, it can select the fastest instance (based on the previous calls) to improve the response time. +However, if you don't need that flexibility, just use the infrastructure layer. diff --git a/content/docs/smallrye-stork/latest/load-balancer/custom-load-balancer.adoc b/content/docs/smallrye-stork/latest/load-balancer/custom-load-balancer.adoc new file mode 100644 index 000000000..c8d02933b --- /dev/null +++ b/content/docs/smallrye-stork/latest/load-balancer/custom-load-balancer.adoc @@ -0,0 +1,145 @@ += Implement your own load balancer mechanism +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Stork is extensible, and you can implement your service selection (load-balancer) mechanism. + +== Dependencies + +To implement your _Load Balancer Provider_, make sure your project depends on Core and Configuration Generator. +The former brings classes necessary to implement custom load balancer, the latter contains an annotation processor that generates classes needed by Stork. + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-core + {project-version} + + + io.smallrye.stork + stork-configuration-generator + provided + + {project-version} + +---- + +== Implementing a load balancer provider + +Load balancer implementation consists of three elements: + +* `LoadBalancer` which is responsible for selecting service instances for a single Stork service, +* `LoadBalancerProvider` which creates instances of `LoadBalancer` for a given load balancer _type_, +* `$typeConfiguration` which is a configuration for the load balancer. This class is automatically generated. + +A _type_, for example `acme-load-balancer`, identifies each provider. +This _type_ is used in the configuration to reference the provider: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.load-balancer.type=acme-load-balancer +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.load-balancer.type=acme-load-balancer +---- + +-- + +==== + +A `LoadBalancerProvider` implementation needs to be annotated with `@LoadBalancerType` that defines the _type_. +Any configuration properties that the provider expects should be defined with `@LoadBalancerAttribute` annotations placed on the provider. +Optionally, you can also add `@ApplicationScoped` annotation in order to provide the load balancer implementation as CDI bean. + +A load balancer provider class should look as follows: + +[source,java] +---- +include::{examples-dir}/examples/AcmeLoadBalancerProvider.java[indent=0] +---- + +Note, that the `LoadBalancerProvider` interface takes a configuration class as a parameter. +This configuration class is generated automatically by the _Configuration Generator_. +Its name is created by appending `Configuration` to the load balancer type, like `AcmeLoadBalancerConfiguration`. + +The next step is to implement the `LoadBalancer` interface. + +The essence of load balancers' work happens in the `selectServiceInstance` method. The method returns a single `ServiceInstance` from a collection. + +[source,java] +---- +include::{examples-dir}/examples/AcmeLoadBalancer.java[indent=0] +---- + +This implementation is simplistic and just picks a random instance from the received list. + +Some load balancers make the pick based on statistics such as calls in progress or response times, or amount of errors of a service instance. +To collect this information in your load balancer, you can wrap the selected service instance into `ServiceInstanceWithStatGathering`. + +Load balancers based on statistics often expect that an operation using a selected service instance is marked as started before the next selection. +By default, Stork assumes that a `LoadBalancer` requires this and guards the calls accordingly. +If this is not the case for your implementation, override the `requiresStrictRecording()` method to return `false`. + +== Using your load balancer + +In the project using it, don't forget to add the dependency on the module providing your implementation. +Then, in the configuration, just add: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-discovery.type=... +stork.my-service.load-balancer.type=acme-load-balancer\ +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.type=... +quarkus.stork.my-service.load-balancer.type=acme-load-balancer +---- + +-- + +==== + +Then, Stork will use your implementation to select the `my-service` service instance. + +== Using your load balancer using the programmatic API + +When building your load balancer project, the configuration generator creates a configuration class. +This class can be used to configure your load balancer using the Stork programmatic API. + +[source,java] +---- +include::{examples-dir}/examples/AcmeSelectorApiUsage.java[indent=0] +---- + +Remember that attributes, like `my-attribute`, are declared using the `@LoadBalancerAttribute` annotation on the `LoadBalancerProvider` implementation. diff --git a/content/docs/smallrye-stork/latest/load-balancer/least-requests.adoc b/content/docs/smallrye-stork/latest/load-balancer/least-requests.adoc new file mode 100644 index 000000000..6a8babbc6 --- /dev/null +++ b/content/docs/smallrye-stork/latest/load-balancer/least-requests.adoc @@ -0,0 +1,59 @@ += Least Requests Load Balancing +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The `least-requests` load balancing strategy monitors the number of inflight calls and selects the less-used instance. + +This strategy keeps track of the inflight calls made by the application and picks the service instance with the smallest number of inflight requests: + +[arabic] +. when the selection happens, the service instance with the smallest number of inflight requests is selected, and this number is incremented +. when the operation completes, successfully or not, the number of inflight requests is decremented + +== Dependency + +First, you need to add the `least-requests` load-balancer to your project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-load-balancer-least-requests + {project-version} + +---- + +== Configuration + +For each service expected to use a least-response-time selection, configure the `load-balancer` to be `least-requests`: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.load-balancer.type=least-requests +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.load-balancer.type=least-requests +---- + +-- + +==== + +Supported configuration properties are the following: + +include::{generated-dir}/least-requests-lb-attributes.adoc[] diff --git a/content/docs/smallrye-stork/latest/load-balancer/least-response-time.adoc b/content/docs/smallrye-stork/latest/load-balancer/least-response-time.adoc new file mode 100644 index 000000000..9742a0b23 --- /dev/null +++ b/content/docs/smallrye-stork/latest/load-balancer/least-response-time.adoc @@ -0,0 +1,94 @@ += Least Response Time Load Balancing +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The `least-response-time` load balancing strategy collects response times of the calls made with service instances and picks an instance based on this information. + +Erroneous responses are treated as responses with a long response time, by default 60 seconds. This can be controlled with the `error-penalty` attribute. + +The algorithm for service instance selection is as follows: + +* if there is a service instance that wasn't used before - use it, otherwise: +* if there are any service instances with collected response times - select the one for which _score_ is the lowest, otherwise: +* select a random instance + +The _score_ for an instance decreases in time if an instance is not used. This way we ensure that instances that haven't been used in a long time, are retried. + +For the details on the _score_ calculation, see xref:#score-calculation[Score calculation] + +== Dependency + +To use this load balancer, start with adding the least-response-time load-balancer dependency to your project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-load-balancer-least-response-time + {project-version} + +---- + +== Configuration + +For each service expected to use a least-response-time selection, configure the `load-balancer` to be `least-response-time`: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.load-balancer.type=least-response-time +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.load-balancer.type=least-response-time +---- + +-- + +==== + +The following attributes are supported: + +include::{generated-dir}/least-response-time-lb-attributes.adoc[] + +== Score calculation + +The _score_ of a service instance is calculated by dividing a weighted sum of response times by sum of the weighs. The result is additionally adjusted to account for instances that haven't been used for a long time. + +Let: + +* $n$ denote how many instance selections were made so far +* $t++_++i$ denote the response time for call $i$ +* $n++_++i$ denote the number of instance selections done until the moment of recording the response time for call $i$ +* $n++_{++max}$ denote the number of instance selections at the moment of last call recorded with this instance +* $$ denote a configurable `declining-factor` + +The idea for the weight is to decrease the importance of the data collected long time (many calls) ago. For call $i$, the weight is calculated as follows: +$$ +w++_++i = ^ ++{++(n - n++_++i)} +$$ + +The score of a service instance is calculated as: +$$ +score(n) = ^++{++n - n++_{++max}} ++*++ = +^++{++n - n++_{++max}} ++*++ +$$ + +The `declining-factor` should be in $(0, 1++]++$ , the default is $0.9$. Using a lower value makes the older response times less important. + +Supported configuration properties are the following: + +include::{generated-dir}/least-response-time-lb-attributes.adoc[] diff --git a/content/docs/smallrye-stork/latest/load-balancer/overview.adoc b/content/docs/smallrye-stork/latest/load-balancer/overview.adoc new file mode 100644 index 000000000..62c2f482c --- /dev/null +++ b/content/docs/smallrye-stork/latest/load-balancer/overview.adoc @@ -0,0 +1,22 @@ += Overview +:version-root: ../ +include::{version-root}_attributes.adoc[] + +== Load Balancer / Service Selection in SmallRye Stork + +Once services are registered and discovered, the next critical step is selecting which service instance will handle a given request. +SmallRye Stork provides flexible load balancing strategies to efficiently distribute requests across multiple instances of a service. +This ensures optimal resource usage, improved performance, and high availability. + +=== Key Features: + +* *Multiple Load Balancing Strategies*: SmallRye Stork supports several built-in strategies for selecting service instances. +Check them out in the following dedicated sections. +* *Customizable Strategies*: You can define custom service selection strategies based on your unique use case or performance requirements, ensuring that the load balancer can adapt to specific needs. + +==== How it Works: + +Once a service has been registered and discovered, the load balancer comes into play when a client makes a request to that service. +Stork applies the configured load balancing strategy to select an instance from the available pool of discovered services. + +This feature ensures that your services remain responsive, scalable, and resilient, providing a smooth experience for both users and developers. diff --git a/content/docs/smallrye-stork/latest/load-balancer/power-of-two-choices.adoc b/content/docs/smallrye-stork/latest/load-balancer/power-of-two-choices.adoc new file mode 100644 index 000000000..9e73e9897 --- /dev/null +++ b/content/docs/smallrye-stork/latest/load-balancer/power-of-two-choices.adoc @@ -0,0 +1,63 @@ += Power Of Two Choices Load Balancing +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The `power-of-two-choices` load balancing selects two random service instances and then chooses the one with the least inflight requests. +It avoids the overhead of `least-requests` and the worst case for `random` where it selects a busy destination. + +This strategy acts as follows: + +[arabic] +. when the selection happens, it picks two random instances from the list, +. it returns the least loaded instance (based on the number of inflight requests), +. when the operation completes, successfully or not, the number of inflight requests for the instance is decremented. + +Check http://www.eecs.harvard.edu/~michaelm/NEWWORK/postscripts/twosurvey.pdf[The Power of Two Random Choices] paper to learn more about this pattern and the benefits. + +== Dependency + +First, you need to add the random load-balancer to your project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-load-balancer-power-of-two-choices + {project-version} + +---- + +== Configuration + +For each service expected to use a random service selection, configure the `load-balancer` to be `power-of-two-choices`: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.load-balancer.type=power-of-two-choices +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.load-balancer.type=power-of-two-choices +---- + +-- + +==== + +Supported configuration properties are the following: + +include::{generated-dir}/power-of-two-choices-lb-attributes.adoc[] diff --git a/content/docs/smallrye-stork/latest/load-balancer/random.adoc b/content/docs/smallrye-stork/latest/load-balancer/random.adoc new file mode 100644 index 000000000..f721585d7 --- /dev/null +++ b/content/docs/smallrye-stork/latest/load-balancer/random.adoc @@ -0,0 +1,53 @@ += Random Load Balancing +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The `random` load balancing is a straightforward service instance selection solution that picks a random instance every time. + +== Dependency + +First, you need to add the random load-balancer to your project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-load-balancer-random + {project-version} + +---- + +== Configuration + +For each service expected to use a random service selection, configure the `load-balancer` to be `random`: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.load-balancer.type=random +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.load-balancer.type=random +---- + +-- + +==== + +Supported attributes are the following: + +include::{generated-dir}/random-lb-attributes.adoc[] diff --git a/content/docs/smallrye-stork/latest/load-balancer/round-robin.adoc b/content/docs/smallrye-stork/latest/load-balancer/round-robin.adoc new file mode 100644 index 000000000..e1f768f10 --- /dev/null +++ b/content/docs/smallrye-stork/latest/load-balancer/round-robin.adoc @@ -0,0 +1,43 @@ += Round-Robin Load Balancing +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The round-robin is a straightforward load-balancing solution that just iterates over the set of service instances. +While being simple, this solution shares the load among the instances and may be sufficient in many cases. + +The round-robin strategy is the default load-balancing strategy. +It is provided by Stork itself, and so does not require an additional dependency. + +== Configuration + +There is no need to configure the load-balancing strategy to be `round-robin`. +Stork automatically uses this strategy when none are configured. + +However, you can also configure it explicitly as follows: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.load-balancer.type=round-robin +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.load-balancer.type=round-robin +---- + +-- + +==== diff --git a/content/docs/smallrye-stork/latest/load-balancer/sticky.adoc b/content/docs/smallrye-stork/latest/load-balancer/sticky.adoc new file mode 100644 index 000000000..8af192d26 --- /dev/null +++ b/content/docs/smallrye-stork/latest/load-balancer/sticky.adoc @@ -0,0 +1,52 @@ += Sticky Load Balancing +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The sticky load balancer selects a single service instance and keeps using it until it fails. +Then, it selects another one. + +It is possible to configure a backoff time to specify for how long a failing service instance should not be retried. + +Precisely, the load balancer works as follows: + +* if no service instance has been selected so far, select the first instance from the collection; +* else if the previously selected service instance has not failed, and is still available, return it; +* else return the first available service instance that has no recorded failure, if one exists; +* else, find the available instance for which the time since the last failure is the longest, and +** if the backoff time since the failure passed, return it; +** or, throw an `NoAcceptableServiceInstanceFoundException` as no acceptable instances are available. + +== Configuration + +To use the `sticky` load service selection strategy, set the load balancer type to `sticky`: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.load-balancer.type=sticky +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.load-balancer.type=sticky +---- + +-- + +==== + +The following attributes are supported: + +include::{generated-dir}/sticky-lb-attributes.adoc[] diff --git a/content/docs/smallrye-stork/latest/logging.adoc b/content/docs/smallrye-stork/latest/logging.adoc new file mode 100644 index 000000000..2f0a9ce7c --- /dev/null +++ b/content/docs/smallrye-stork/latest/logging.adoc @@ -0,0 +1,78 @@ += Logging Configuration +include::_attributes.adoc[] + +Stork uses https://github.com/jboss-logging/jboss-logging[JBoss Logging] for all its logging needs. JBoss Logging is a logging facade that can delegate to various logging frameworks such as Log4j, SLF4J, or JDK logging. + +== Logger Categories + +Stork uses the following main logger categories: + +=== Core Loggers + +* `io.smallrye.stork.Stork` - Main Stork class, logs service initialization and configuration +* `io.smallrye.stork.impl.CachingServiceDiscovery` - Service discovery caching operations +* `io.smallrye.stork.utils.StorkConfigUtils` - Configuration parsing and validation + +=== Service Discovery Loggers + +* `io.smallrye.stork.servicediscovery.kubernetes.KubernetesServiceDiscovery` - Kubernetes service discovery events +* `io.smallrye.stork.servicediscovery.dns.DnsServiceDiscovery` - DNS-based service discovery +* `io.smallrye.stork.servicediscovery.knative.KnativeServiceDiscovery` - KNative service discovery events + +=== Load Balancer Loggers + +* `io.smallrye.stork.loadbalancer.leastresponsetime.LeastResponseTimeLoadBalancer` - Least response time load balancer selection logic + +=== Service Registration Loggers + +* `io.smallrye.stork.serviceregistration.consul.ConsulServiceRegistrar` - Consul service registration/deregistration +* `io.smallrye.stork.serviceregistration.consul.ConsulServiceRegistrarProvider` - Consul registrar provider initialization +* `io.smallrye.stork.serviceregistration.staticlist.StaticListServiceRegistrar` - Static list service registration +* `io.smallrye.stork.serviceregistration.staticlist.StaticListServiceRegistrarProvider` - Static list registrar provider +* `io.smallrye.stork.serviceregistration.eureka.EurekaServiceRegistrar` - Eureka service registration/deregistration + +=== Configuration Provider Loggers + +* `io.smallrye.stork.microprofile.MicroProfileConfigProvider` - MicroProfile configuration loading + +== Configuring Log Levels with Quarkus + +In your `application.properties`: + +[source,properties] +---- +# Set global Stork log level +quarkus.log.category."io.smallrye.stork".level=DEBUG + +# Service discovery logging +quarkus.log.category."io.smallrye.stork.servicediscovery".level=DEBUG + +# Specific implementations +quarkus.log.category."io.smallrye.stork.servicediscovery.kubernetes.KubernetesServiceDiscovery".level=TRACE +quarkus.log.category."io.smallrye.stork.servicediscovery.dns.DnsServiceDiscovery".level=DEBUG + +# Load balancer logging +quarkus.log.category."io.smallrye.stork.loadbalancer".level=DEBUG + +# Service registration logging +quarkus.log.category."io.smallrye.stork.serviceregistration".level=INFO +quarkus.log.category."io.smallrye.stork.serviceregistration.consul.ConsulServiceRegistrar".level=DEBUG + +# Console output format +quarkus.log.console.format=%d{HH:mm:ss} %-5p [%c{2.}] (%t) %s%e%n +---- + +== Troubleshooting + +=== Too Many Logs + +If you're getting too many debug logs in production: + +[arabic] +. Set the root Stork logger to INFO or WARN: ++ +[source,properties] +---- +quarkus.log.category."io.smallrye.stork".level=INFO +---- +. Selectively enable DEBUG only for components you're troubleshooting diff --git a/content/docs/smallrye-stork/latest/microprofile-config.adoc b/content/docs/smallrye-stork/latest/microprofile-config.adoc new file mode 100644 index 000000000..48ba3a487 --- /dev/null +++ b/content/docs/smallrye-stork/latest/microprofile-config.adoc @@ -0,0 +1,30 @@ += Stork MicroProfile Config +include::_attributes.adoc[] + +Stork integrates with MicroProfile Configuration out of the box, enabling seamless access to configuration properties. +This documentation explains how Stork can retrieve configuration details from the MicroProfile Config file present in the classpath. +Quarkus uses this approach for reading configuration details from the MicroProfile Config file located within the classpath. + +== Dependency setup + +To enable MicroProfile Config integration in Stork, you need to include the following dependency: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + smallrye-stork-microprofile + {project-version} + +---- + +== Initializing Stork + +If your framework lacks a pre-configured Stork instance, you'll need to perform initialization: + +[source,java] +---- +include::{examples-dir}/examples/InitializationExample.java[indent=0] +---- + +Upon initialization, Stork scans for the `io.smallrye.stork.config.MicroProfileConfigProvider` SPI provider and CDI beans (from version 2.x onwards). It then builds a comprehensive list of managed services by parsing the properties configuration files. diff --git a/content/docs/smallrye-stork/latest/observability.adoc b/content/docs/smallrye-stork/latest/observability.adoc new file mode 100644 index 000000000..3409d5a75 --- /dev/null +++ b/content/docs/smallrye-stork/latest/observability.adoc @@ -0,0 +1,112 @@ += Stork Observability API +include::_attributes.adoc[] + +Stork proposes an observability API that automatically observes some parameters to show how the Stork service discovery and selection are behaving. + +For any _observation_ to happen, you need to provide your own implementation of an `ObservationCollector.` By default, Stork provides a no-op implementation. + +The `ObservationCollector` is responsible for instantiating the `StorkObservation`. + +The `StorkObservation` reacts to Stork events thanks to a `StorkEventHandler`. + +You can extend the metrics collection by extending the `StorkEventHandler` interface. + +The following sequence diagram shows how the observability is initialized : + +image::images/diagrams/plantuml-3c6c7f0192c2.svg[observability_sequence] + +The `StorkObservation` registers times, number of discovered instances, the selected instance and failures by reacting to the lifecycle of a Stork event such as: + +* start : Observation has been started. +The beginning time is registered. +It happens when the `ObservationCollector#create()` method gets called. +* service discovery success: a collection of instances has been successfully discovered for a service. +The end discovery time and number of instances are recorded. +It happens when the `StorkObservation#onServiceDiscoverySuccess` gets called. +* service discovery error: an error occurs when discovering a service. +The end discovery time and failure cause are captured. +It happens when the `StorkObservation#onServiceDiscoveryFailure` gets called. +* service selection success: an instance has been successfully selected from the collection. +The end selection time and selected instance ID are registered. +It happens when the `StorkObservation#onServiceSelectionSuccess` gets called. +* service selection error: an error occurred during selecting the instance. +End selection time and failure cause are registered. +It happens when the `StorkObservation#onServiceSelectionFailure` gets called. +* end: Observation has finished. Overall duration is registered. +It happens when the `StorkObservation#onServiceSelectionSuccess` gets called. + +The following sequence diagram represents the described observation process above: + +image::images/diagrams/plantuml-c8137b2f4bb7.svg[observation_sequence] + +== Implementing an observation collector + +An `ObservationCollector` implementation must override the `create` method to provide an instance of StorkObservation. +In addition, the user can access and enrich the observation data through the `StorkEventHandler`. + +A custom observation collector class should look as follows: + +[source,java] +---- +include::{examples-dir}/examples/AcmeObservationCollector.java[indent=0] +---- + +The next step is to initialize Stork with an `ObservableStorkInfrastructure`, taking an instance of your `ObservationCollector` as parameter. + +[source,java] +---- +include::{examples-dir}/examples/ObservableInitializationExample.java[indent=0] +---- + +Then, Stork uses your implementation to register metrics. + +== Observing service discovery and selection behaviours + +To access metrics registered by `StorkObservation`, use the following code: + +[source,java] +---- +include::{examples-dir}/examples/ObservationExample.java[indent=0] +---- + +Stork Observability with Quarkus + +Stork metrics are automatically enabled when using Stork together with the Micrometer extension in a Quarkus application. + +Micrometer collects the metrics of the rest and grpc client using Stork, as well as when using the Stork API. + +As an example, if you export the metrics to Prometheus, you will get: + +[source,text] +---- +# HELP stork_load_balancer_failures_total The number of failures during service selection. +# TYPE stork_load_balancer_failures_total counter +stork_load_balancer_failures_total{service_name="hello-service",} 0.0 +# HELP stork_service_selection_duration_seconds The duration of the selection operation +# TYPE stork_service_selection_duration_seconds summary +stork_service_selection_duration_seconds_count{service_name="hello-service",} 13.0 +stork_service_selection_duration_seconds_sum{service_name="hello-service",} 0.001049291 +# HELP stork_service_selection_duration_seconds_max The duration of the selection operation +# TYPE stork_service_selection_duration_seconds_max gauge +stork_service_selection_duration_seconds_max{service_name="hello-service",} 0.0 +# HELP stork_overall_duration_seconds_max The total duration of the Stork service discovery and selection operations +# TYPE stork_overall_duration_seconds_max gauge +stork_overall_duration_seconds_max{service_name="hello-service",} 0.0 +# HELP stork_overall_duration_seconds The total duration of the Stork service discovery and selection operations +# TYPE stork_overall_duration_seconds summary +stork_overall_duration_seconds_count{service_name="hello-service",} 13.0 +stork_overall_duration_seconds_sum{service_name="hello-service",} 0.001049291 +# HELP stork_service_discovery_failures_total The number of failures during service discovery +# TYPE stork_service_discovery_failures_total counter +stork_service_discovery_failures_total{service_name="hello-service",} 0.0 +# HELP stork_service_discovery_duration_seconds_max The duration of the discovery operation +# TYPE stork_service_discovery_duration_seconds_max gauge +stork_service_discovery_duration_seconds_max{service_name="hello-service",} 0.0 +# HELP stork_service_discovery_duration_seconds The duration of the discovery operation +# TYPE stork_service_discovery_duration_seconds summary +stork_service_discovery_duration_seconds_count{service_name="hello-service",} 13.0 +stork_service_discovery_duration_seconds_sum{service_name="hello-service",} 6.585046209 +# HELP stork_instances_count_total The number of service instances discovered +# TYPE stork_instances_count_total counter +stork_instances_count_total{service_name="hello-service",} 26.0 +---- diff --git a/content/docs/smallrye-stork/latest/programmatic-api.adoc b/content/docs/smallrye-stork/latest/programmatic-api.adoc new file mode 100644 index 000000000..32f17e23b --- /dev/null +++ b/content/docs/smallrye-stork/latest/programmatic-api.adoc @@ -0,0 +1,54 @@ += Stork Programmatic API +include::_attributes.adoc[] + +Stork proposes a programmatic API that lets you register new service Definitions and do manual lookup and selection. +When using the programmatic API of Stork, you can: +Retrieve the singleton Stork instance. This instance is configured with the set of Services it manages. +Register new service definition. +Retrieve the Service you want to use. Each Service is associated with a name. +Retrieve the ServiceInstance, which will provide the metadata to access the actual instance. + +== Initializing Stork + +If your framework does not already provide a configured `Stork` instance, you need to do: + +[source,java] +---- +include::{examples-dir}/examples/InitializationExample.java[indent=0] +---- + +== Adding service dynamically + +To register a new `ServiceDefinition`, use the `defineIfAbsent` method: + +[source,java] +---- +include::{examples-dir}/examples/DefinitionExample.java[indent=0] +---- + +The `ServiceDefinition` instances can be created from: + +* A service discovery configuration - these classes are provided by the service discovery implementations, +* An optional load balancer configuration - these classes are provided by the load balancer implementations + +Attributes from the service discovery and load balancer can be configured from the `Configuration` classes. + +== Looking for service instances + +To list the service instances for a given service, or to select an instance according to the load balancer strategy, use the following code: + +[source,java] +---- +include::{examples-dir}/examples/LookupExample.java[indent=0] +---- + +The lookup and selection methods are returning Uni as these processes are asynchronous. + +== All in one example + +The following snippet provides an _all in one_ example of the Stork programmatic API: + +[source,java] +---- +include::{examples-dir}/examples/StorkApiExample.java[indent=0] +---- diff --git a/content/docs/smallrye-stork/latest/quarkus.adoc b/content/docs/smallrye-stork/latest/quarkus.adoc new file mode 100644 index 000000000..08c9b3186 --- /dev/null +++ b/content/docs/smallrye-stork/latest/quarkus.adoc @@ -0,0 +1,79 @@ += Using Stork with Quarkus +include::_attributes.adoc[] + +https://quarkus.io[Quarkus] is a Kubernetes Native Java stack tailored for OpenJDK HotSpot and GraalVM. + +Quarkus REST Client Reactive and gRPC extensions come with built-in integration with SmallRye Stork. + +This page describes how to use REST Client Reactive with Stork. Using gRPC with Stork is similar. + +We will use the Consul service discovery and the round-robin load balancer as examples. + +== The project + +You can create a Quarkus project with the REST Client Reactive extension using https://code.quarkus.io[code.quarkus.io]. + +The corresponding https://quarkus.io/guides/rest-client-reactive[Quarkus guide] describes the extension in more detail. + +== The client + +To use the REST client to communicate with a remote endpoint, you need to create an interface that describes how the communication should work. +The client requires `baseUri` (or `baseUrl`) pointing to the address of the remote endpoint. + +To use Stork to determine the actual address, set the scheme of the URI to `stork` and the hostname of the URI to the name of the Stork service. + +For example, the `HelloClient` below will use the Stork service called `hello-service` to determine the address of the destination, and `/hello` as the base path for queries: + +[source,java] +---- +include::{examples-dir}/examples/HelloClient.java[indent=0] +---- + +== The service + +In Stork, a `Service` consists of service discovery and a load balancer. +The Service discovery is responsible for determining the `ServiceInstance`s, that is, available addresses for a service. +The load balancer picks a single `ServiceInstance` for a call. + +=== Dependencies + +To use the service discovery and the load balancer of your choosing, you need to add the appropriate dependencies to your application. +For example, if you wish to use Consul and load-balance the calls with round-robin, add the following to your `pom.xml`: + +[source,xml] +---- + + io.smallrye.stork + stork-service-discovery-consul + +---- + +[NOTE] +==== + +No need to add a dependency for the round-robin load-balancer. This strategy is built-in. +Also, no need to specify the stork version, Quarkus imports the Stork _BOM_. + +==== + +When using Eureka, Kubernetes, or any other service discovery mechanism, or a different load balancer, replace the dependencies above with the ones you need. +Based on the defined dependencies, Stork automatically registers providers for service discovery mechanisms and load balancers. + +=== The config + +The last piece of the puzzle is the actual service configuration. +If your Consul instance is running on `localhost` on port `8500`, service discovery configuration should look as follows: + +[source,properties] +---- +quarkus.stork.hello-service.service-discovery.type=consul +quarkus.stork.hello-service.service-discovery.consul-host=localhost +quarkus.stork.hello-service.service-discovery.consul-port=8500 +---- + +[NOTE] +==== + +When no load-balancing is configured, Stork uses a round-robin. + +==== diff --git a/content/docs/smallrye-stork/latest/service-discovery/composite.adoc b/content/docs/smallrye-stork/latest/service-discovery/composite.adoc new file mode 100644 index 000000000..7be362c60 --- /dev/null +++ b/content/docs/smallrye-stork/latest/service-discovery/composite.adoc @@ -0,0 +1,71 @@ += Composite Service Discovery +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Some organizations use hybrid infrastructure. In such an infrastructure, different service instances may be discoverable via different service discovery providers. E.g. when _serviceA_ is being migrated from a VM to Kubernetes, its older versions can be discovered via Consul and newer via Kubernetes. + +`composite` service discovery addresses this problem by letting you define a service that consists of multiple services. + +== Dependency + +To use the `composite` service discovery, first add the appropriate Service Discovery provider dependency to your project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-service-discovery-composite + {project-version} + +---- + +== Configuration + +For each service that should consist of multiple services, configure the service discovery `type`, and set the `services` property to a comma separated list of services: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-discovery.type=composite +stork.my-service.service-discovery.services=serviceA,serviceB + +stork.serviceA.service-discovery.type=... +stork.serviceB.service-discovery.type=... +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.type=composite +quarkus.stork.my-service.service-discovery.services=serviceA,serviceB + +quarkus.stork.serviceA.service-discovery.type=... +quarkus.stork.serviceB.service-discovery.type=... +---- + +-- + +==== + +Remember to define the services that make up your composite service. + +Be aware that Stork doesn't work as a standalone service discovery cluster. +Instead, it processes composite configurations—meaning it handles multiple configurations that may use different service discovery implementation. +For each specific configuration, Stork delegates the service discovery task to the appropriate service discovery provider. +If one of these providers doesn't respond or fails, it affects Stork's ability to resolve that specific configuration, but it doesn't mean Stork itself is faulty; +it relies on the performance of the service discovery systems it’s configured to work with. + +These are all the parameters of the composite service discovery: + +include::{generated-dir}/composite-sd-attributes.adoc[] diff --git a/content/docs/smallrye-stork/latest/service-discovery/consul.adoc b/content/docs/smallrye-stork/latest/service-discovery/consul.adoc new file mode 100644 index 000000000..210076247 --- /dev/null +++ b/content/docs/smallrye-stork/latest/service-discovery/consul.adoc @@ -0,0 +1,167 @@ += Consul Service Discovery +:version-root: ../ +include::{version-root}_attributes.adoc[] + +https://www.consul.io/[Consul] is a distributed, highly available, and data center aware solution to connect and configure applications across dynamic, distributed infrastructure. +It's often used as service discovery backend to register and locate the services composing your system. +Consul makes it simple for services to register themselves and to discover other services via a DNS or HTTP interface. +External services can be registered as well. + +As https://developer.hashicorp.com/consul/api-docs/agent/service#address[specified] in the Consul documentation, if the host address is not provided, Stork will automatically use the Consul node address for the instance. + +This page explains how Stork can use Consul to handle the service discovery and service registration. + +== Dependency + +First, you need to add the Stork Consul Service Discovery provider: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-service-discovery-consul + {project-version} + +---- + +=== Service discovery configuration + +For each service that should get the service instances from Consul, configure the service discovery `type`: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-discovery.type=consul +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.type=consul +---- + +-- + +==== + +== Secure communication with Consul + +Stork supports SSL/TLS encryption when connecting to a Consul server. This is useful when Consul is configured with HTTPS or when you need to authenticate using client certificates. + +To enable SSL/TLS, set the `ssl` attribute to `true` and configure the trust store and optionally the key store: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-discovery.type=consul +stork.my-service.service-discovery.consul-host=consul.example.com +stork.my-service.service-discovery.consul-port=8501 +stork.my-service.service-discovery.ssl=true +stork.my-service.service-discovery.trust-store-path=/path/to/truststore.jks +stork.my-service.service-discovery.trust-store-password=changeit +stork.my-service.service-discovery.verify-host=true +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.type=consul +quarkus.stork.my-service.service-discovery.consul-host=consul.example.com +quarkus.stork.my-service.service-discovery.consul-port=8501 +quarkus.stork.my-service.service-discovery.ssl=true +quarkus.stork.my-service.service-discovery.trust-store-path=/path/to/truststore.jks +quarkus.stork.my-service.service-discovery.trust-store-password=changeit +quarkus.stork.my-service.service-discovery.verify-host=true +---- + +-- + +==== + +For mutual TLS (mTLS) authentication, also configure the key store containing the client certificate: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-discovery.key-store-path=/path/to/keystore.jks +stork.my-service.service-discovery.key-store-password=changeit +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.key-store-path=/path/to/keystore.jks +quarkus.stork.my-service.service-discovery.key-store-password=changeit +---- + +-- + +==== + +== ACL Token Authentication + +If your Consul cluster has ACL enabled, you can provide an ACL token for authentication: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-discovery.acl-token=your-consul-acl-token +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.acl-token=your-consul-acl-token +---- + +-- + +==== + +Consul service discovery is configured with the following parameters: + +include::{generated-dir}/consul-sd-attributes.adoc[] diff --git a/content/docs/smallrye-stork/latest/service-discovery/custom-service-discovery.adoc b/content/docs/smallrye-stork/latest/service-discovery/custom-service-discovery.adoc new file mode 100644 index 000000000..04945b53b --- /dev/null +++ b/content/docs/smallrye-stork/latest/service-discovery/custom-service-discovery.adoc @@ -0,0 +1,202 @@ += Implement your own service discovery mechanism +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Stork is extensible, and you can implement your own service discovery mechanism. + +== Dependencies + +To implement your _Service Discovery Provider_, make sure your project depends on Core and Configuration Generator. +The former brings classes necessary to implement custom discovery, the latter contains an https://docs.oracle.com/en/java/javase/11/docs/api/java.compiler/javax/annotation/processing/Processor.html[annotation processor] that generates classes needed by Stork. + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-core + {project-version} + + + io.smallrye.stork + stork-configuration-generator + provided + + {project-version} + +---- + +== Implementing a service discovery provider + +Service discovery implementation consists of three elements: + +* `ServiceDiscovery` which is responsible for locating service instances for a single Stork service. +* `ServiceDiscoveryProvider` which creates instances of `ServiceDiscovery` for a given service discovery _type_. +* `$typeConfiguration` which is a configuration for the discovery. This class is automatically generated during the compilation (using an annotation processor). + +A _type_, for example, `acme`, identifies each provider. +This _type_ is used in the configuration to reference the provider: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-discovery.type=acme +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.type=acme +---- + +-- + +==== + +A `ServiceDiscoveryProvider` implementation needs to be annotated with `@ServiceDiscoveryType` that defines the _type_. +Any configuration properties that the provider expects should be defined with `@ServiceDiscoveryAttribute` annotations placed on the provider. +Optionally, you can also add `@ApplicationScoped` annotation in order to provide the service discovery implementation as CDI bean. + +A service discovery provider class should look as follows: + +[source,java] +---- +include::{examples-dir}/examples/AcmeServiceDiscoveryProvider.java[indent=0] +---- + +Note, that the `ServiceDiscoveryProvider` interface takes a configuration class as a parameter. This configuration class +is generated automatically by the _Configuration Generator_. +Its name is created by appending `Configuration` to the service discovery type, such as `AcmeConfiguration`. + +The next step is to implement the `ServiceDiscovery` interface: + +[source,java] +---- +include::{examples-dir}/examples/AcmeServiceDiscovery.java[indent=0] +---- + +This implementation is simplistic. +Typically, instead of creating a service instance with values from the configuration, you would connect to a service discovery backend, look for the service and build the list of service instance accordingly. +That's why the method returns a `Uni`. +Most of the time, the lookup is a remote operation. + +As you can see, the `AcmeConfiguration` class gives access to the configuration attribute. + +== Using your service discovery + +In the project using it, don't forget to add the dependency on the module providing your implementation. +Then, in the configuration, just add: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-discovery.type=acme +stork.my-service.service-discovery.host=localhost +stork.my-service.service-discovery.port=1234 +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.type=acme +quarkus.stork.my-service.service-discovery.host=localhost +quarkus.stork.my-service.service-discovery.port=1234 +---- + +-- + +==== + +Then, Stork will use your implementation to locate the `my-service` service. + +== Using your service discovery using the programmatic API + +When building your service discovery project, the configuration generator creates a configuration class. +This class can be used to configure your service discovery using the Stork programmatic API. + +[source,java] +---- +include::{examples-dir}/examples/AcmeDiscoveryApiUsage.java[indent=0] +---- + +Remember that attributes, like `host`, are declared using the `@ServiceDiscoveryAttribute` annotation on the `ServiceDiscoveryProvider` implementation. + +== Caching the service instances + +Your `ServiceDiscovery` implementation can extend `io.smallrye.stork.impl.CachingServiceDiscovery` to automatically _cache_ the service instances. +In this case, the retrieved set of `ServiceInstance` is cached and only updated after some time. +This duration is an additional configuration attribute. +For homogeneity, we recommend the following attribute: + +[source,java] +---- +@ServiceDiscoveryAttribute(name = "refresh-period", description = "Service discovery cache refresh period.", + defaultValue = CachingServiceDiscovery.DEFAULT_REFRESH_INTERVAL) +---- + +The following snippet extends the _acme_ service discovery with the `refresh-period` attribute: + +[source,java] +---- +include::{examples-dir}/examples/CachedAcmeServiceDiscoveryProvider.java[indent=0] +---- + +Extending `io.smallrye.stork.impl.CachingServiceDiscovery` changes the structure of the service discovery implementation: + +[source,java] +---- +include::{examples-dir}/examples/CachedAcmeServiceDiscovery.java[indent=0] +---- + +[arabic] +. Call the `super` constructor with the `refresh-period` value +. Implement `fetchNewServiceInstances` instead of `getServiceInstances`. +The method is called periodically, and the retrieved instances are cached. +This implementation is simplistic. + +If the retrieval fails, the error is reported, and Stork keeps the previously retrieved list of instances. + +=== Customizing the caching strategy + +Sometimes it can be useful to change this behaviour and customize the cache expiration strategy. + +For example, imagine you are using a backend service discovery where service instances can change very frequently. + +Moreover, contacting the backend service discovery can be expensive in terms of computing, +thus finding a good value for the refreshing time can be mission impossible. + +For these situations, Stork allows to implement a better expiration strategy for the cache. + +If you want to customize the expiration strategy, you need: +1. Implement the `cache` method where the expiration strategy should be defined. +2. Invalidate the cache when expiration condition evaluates to true. + +Look at the example bellow: + +[source,java] +---- +include::{examples-dir}/examples/CustomExpirationCachedAcmeServiceDiscovery.java[indent=0] +---- + +Additionally, you can check the xref:service-discovery/kubernetes.adoc#caching-the-service-instances[Kubernetes Service Discovery] for further details about an event-based invalidation example. diff --git a/content/docs/smallrye-stork/latest/service-discovery/dns.adoc b/content/docs/smallrye-stork/latest/service-discovery/dns.adoc new file mode 100644 index 000000000..7e32934bc --- /dev/null +++ b/content/docs/smallrye-stork/latest/service-discovery/dns.adoc @@ -0,0 +1,76 @@ += DNS Service Discovery +:version-root: ../ +include::{version-root}_attributes.adoc[] + +DNS is a name resolution protocol used to determine IP addresses for hostnames. +That makes it a natural fit for service discovery. +Consul and AWS Cloud Map provide DNS resolutions for service discovery. + +This page explains how Stork can use DNS to handle the service discovery. + +== DNS records + +DNS supports a https://en.wikipedia.org/wiki/List_of_DNS_record_types[variety of record types]. Stork can resolve hostnames to addresses based on https://en.wikipedia.org/wiki/SRV_record[_SRV_], A and AAAA records. All these types of records may return multiple addresses for a single hostname. You may choose to not resolve target hostnames. + +While _A_ and _AAAA_ records are quite similar, they just carry an IP (_v4_ for _A_ and _v6_ for _AAAA_), the _SRV_ records are different. +They contain a _weight_, a _target_ and a _port_ for a service instance. +The _target_ returned in an _SRV_ record needs to be resolved further by an _A_ or an _AAAA_ record. + +In short, it works as follows: + +image::images/diagrams/plantuml-bc915c97a9fa.svg[srv_sequence] + +== Dependency + +To use the DNS service discovery, you need to add the Stork DNS Service Discovery provider dependency to your project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-service-discovery-dns + {project-version} + +---- + +== Configuration + +Next, set the service discovery `type` to `dns`. +Additionally, you would usually specify the DNS server, or servers, to use for the discovery. +All in all, your configuration could look as follows: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-discovery.type=dns +# optional dns servers: +stork.my-service.service-discovery.dns-servers=my-dns-server:8221,my-dns-server2 +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.type=dns + +# optional dns servers: +quarkus.stork.my-service.service-discovery.dns-servers=my-dns-server:8221,my-dns-server2 +---- + +-- + +==== + +All the available parameters are as follows: + +include::{generated-dir}/dns-sd-attributes.adoc[] diff --git a/content/docs/smallrye-stork/latest/service-discovery/eureka.adoc b/content/docs/smallrye-stork/latest/service-discovery/eureka.adoc new file mode 100644 index 000000000..87b5c9208 --- /dev/null +++ b/content/docs/smallrye-stork/latest/service-discovery/eureka.adoc @@ -0,0 +1,71 @@ += Eureka Service Discovery +:version-root: ../ +include::{version-root}_attributes.adoc[] + +https://github.com/Netflix/eureka[Eureka] is a RESTful service that is primarily used in the AWS cloud for the purpose of discovery, load balancing, and failover of middle-tier servers. + +This page explains how Stork can use Eureka to handle the service discovery and service registration. + +== Dependency + +First, you need to add the Stork Eureka Service Discovery provider: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-service-discovery-eureka + {project-version} + +---- + +=== Service discovery configuration + +For each application instance expected to be registered in Eureka, configure the lookup: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-discovery.type=eureka +stork.my-service.service-discovery.eureka-host=localhost +stork.my-service.service-discovery.eureka-port=8761 +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.type=eureka +quarkus.stork.my-service.service-discovery.eureka-host=localhost +quarkus.stork.my-service.service-discovery.eureka-port=8761 +---- + +-- + +==== + +Stork looks for the service with the given name (`my-service` in the previous example). + +Supported attributes are the following: + +include::{generated-dir}/eureka-sd-attributes.adoc[] + +The `application` attribute is optional. +It uses the Stork service name (`my-service` in the previous configuration) if not set. + +The `instance` attribute allows selecting a specific instance. +Using this attribute prevents load-balancing as you will always select a single instance. + +The `secure` attribute indicates whether to connect to instances over TLS. +If set to `true`, instances without a secure port enabled are filtered out. +The connection host is always resolved from the `hostName` field of the Eureka instance (falling back to `ipAddr`), regardless of this setting. diff --git a/content/docs/smallrye-stork/latest/service-discovery/knative.adoc b/content/docs/smallrye-stork/latest/service-discovery/knative.adoc new file mode 100644 index 000000000..539fcdb5b --- /dev/null +++ b/content/docs/smallrye-stork/latest/service-discovery/knative.adoc @@ -0,0 +1,129 @@ += Knative Service Discovery +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Knative is a Kubernetes-based platform for serverless workloads. Knative provides a set of objects as Kubernetes Custom Resource Definitions (CRDs). +These resources are used to define and control how your serverless workload behaves on the cluster. +The Stork Knative service discovery implementation is very similar to the Kubernetes one. +Stork will ask for https://knative.dev/docs/serving/reference/serving-api/#serving.knative.dev/v1.Service[Knative services] to the cluster instead of vanilla https://kubernetes.io/docs/concepts/services-networking/service/#service-resource[Kubernetes services] used by the Kubernetes implementation. +To do so, Stork uses https://github.com/fabric8io/kubernetes-client/blob/master/extensions/knative/client/src/main/java/io/fabric8/knative/client/KnativeClient.java[Fabric 8 Knative Client] which is just an extension of Fabric8 Kubernetes Client. + +== Dependency + +First, you need to add the Stork Knative Service Discovery provider: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-service-discovery-knative + {project-version} + +---- + +=== A few words about server authentication. + +Stork uses Fabric8 Knative Client which is a https://github.com/fabric8io/kubernetes-client#readme[Fabric8 Kubernetes Client] extension to access the Kubernetes resources, concretely the `DefaultKnativeClient` implementation. + +Since Knative Client is just an extension of Fabric8 Kubernetes Client, it’s also possible to get an instance of KnativeClient from KubernetesClient. + +`DefaultKubernetesClient` will try to read the `~/.kube/config` file from your local machine and load the token for authenticating with the Kubernetes API server. + +The level of access (Roles) depends on the configured `ServiceAccount`. + +You can override this configuration if you want fine-grain control. + +==== Role-based access control (RBAC) + +If you're using a Kubernetes cluster with Role-Based Access Control (RBAC) enabled, the default permissions for a ServiceAccount don't allow it to list or modify any resources. +A `ServiceAccount`, a `Role` and a `RoleBinding` are needed in order to allow Stork to list the available service instances from the cluster or the namespace. + +An example that allows listing all endpoints could look something like this: + +[source,yaml] +---- +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: + namespace: +--- +kind: Role +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: + namespace: +rules: + - apiGroups: [""] # "" indicates the core API group + resources: ["endpoints", "pods"] # stork queries service endpoints and pods + verbs: ["get", "list"] +--- +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: RoleBinding +metadata: + name: + namespace: +subjects: + - kind: ServiceAccount + # Reference to upper's `metadata.name` + name: + # Reference to upper's `metadata.namespace` + namespace: +roleRef: + kind: Role + name: + apiGroup: rbac.authorization.k8s.io +---- + +== Configuration + +For each service expected to be exposed as _Kubernetes Service_, configure the lookup: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-knservice.service-discovery.type=knative +stork.my-knservice.service-discovery.knative-namespace=my-namespace +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-knservice.service-discovery.type=knative +quarkus.stork.my-knservice.service-discovery.knative-namespace=my-namespace +---- + +-- + +==== + +Stork looks for the _Knative Service_ with the given name (`my-knservice` in the previous example) in the specified namespace. + +Stork inspects the _Knative Service_ and retrieves the url of the service. + +Supported attributes are the following: + +include::{generated-dir}/knative-sd-attributes.adoc[] + +== Caching the service instances + +Contacting the cluster too much frequently can result in performance problems. It's why Knative Service discovery extends `io.smallrye.stork.impl.CachingServiceDiscovery` to automatically _cache_ the service instances. +Moreover, the caching expiration has been also improved in order to only update the retrieved set of `ServiceInstance` if some of them changes and an event is emitted. +This is done by creating an https://www.javadoc.io/static/io.fabric8/kubernetes-client-api/6.1.1/io/fabric8/kubernetes/client/informers/SharedIndexInformer.html[Informer], similar to a https://www.javadoc.io/static/io.fabric8/kubernetes-client-api/6.1.1/io/fabric8/kubernetes/client/Watch.html[Watch], able to observe the events on the Knative Service instances resources. + +Note that: +- the cache is invalidated when an event is received. +- the cache is validated once the instances are retrieved from the cluster, in the `fetchNewServiceInstances` method. +- the `cache` method is overrided to customize the expiration strategy. In this case the collection of service instances will be kept until an event occurs. diff --git a/content/docs/smallrye-stork/latest/service-discovery/kubernetes.adoc b/content/docs/smallrye-stork/latest/service-discovery/kubernetes.adoc new file mode 100644 index 000000000..1bc26e0a4 --- /dev/null +++ b/content/docs/smallrye-stork/latest/service-discovery/kubernetes.adoc @@ -0,0 +1,335 @@ += Kubernetes Service Discovery +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Kubernetes has a built-in support for service discovery and load-balancing. +However, you may need more flexibility to carefully select the service instance you want. + +This page explains how Stork can use the Kubernetes API to handle the service discovery. + +== Dependency + +First, you need to add the Stork Kubernetes Service Discovery provider: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-service-discovery-kubernetes + {project-version} + +---- + +=== A few words about server authentication. + +Stork uses https://github.com/fabric8io/kubernetes-client#readme[Fabric8 Kubernetes Client] to access the Kubernetes resources, concretely the `DefaultKubernetesClient` implementation. + +It will try to read the `~/.kube/config` file from your local machine and load the token for authenticating with the Kubernetes API server. + +If you are using the Stork Kubernetes discovery provider from inside a _Pod_, it loads `~/.kube/config` from the container file system. + +This file is automatically mounted inside the Pod. + +The level of access (Roles) depends on the configured `ServiceAccount`. + +You can override this configuration if you want fine-grain control. + +==== Role-based access control (RBAC) + +If you're using a Kubernetes cluster with Role-Based Access Control (RBAC) enabled, the default permissions for a ServiceAccount don't allow it to list or modify any resources. +A `ServiceAccount`, a `Role` and a `RoleBinding` are needed in order to allow Stork to list the available service instances from the cluster or the namespace. + +An example that allows listing all endpoints could look something like this: + +[source,yaml] +---- +------ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: + namespace: +--- +kind: Role +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: + namespace: +rules: + - apiGroups: [""] # "" indicates the core API group + resources: ["endpoints", "pods", "services"] # stork queries service endpoints, pods and services + verbs: ["get", "list"] +--- +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: RoleBinding +metadata: + name: + namespace: +subjects: + - kind: ServiceAccount + # Reference to upper's `metadata.name` + name: + # Reference to upper's `metadata.namespace` + namespace: +roleRef: + kind: Role + name: + apiGroup: rbac.authorization.k8s.io +---- + +== Configuration + +For each service expected to be exposed as _Kubernetes Service_, configure the lookup: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-discovery.type=kubernetes +stork.my-service.service-discovery.k8s-namespace=my-namespace +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.type=kubernetes +quarkus.stork.my-service.service-discovery.k8s-namespace=my-namespace +---- + +-- + +==== + +Stork looks for the _Kubernetes Service_ with the given name (`my-service` in the previous example) in the specified namespace. + +Instead of using the _Kubernetes Service_ IP directly, and let Kubernetes handle the selection and balancing, Stork inspects the service and retrieves the list of _pods_ providing the service. +Then, it can select the instance. + +== EndpointSlice support (Experimental) + +This feature is experimental. It is opt-in, and the API or behavior may change based on user feedback and Kubernetes evolution. +Recent Kubernetes versions expose service endpoints using *EndpointSlices* (`discovery.k8s.io/v1`), +which are the recommended and scalable replacement for classic `Endpoints`. + +Stork supports EndpointSlices and can use them transparently for service discovery. + +=== Why EndpointSlices + +Compared to classic `Endpoints`, EndpointSlices: + +* Scale better for services with many pods. +* Reduce load on the Kubernetes API server. +* Produce smaller and more frequent updates. +* Are the preferred API in modern Kubernetes clusters. + +When available, EndpointSlices are the recommended source for service discovery. + +== Configuration + +EndpointSlices can be enabled or disabled explicitly. By default, Stork uses automatic detection. + +=== Explicit configuration + +You can force the use of EndpointSlices per service (note that it will still fallback to Endpoints if EndpointSlices are not available for the service): + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.use-endpoint-slices=true +---- + +Or explicitly disable them: + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.use-endpoint-slices=false +---- + +=== Automatic detection (default behavior) + +If `use-endpoint-slices` is not configured, Stork applies the following logic: + +[arabic] +. If the Kubernetes cluster exposes the `discovery.k8s.io/v1` API *and* +. EndpointSlices exist for the target service + +→ Stork uses EndpointSlices. + +Otherwise, it falls back to classic Endpoints. + +This ensures backward compatibility with older clusters and services while automatically benefiting from EndpointSlices when available. + +The detection is performed *asynchronously* (off the event loop) and its result is cached for the lifetime of the service discovery instance. +If the cluster API is temporarily unreachable during detection, Stork retries a few times before falling back to classic Endpoints. +This means a transient connectivity issue at startup does not permanently disable EndpointSlice discovery, but it may cause Stork to fall back to Endpoints for the lifetime of that instance. + +Falling back to classic Endpoints is safe even on modern clusters: Kubernetes maintains `Endpoints` objects alongside `EndpointSlices` for backward compatibility, so both APIs reflect the same service instances. + +== Service instance resolution with EndpointSlices + +When using EndpointSlices, Stork does *not* resolve Pods. + +Instead, service instances are derived directly from the EndpointSlice objects: + +* `endpoint.addresses` → instance host +* `endpointSlice.ports` → instance port +* `endpointSlice.labels` → instance metadata + +Each EndpointSlice represents a homogeneous group of endpoints. +All addresses in a slice share the same set of ports. +If different port combinations exist, Kubernetes exposes them as separate EndpointSlices. + +This guarantees that all `(address, port)` combinations returned by Stork are valid. + +== ClusterIP mode + +By default, Stork resolves individual *pod IPs* from Kubernetes Endpoints (or EndpointSlices). +In ClusterIP mode, Stork resolves to the *ClusterIP and port* of the Kubernetes Service instead, without inspecting the backing pods. + +The `use-cluster-ip` attribute enables this mode. + +=== When to use ClusterIP mode + +ClusterIP mode is useful when: + +* You have a *multi-port Service* and want Stork to resolve the port by name, without needing to know actual port numbers. +* You don't need client-side load balancing — for example, because `kube-proxy`, a service mesh, or another network-level mechanism already handles traffic distribution. +* You want to *control the resolution strategy through configuration*, without changing application code. + +=== Configuration + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-discovery.type=kubernetes +stork.my-service.service-discovery.k8s-namespace=my-namespace +stork.my-service.service-discovery.use-cluster-ip=true +stork.my-service.service-discovery.port-name=http +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.type=kubernetes +quarkus.stork.my-service.service-discovery.k8s-namespace=my-namespace +quarkus.stork.my-service.service-discovery.use-cluster-ip=true +quarkus.stork.my-service.service-discovery.port-name=http +---- + +-- + +==== + +In this mode, Stork queries the Kubernetes `Service` resource directly and returns a single `ServiceInstance` with: + +* *Host* — the Service's ClusterIP (e.g., `10.96.0.15`) +* *Port* — resolved from the Service spec, matching `port-name` when multiple ports are defined + +Since only one instance is returned, any configured Stork load balancer becomes a passthrough. + +[NOTE] +==== + +ClusterIP mode does not support headless Services (`clusterIP: None`). +If the target Service is headless, Stork returns an empty list of service instances. + +==== + +=== Multi-port Services + +ClusterIP mode works well with multi-port Services. +Use the `port-name` attribute to select the desired port: + +[source,yaml] +---- +apiVersion: v1 +kind: Service +metadata: + name: my-service +spec: + ports: + - name: http + port: 8080 + - name: grpc + port: 50051 +---- + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-discovery.type=kubernetes +stork.my-service.service-discovery.use-cluster-ip=true +stork.my-service.service-discovery.port-name=grpc +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.type=kubernetes +quarkus.stork.my-service.service-discovery.use-cluster-ip=true +quarkus.stork.my-service.service-discovery.port-name=grpc +---- + +-- + +==== + +This resolves to `10.96.x.x:50051` — the ClusterIP with the `grpc` port without the application needing to know the actual port number. +If `port-name` is not set, Stork uses the first port defined in the Service spec. + +Supported attributes are the following: + +include::{generated-dir}/kubernetes-sd-attributes.adoc[] + +== Caching the service instances + +Contacting the cluster too much frequently can result in performance problems. It's why Kubernetes Service discovery extends `io.smallrye.stork.impl.CachingServiceDiscovery` to automatically _cache_ the service instances. +Moreover, the caching expiration has been also improved in order to only update the retrieved set of `ServiceInstance` if some of them changes and an event is emitted. +This is done by creating an https://www.javadoc.io/static/io.fabric8/kubernetes-client-api/6.1.1/io/fabric8/kubernetes/client/informers/SharedIndexInformer.html[Informer], similar to a https://www.javadoc.io/static/io.fabric8/kubernetes-client-api/6.1.1/io/fabric8/kubernetes/client/Watch.html[Watch], able to observe the events on the service instances resources. + +Note that: +- the cache is invalidated when an event is received. +- the cache is validated, if and only if, the instances are retrieved successfully from the cluster, in the `fetchNewServiceInstances` method. +- In case of an error contacting the cluster (e.g., network failure, API server unreachable), a retry mechanism is triggered. +The system will attempt to contact the cluster up to the number of times specified by request-retry-backoff-limit, waiting request-retry-backoff-interval milliseconds between each attempt. +By default, retries are disabled to prevent the system from entering an infinite loop of calls to an unresponsive cluster. +- If no service instances are found (e.g., no matching endpoints, headless service in ClusterIP mode), Stork returns an empty list. +- the `cache` method is overridden to customize the expiration strategy. In this case the collection of service instances will be kept until an event occurs. + +When EndpointSlice are selected, Stork creates informers on the `EndpointSlice` resources instead of classic `Endpoints`. +The caching behavior remains unchanged. + +In ClusterIP mode, Stork creates an Informer on the Kubernetes `Service` resource (instead of Endpoints or EndpointSlices). +The cache is invalidated when the Service is added, updated, or deleted. +The same caching behavior described above applies. diff --git a/content/docs/smallrye-stork/latest/service-discovery/overview.adoc b/content/docs/smallrye-stork/latest/service-discovery/overview.adoc new file mode 100644 index 000000000..42531f0d8 --- /dev/null +++ b/content/docs/smallrye-stork/latest/service-discovery/overview.adoc @@ -0,0 +1,34 @@ += Overview +:version-root: ../ +include::{version-root}_attributes.adoc[] + +== Service Discovery in SmallRye Stork + +As already introduced, service discovery is a crucial part of modern microservices architectures. +It allows services to dynamically discover the location of other services at runtime, which is particularly useful in distributed systems where services may scale up or down, +or change their network addresses. + +SmallRye Stork provides a flexible and extensible mechanism for service discovery. +It supports out of the box some service discovery such as Kubernetes or Consul but the main strength of it is customization so you can easily create your own implementation related on your business for example. +Stork allows services to communicate with each other without requiring hardcoded addresses, making it an ideal solution for microservices deployments. +SmallRye Stork brings this capability to clients for Quarkus applications but it's vendor agnostic so you easily use it with other solutions and even in standalone mode. + +== Service discovery caching overview + +Stork provides a built-in caching mechanism for service discovery results. +By default, each service discovery implementation may repeatedly contact its backend (e.g., Consul, Kubernetes, Eureka) to retrieve the current list of service instances. +Since these lookups can be expensive or frequent, Stork allows service discovery providers to cache the retrieved instances for a configurable period of time. + +* Cached results are reused for subsequent lookups, avoiding unnecessary backend calls. +* The cache is automatically refreshed after the configured `refresh-period`. +* If a refresh fails, Stork keeps serving the previously retrieved list of instances until a new successful refresh occurs. + +In addition to the time-based refresh mechanism, Stork also exposes an *cache invalidation API* . +This API can be used to explicitly clear the cached results, forcing the next lookup to perform a new service discovery call. +This is especially useful when the service discovery backend supports event-based notifications (for example, Consul blocking queries), allowing the cache to be invalidated as soon as a change is detected. + +____ +For more details about how to enable caching in a custom service discovery implementation, see the xref:service-discovery/custom-service-discovery.adoc#caching-the-service-instances[Caching the service instances] section. +____ + +You can explore the different implementations and learn how to create your own in the following sections. diff --git a/content/docs/smallrye-stork/latest/service-discovery/static-list.adoc b/content/docs/smallrye-stork/latest/service-discovery/static-list.adoc new file mode 100644 index 000000000..4e2b066f3 --- /dev/null +++ b/content/docs/smallrye-stork/latest/service-discovery/static-list.adoc @@ -0,0 +1,56 @@ += Static List Service Discovery +:version-root: ../ +include::{version-root}_attributes.adoc[] + +In some situations, such as demos, development, or testing, you may want to mock the service discovery by providing a predefined list of service instances. +For this purpose, Stork comes with a `static` service discovery type. + +== Dependency + +To use the `static` service discovery, first add the appropriate Service Discovery provider dependency to your project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-service-discovery-static-list + {project-version} + +---- + +== Configuration + +For each service that should use the static list of service instances configure the service registrar `type`: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-discovery.type=static +stork.my-service.service-discovery.address-list=localhost:8080,localhost:8081 +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.type=static +quarkus.stork.my-service.service-discovery.address-list=localhost:8080,localhost:8081 +---- + +-- + +==== + +These are all the static service discovery parameters: + +include::{generated-dir}/static-sd-attributes.adoc[] diff --git a/content/docs/smallrye-stork/latest/service-registration/consul.adoc b/content/docs/smallrye-stork/latest/service-registration/consul.adoc new file mode 100644 index 000000000..10fe6d6ea --- /dev/null +++ b/content/docs/smallrye-stork/latest/service-registration/consul.adoc @@ -0,0 +1,228 @@ += Consul Service Registration +:version-root: ../ +include::{version-root}_attributes.adoc[] + +https://www.consul.io/[Consul] is a distributed, highly available, and data center aware solution to connect and configure applications across dynamic, distributed infrastructure. +It's often used as service discovery backend to register and locate the services composing your system. +Consul makes it simple for services to register themselves and to discover other services via a DNS or HTTP interface. +External services can be registered as well. + +This page explains how Stork can use Consul to handle the service registration. + +== Dependency + +First, you need to add the Stork Consul Service Registration provider: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-service-registration-consul + {project-version} + +---- + +== Service registration configuration + +For each service that should register the service instances in Consul, configure the service registrar `type`: + +[.tabs] +==== + +stork standalone:: ++ +-- + +-- + +==== + +[source,properties] +---- +stork.my-service.service-registrar.type=consul +---- + +[.tabs] +==== + +stork in quarkus:: ++ +-- + +-- + +==== + +[source,properties] +---- +quarkus.stork.my-service.service-registrar.type=consul +---- + +Consul service registrar is configured with the following parameters: + +include::{generated-dir}/consul-sr-attributes.adoc[] + +== Service deregistration configuration + +There is no specific configuration required to enable deregistration; however, you must ensure that a consul service registrar is configured for the service: + +[.tabs] +==== + +stork standalone:: ++ +-- + +-- + +==== + +[source,properties] +---- +stork.my-service.service-registrar.type=consul +---- + +[.tabs] +==== + +stork in quarkus:: ++ +-- + +-- + +==== + +[source,properties] +---- +quarkus.stork.my-service.service-registrar.type=consul +---- + +As with registration, deregistration relies on the service name. + +== Secure communication with Consul + +When your Consul cluster uses TLS/SSL encryption, you can configure Stork to establish secure connections. Enable SSL and provide the necessary trust store configuration: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-registrar.type=consul +stork.my-service.service-registrar.consul-host=localhost +stork.my-service.service-registrar.consul-port=8501 +stork.my-service.service-registrar.ssl=true +stork.my-service.service-registrar.trust-store-path=/path/to/truststore.jks +stork.my-service.service-registrar.trust-store-password=changeit +stork.my-service.service-registrar.verify-host=true +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-registrar.type=consul +quarkus.stork.my-service.service-registrar.consul-host=localhost +quarkus.stork.my-service.service-registrar.consul-port=8501 +quarkus.stork.my-service.service-registrar.ssl=true +quarkus.stork.my-service.service-registrar.trust-store-path=/path/to/truststore.jks +quarkus.stork.my-service.service-registrar.trust-store-password=changeit +quarkus.stork.my-service.service-registrar.verify-host=true +---- + +-- + +==== + +For mutual TLS (mTLS) authentication where the client must also present a certificate, provide both trust store and key store configuration: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-registrar.type=consul +stork.my-service.service-registrar.consul-host=localhost +stork.my-service.service-registrar.consul-port=8501 +stork.my-service.service-registrar.ssl=true +stork.my-service.service-registrar.trust-store-path=/path/to/truststore.jks +stork.my-service.service-registrar.trust-store-password=changeit +stork.my-service.service-registrar.key-store-path=/path/to/keystore.jks +stork.my-service.service-registrar.key-store-password=changeit +stork.my-service.service-registrar.verify-host=true +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-registrar.type=consul +quarkus.stork.my-service.service-registrar.consul-host=localhost +quarkus.stork.my-service.service-registrar.consul-port=8501 +quarkus.stork.my-service.service-registrar.ssl=true +quarkus.stork.my-service.service-registrar.trust-store-path=/path/to/truststore.jks +quarkus.stork.my-service.service-registrar.trust-store-password=changeit +quarkus.stork.my-service.service-registrar.key-store-path=/path/to/keystore.jks +quarkus.stork.my-service.service-registrar.key-store-password=changeit +quarkus.stork.my-service.service-registrar.verify-host=true +---- + +-- + +==== + +== ACL Token Authentication + +When your Consul cluster has ACL (Access Control List) enabled, you need to provide an ACL token for authentication: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-registrar.type=consul +stork.my-service.service-registrar.consul-host=localhost +stork.my-service.service-registrar.consul-port=8500 +stork.my-service.service-registrar.acl-token=your-acl-token +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-registrar.type=consul +quarkus.stork.my-service.service-registrar.consul-host=localhost +quarkus.stork.my-service.service-registrar.consul-port=8500 +quarkus.stork.my-service.service-registrar.acl-token=your-acl-token +---- + +-- + +==== + +The ACL token can be combined with SSL/TLS configuration for secure, authenticated access to Consul. diff --git a/content/docs/smallrye-stork/latest/service-registration/custom-service-registration.adoc b/content/docs/smallrye-stork/latest/service-registration/custom-service-registration.adoc new file mode 100644 index 000000000..9e7d8e4cd --- /dev/null +++ b/content/docs/smallrye-stork/latest/service-registration/custom-service-registration.adoc @@ -0,0 +1,165 @@ += Implement your own service registration mechanism +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Stork is extensible, and you can implement your own service registrar mechanism. + +== Dependencies + +To implement your _Service Registration Provider_, make sure your project depends on Core and Configuration Generator. +The former brings classes necessary to implement custom registrar, the latter contains an https://docs.oracle.com/en/java/javase/11/docs/api/java.compiler/javax/annotation/processing/Processor.html[annotation processor] that generates classes needed by Stork. + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-core + {project-version} + + + io.smallrye.stork + stork-configuration-generator + provided + + {project-version} + +---- + +== Implementing a service registrar provider + +Service registration implementation consists of three elements: + +* `ServiceRegistrar` which is responsible for registering service instances for a single Stork service. +* `ServiceRegistrarProvider` which creates instances of `ServiceRegistrar` for a given service registrar _type_. +* `$typeConfiguration` which is a configuration for the registrar. This class is automatically generated during the compilation (using an annotation processor). + +A _type_, for example, `acme`, identifies each provider. +This _type_ is used in the configuration to reference the provider: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-registrar.type=acme +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-registrar.type=acme +---- + +-- + +==== + +A `ServiceRegistrarProvider` implementation needs to be annotated with `@ServiceRegistrarType` that defines the _type_. +Any configuration properties that the provider expects should be defined with `@ServiceRegistrarAttribute` annotations placed on the provider. +Optionally, you can also add `@ApplicationScoped` annotation in order to provide the service registrar implementation as CDI bean. + +A service registrar provider class should look as follows: + +[source,java] +---- +include::{examples-dir}/examples/AcmeServiceRegistrarProvider.java[indent=0] +---- + +Note, that the `ServiceRegistrarProvider` interface takes a configuration class as a parameter. This configuration class +is generated automatically by the _Configuration Generator_. +Its name is created by appending `RegistrarConfiguration` to the service registrar type, such as `AcmeRegistrarConfiguration`. + +The next step is to implement the `ServiceRegistrar` interface: + +[source,java] +---- +include::{examples-dir}/examples/AcmeServiceRegistrar.java[indent=0] +---- + +This implementation is simplistic. +Typically, instead of creating a service instance with values from the configuration, you would connect to a service registration backend, look for the service and build the list of service instance accordingly. +That's why the method returns a `Uni`. +Most of the time, the lookup is a remote operation. + +As you can see, the `AcmeConfiguration` class gives access to the configuration attribute. + +=== Implement your own service deregistration mechanism + +The `registerServiceInstance` method receives an optional `instanceName` parameter that acts as an explicit identifier for the registered instance. +When provided, it should be used as the registration ID so that the same instance can be precisely targeted during deregistration. +If `instanceName` is `null` or empty, implementations should fall back to a deterministic ID derived from the service name, IP address, and port. + +Finally, you can implement your own service deregistration mechanism in `deregisterServiceInstance` method. + +[WARNING] +.Override the specific-instance deregistration methods +==== + +The `ServiceRegistrar` interface provides several deregistration methods: + +* `deregisterServiceInstance(String serviceName)` — deregisters *all* instances of a service. +* `deregisterServiceInstance(String serviceName, String instanceName)` — deregisters the instance registered under the given name. +* `deregisterServiceInstance(String serviceName, String ipAddress, int port)` — deregisters the instance identified by IP and port. + +The default implementations of the second and third methods fall back to the first, which may deregister *all* instances. +*Custom registrar implementations should override at least one of the specific-instance methods* to provide precise, instance-level deregistration and avoid unintended side effects. + +Note that the `Service` class wraps these with simpler methods that do not require passing the service name: +`deregisterNamedInstance(instanceName)`, `deregisterServiceInstance(ipAddress, port)`, and the deprecated no-arg `deregisterServiceInstance()`. + +==== + +== Using your service registrar using the programmatic API + +In the project using it, don't forget to add the dependency on the module providing your implementation. +Then, in the configuration, just add: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-registrar.type=acme +stork.my-service.service-registrar.host=localhost +stork.my-service.service-registrar.port=1234 +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-registrar.type=acme +quarkus.stork.my-service.service-registrar.host=localhost +quarkus.stork.my-service.service-registrar.port=1234 +---- + +-- + +==== + +When building your service registrar project project, the configuration generator creates a configuration class. +Then, Stork will use your implementation to register/deregister the service instances using the `acme` backend. + +This class can be used to configure your service registrar using the Stork programmatic API. + +[source,java] +---- +include::{examples-dir}/examples/AcmeRegistrarApiUsage.java[indent=0] +---- diff --git a/content/docs/smallrye-stork/latest/service-registration/eureka.adoc b/content/docs/smallrye-stork/latest/service-registration/eureka.adoc new file mode 100644 index 000000000..8e327934d --- /dev/null +++ b/content/docs/smallrye-stork/latest/service-registration/eureka.adoc @@ -0,0 +1,105 @@ += Eureka Service Registration +:version-root: ../ +include::{version-root}_attributes.adoc[] + +https://github.com/Netflix/eureka[Eureka] is a RESTful service that is primarily used in the AWS cloud for the purpose of discovery, load balancing, and failover of middle-tier servers. + +This page explains how Stork can use Eureka to handle the service registration. + +== Dependency + +First, you need to add the Stork Eureka Service Registration provider: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-service-registration-eureka + {project-version} + +---- + +== Service registration configuration + +For each service that should register the service instances in Eureka, configure the service registrar `type`: + +[.tabs] +==== + +stork standalone:: ++ +-- + +-- + +==== + +[source,properties] +---- +stork.my-service.service-registrar.type=eureka +stork.my-service.service-registrar.eureka-host=localhost +stork.my-service.service-registrar.eureka-port=8761 +---- + +[.tabs] +==== + +stork in quarkus:: ++ +-- + +-- + +==== + +[source,properties] +---- +quarkus.stork.my-service.service-registrar.type=eureka +quarkus.stork.my-service.service-registrar.eureka-host=localhost +quarkus.stork.my-service.service-registrar.eureka-port=8761 +---- + +Eureka service registrar is configured with the following parameters: + +include::{generated-dir}/eureka-sr-attributes.adoc[] + +The `ipAddress` passed to `registerServiceInstance()` is used as both the IP address (`ipAddr`) and the hostname (`hostName`) registered in Eureka. +If the instance has a distinct DNS hostname, set `host-name` explicitly to override this default. + +== Service deregistration configuration + +There is no specific configuration required to enable deregistration; however, you must ensure that a consul service registrar is configured for the service: + +[.tabs] +==== + +stork standalone:: ++ +-- + +-- + +==== + +[source,properties] +---- +stork.my-service.service-registrar.type=eureka +---- + +[.tabs] +==== + +stork in quarkus:: ++ +-- + +-- + +==== + +[source,properties] +---- +quarkus.stork.my-service.service-registrar.type=eureka +---- + +As with registration, deregistration relies on the service name. diff --git a/content/docs/smallrye-stork/latest/service-registration/overview.adoc b/content/docs/smallrye-stork/latest/service-registration/overview.adoc new file mode 100644 index 000000000..db10b9a5f --- /dev/null +++ b/content/docs/smallrye-stork/latest/service-registration/overview.adoc @@ -0,0 +1,82 @@ += Overview +:version-root: ../ +include::{version-root}_attributes.adoc[] + +== Service Registration in SmallRye Stork + +Service registration is the process by which services announce their availability to a central registry, allowing other services to discover and communicate with them. +In SmallRye Stork, service registration is automated and integrated with supported registries like *Consul*. +This ensures that services can dynamically join and leave the network. + +=== Key Features: + +* *Automatic Registration*: For *Quarkus applications*, SmallRye Stork automatically registers the application with the configured service registry — but *only for built-in registrars such as Consul, Eureka, and Static*. +If you're using a custom registrar, registration must still be handled programmatically, as before this feature. + +Registration is enabled by default. If you want to disable registration, you can do it per service using the following property: + +[.tabs] +==== + +stork standalone:: ++ +-- + +-- + +==== + +[source,properties] +---- +stork.my-service.service-registrar.enable=false +---- + +[.tabs] +==== + +stork in quarkus:: ++ +-- + +-- + +==== + +[source,properties] +---- +quarkus.stork.my-service.service-registrar.enable=false +---- + +*IMPORTANT* Public IP address needs to be provided. Smallrye Stork will fail if the service IP address is not provided during registration. + +==== Supported Registries: + +Currently, Smallrye Stork provides seamless integration with Consul, Eureka and a Static registry. +This integration simplifies the management of dynamic environments where services are frequently added or removed. + +==== Custom Registration: + +In addition to the default mechanisms, SmallRye Stork allows you to implement custom service registration strategies, +providing flexibility for different infrastructures or custom service registration needs. + +In the following sections you can have more details about each specific implementation. + +=== Deregister Services + +SmallRye Stork now supports deregistration of service instances from a central registry when explicitly requested. +This feature complements the registration mechanism. + +This operation is typically should be triggered when a service instance is shut down or no longer available, helping maintain a consistent and accurate service registry. +To enable service deregistration, you can invoke the deregistration methods on the `Service` instance programmatically. +To target a specific instance, prefer one of these methods: + +* `deregisterNamedInstance(instanceName)` — deregisters the instance registered under the given name. +* `deregisterServiceInstance(ipAddress, port)` — deregisters the instance identified by IP and port. + +The no-argument overload `deregisterServiceInstance()` deregisters *all* instances of the service and is deprecated, prefer one of the specific methods above. + +*Note*: When used in standalone mode, Stork does *not* automatically handle service instance registration or deregistration. +However, when using the quarkus-stork-registration extension within a Quarkus application, +service instances are automatically registered at application startup and deregistered upon shutdown, unless this behavior is explicitly disabled. + +In the following sections you can have more details about each specific implementation. diff --git a/content/docs/smallrye-stork/latest/service-registration/static-list.adoc b/content/docs/smallrye-stork/latest/service-registration/static-list.adoc new file mode 100644 index 000000000..87e3eb570 --- /dev/null +++ b/content/docs/smallrye-stork/latest/service-registration/static-list.adoc @@ -0,0 +1,96 @@ += Static List Service Registration +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Stork provides the ability to register services using Static list as backend. + +== Dependency + +To use the `static` service registrar, first add the appropriate Service Registration provider dependency to your project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-service-registration-static-list + {project-version} + +---- + +== Service registration configuration + +For each service that should register the service instances in a static list, configure the service registrar `type`: + +[.tabs] +==== + +stork standalone:: ++ +-- + +-- + +==== + +[source,properties] +---- +stork.my-service.service-registrar.type=static +---- + +[.tabs] +==== + +stork in quarkus:: ++ +-- + +-- + +==== + +[source,properties] +---- +quarkus.stork.my-service.service-registrar.type=static +---- + +Static service registrar is configured with the following parameters: + +include::{generated-dir}/static-sr-attributes.adoc[] + +== Service deregistration configuration + +There is no specific configuration required to enable deregistration; however, you must ensure that a consul service registrar is configured for the service: + +[.tabs] +==== + +stork standalone:: ++ +-- + +-- + +==== + +[source,properties] +---- +stork.my-service.service-registrar.type=static +---- + +[.tabs] +==== + +stork in quarkus:: ++ +-- + +-- + +==== + +[source,properties] +---- +quarkus.stork.my-service.service-registrar.type=static +---- + +As with registration, deregistration relies on the service name. diff --git a/content/docs/smallrye-stork/latest/springboot-config.adoc b/content/docs/smallrye-stork/latest/springboot-config.adoc new file mode 100644 index 000000000..05d2f1a3b --- /dev/null +++ b/content/docs/smallrye-stork/latest/springboot-config.adoc @@ -0,0 +1,40 @@ += Stork Spring Boot Config +include::_attributes.adoc[] + +Stork seamlessly supports Spring Boot configuration, facilitating access to configuration properties. +This documentation elaborates on how Spring Boot developers can use Stork in their Spring Boot applications and configure it using the application.properties file. + +== Dependency setup + +To enable Spring Boot configuration integration in Stork, you need to include the following dependency: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-spring-boot-config + {project-version} + +---- + +== Initializing Stork + +Since Spring Boot lacks a pre-configured Stork instance, you'll need create one. It can be done by providing a Spring bean performing Stork initialization: + +[source,java] +---- +include::{examples-dir}/examples/SpringBootInitializationExample.java[indent=0] +---- + +Upon initialization, Stork scans for the `io.smallrye.stork.springboot.SpringBootConfigProvider` SPI provider and CDI beans (from version 2.x onwards). +It then builds a comprehensive list of managed services by parsing the properties configuration files. + +Please note the importance of the `io.smallrye.stork.springboot.SpringBootApplicationContextProvider` bean in our setup. +This bean has a critical role by granting Stork access to the current `org.springframework.context.ApplicationContext`. +It enables it to retrieve configuration details effectively. Consequently, it's imperative that this bean is instantiated prior to initiating the Stork initialization process. +In this case, we utilize the `@DependsOn` annotation for that. +It allows us controlling the bean creation order. + +== Comprehensive Example + +You can check our https://github.com/aureamunoz/spring-stork-guitar-hero/[Guitar Hero Application] showcasing the seamless integration of Stork with Spring Boot Configuration. diff --git a/content/docs/smallrye-stork/main/_attributes.adoc b/content/docs/smallrye-stork/main/_attributes.adoc new file mode 100644 index 000000000..5b591a7de --- /dev/null +++ b/content/docs/smallrye-stork/main/_attributes.adoc @@ -0,0 +1,20 @@ +:project-version: 3.0.3 +// Attributes shared by every page. smallrye.io overrides the ones it needs when it publishes. +// Pages in subdirectories set :version-root: (../ per level) before including this file. +ifndef::version-root[:version-root:] +:idprefix: +:idseparator: - +:sectanchors: +:examples-dir: {version-root}_examples +:partialsdir: {version-root}_partials +:project-name: SmallRye Stork +:javadoc-version: latest +:generated-dir: {version-root}_generated + +// Added by smallrye.io (tools/docs/Docs.java sync) +:javadoc-version: 3.0.3 +:relfileprefix: /docs/smallrye-stork/main/ +:imagesdir: /docs/smallrye-stork/main +:docs-project: smallrye-stork +:docs-version: main +:docs-ref: main diff --git a/content/docs/smallrye-stork/main/_examples/examples/AcmeDiscoveryApiUsage.java b/content/docs/smallrye-stork/main/_examples/examples/AcmeDiscoveryApiUsage.java new file mode 100644 index 000000000..1487b571a --- /dev/null +++ b/content/docs/smallrye-stork/main/_examples/examples/AcmeDiscoveryApiUsage.java @@ -0,0 +1,20 @@ +package examples; + +import io.smallrye.mutiny.Uni; +import io.smallrye.stork.api.ServiceDefinition; +import io.smallrye.stork.api.ServiceInstance; +import io.smallrye.stork.api.StorkServiceRegistry; + +import java.util.List; + +public class AcmeDiscoveryApiUsage { + + public void example(StorkServiceRegistry stork) { + stork.defineIfAbsent("my-service", ServiceDefinition.of( + new AcmeConfiguration().withHost("my-host")) + ); + + Uni> serviceInstances = stork.getService("my-service").getInstances(); + } + +} diff --git a/content/docs/smallrye-stork/main/_examples/examples/AcmeLoadBalancer.java b/content/docs/smallrye-stork/main/_examples/examples/AcmeLoadBalancer.java new file mode 100644 index 000000000..6fb0b1be1 --- /dev/null +++ b/content/docs/smallrye-stork/main/_examples/examples/AcmeLoadBalancer.java @@ -0,0 +1,32 @@ +package examples; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Random; + +import io.smallrye.stork.api.LoadBalancer; +import io.smallrye.stork.api.NoServiceInstanceFoundException; +import io.smallrye.stork.api.ServiceInstance; + +public class AcmeLoadBalancer implements LoadBalancer { + + private final Random random; + + public AcmeLoadBalancer(AcmeLoadBalancerConfiguration config) { + random = new Random(); + } + + @Override + public ServiceInstance selectServiceInstance(Collection serviceInstances) { + if (serviceInstances.isEmpty()) { + throw new NoServiceInstanceFoundException("No services found."); + } + int index = random.nextInt(serviceInstances.size()); + return new ArrayList<>(serviceInstances).get(index); + } + + @Override + public boolean requiresStrictRecording() { + return false; + } +} diff --git a/content/docs/smallrye-stork/main/_examples/examples/AcmeLoadBalancerProvider.java b/content/docs/smallrye-stork/main/_examples/examples/AcmeLoadBalancerProvider.java new file mode 100644 index 000000000..3640322ee --- /dev/null +++ b/content/docs/smallrye-stork/main/_examples/examples/AcmeLoadBalancerProvider.java @@ -0,0 +1,22 @@ +package examples; + +import io.smallrye.stork.api.LoadBalancer; +import io.smallrye.stork.api.ServiceDiscovery; +import io.smallrye.stork.api.config.LoadBalancerAttribute; +import io.smallrye.stork.api.config.LoadBalancerType; +import io.smallrye.stork.spi.LoadBalancerProvider; +import jakarta.enterprise.context.ApplicationScoped; + +@LoadBalancerType("acme-load-balancer") +@LoadBalancerAttribute(name = "my-attribute", + description = "Attribute that alters the behavior of the LoadBalancer") +@ApplicationScoped +public class AcmeLoadBalancerProvider implements + LoadBalancerProvider { + + @Override + public LoadBalancer createLoadBalancer(AcmeLoadBalancerConfiguration config, + ServiceDiscovery serviceDiscovery) { + return new AcmeLoadBalancer(config); + } +} diff --git a/content/docs/smallrye-stork/main/_examples/examples/AcmeObservationCollector.java b/content/docs/smallrye-stork/main/_examples/examples/AcmeObservationCollector.java new file mode 100644 index 000000000..3c4655e7d --- /dev/null +++ b/content/docs/smallrye-stork/main/_examples/examples/AcmeObservationCollector.java @@ -0,0 +1,35 @@ +package examples; + +import io.smallrye.stork.Stork; +import io.smallrye.stork.api.observability.ObservationCollector; +import io.smallrye.stork.api.observability.StorkEventHandler; +import io.smallrye.stork.api.observability.StorkObservation; +import org.jboss.logging.Logger; + +public class AcmeObservationCollector implements ObservationCollector { + + private static final Logger LOGGER = Logger.getLogger(AcmeObservationCollector.class); + + private static final StorkEventHandler ACME_HANDLER = event -> { + //This is the terminal event. Put here your custom logic to extend the metrics collection. + + //E.g. Expose metrics to Micrometer, additional logs.... + LOGGER.info( "Service discovery took " + event.getServiceDiscoveryDuration() + "."); + LOGGER.info( event.getDiscoveredInstancesCount() + " have been discovered for " + event.getServiceName() + "."); + LOGGER.info( "Service selection took " + event.getServiceSelectionDuration() + "."); + + // ... + + }; + + public static StorkObservation ACME_STORK_EVENT; + + @Override + public StorkObservation create(String serviceName, String serviceDiscoveryType, + String serviceSelectionType) { + ACME_STORK_EVENT = new StorkObservation( + serviceName, serviceDiscoveryType, serviceSelectionType, + ACME_HANDLER); + return ACME_STORK_EVENT; + } +} diff --git a/content/docs/smallrye-stork/main/_examples/examples/AcmeRegistrarApiUsage.java b/content/docs/smallrye-stork/main/_examples/examples/AcmeRegistrarApiUsage.java new file mode 100644 index 000000000..2c4b52cef --- /dev/null +++ b/content/docs/smallrye-stork/main/_examples/examples/AcmeRegistrarApiUsage.java @@ -0,0 +1,21 @@ +package examples; + +import io.smallrye.stork.api.ServiceDefinition; +import io.smallrye.stork.api.StorkServiceRegistry; +import io.smallrye.stork.servicediscovery.staticlist.StaticConfiguration; + +public class AcmeRegistrarApiUsage { + + public void example(StorkServiceRegistry stork) { + String list = "localhost:8080, localhost:8081"; + + stork.defineIfAbsent("my-service", ServiceDefinition.of( + new StaticConfiguration().withAddressList(list), + new AcmeLoadBalancerConfiguration().withMyAttribute("my-value"),new AcmeRegistrarConfiguration()) + ); + + stork.getService("my-service").registerNamedInstance("my-instance", "localhost", + 9000); + } + +} diff --git a/content/docs/smallrye-stork/main/_examples/examples/AcmeSelectorApiUsage.java b/content/docs/smallrye-stork/main/_examples/examples/AcmeSelectorApiUsage.java new file mode 100644 index 000000000..67440ddf9 --- /dev/null +++ b/content/docs/smallrye-stork/main/_examples/examples/AcmeSelectorApiUsage.java @@ -0,0 +1,22 @@ +package examples; + +import io.smallrye.mutiny.Uni; +import io.smallrye.stork.api.ServiceDefinition; +import io.smallrye.stork.api.ServiceInstance; +import io.smallrye.stork.api.StorkServiceRegistry; +import io.smallrye.stork.servicediscovery.staticlist.StaticConfiguration; +import io.smallrye.stork.serviceregistration.staticlist.StaticRegistrarConfiguration; + +public class AcmeSelectorApiUsage { + + public void example(StorkServiceRegistry stork) { + String list = "localhost:8080, localhost:8081"; + stork.defineIfAbsent("my-service", ServiceDefinition.of( + new StaticConfiguration().withAddressList(list), + new AcmeLoadBalancerConfiguration().withMyAttribute("my-value"),new StaticRegistrarConfiguration()) + ); + + Uni uni = stork.getService("my-service").selectInstance(); + } + +} diff --git a/content/docs/smallrye-stork/main/_examples/examples/AcmeServiceDiscovery.java b/content/docs/smallrye-stork/main/_examples/examples/AcmeServiceDiscovery.java new file mode 100644 index 000000000..b9368c4b4 --- /dev/null +++ b/content/docs/smallrye-stork/main/_examples/examples/AcmeServiceDiscovery.java @@ -0,0 +1,32 @@ +package examples; + +import java.util.Collections; +import java.util.List; + +import io.smallrye.mutiny.Uni; +import io.smallrye.stork.api.ServiceDiscovery; +import io.smallrye.stork.api.ServiceInstance; +import io.smallrye.stork.impl.DefaultServiceInstance; +import io.smallrye.stork.utils.ServiceInstanceIds; + +public class AcmeServiceDiscovery implements ServiceDiscovery { + + private final String host; + private final int port; + + public AcmeServiceDiscovery(AcmeConfiguration configuration) { + this.host = configuration.getHost(); + this.port = Integer.parseInt(configuration.getPort()); + } + + @Override + public Uni> getServiceInstances() { + // Proceed to the lookup... + // Here, we just return a DefaultServiceInstance with the configured host and port + // The last parameter specifies whether the communication with the instance should + // happen over a secure connection + DefaultServiceInstance instance = + new DefaultServiceInstance(ServiceInstanceIds.next(), host, port, false); + return Uni.createFrom().item(() -> Collections.singletonList(instance)); + } +} diff --git a/content/docs/smallrye-stork/main/_examples/examples/AcmeServiceDiscoveryProvider.java b/content/docs/smallrye-stork/main/_examples/examples/AcmeServiceDiscoveryProvider.java new file mode 100644 index 000000000..ca39d5289 --- /dev/null +++ b/content/docs/smallrye-stork/main/_examples/examples/AcmeServiceDiscoveryProvider.java @@ -0,0 +1,28 @@ +package examples; + +import io.smallrye.stork.api.ServiceDiscovery; +import io.smallrye.stork.api.config.ServiceConfig; +import io.smallrye.stork.api.config.ServiceDiscoveryAttribute; +import io.smallrye.stork.api.config.ServiceDiscoveryType; +import io.smallrye.stork.spi.StorkInfrastructure; +import io.smallrye.stork.spi.ServiceDiscoveryProvider; +import jakarta.enterprise.context.ApplicationScoped; + +@ServiceDiscoveryType("acme") +@ServiceDiscoveryAttribute(name = "host", + description = "Host name of the service discovery server.", required = true) +@ServiceDiscoveryAttribute(name = "port", + description = "Hort of the service discovery server.", required = false) +@ApplicationScoped +public class AcmeServiceDiscoveryProvider + implements ServiceDiscoveryProvider { + + @Override + public ServiceDiscovery createServiceDiscovery( + AcmeConfiguration config, + String serviceName, + ServiceConfig serviceConfig, + StorkInfrastructure storkInfrastructure) { + return new AcmeServiceDiscovery(config); + } +} diff --git a/content/docs/smallrye-stork/main/_examples/examples/AcmeServiceRegistrar.java b/content/docs/smallrye-stork/main/_examples/examples/AcmeServiceRegistrar.java new file mode 100644 index 000000000..52ce8d799 --- /dev/null +++ b/content/docs/smallrye-stork/main/_examples/examples/AcmeServiceRegistrar.java @@ -0,0 +1,38 @@ +package examples; + +import io.smallrye.mutiny.Uni; +import io.smallrye.stork.api.Metadata; +import io.smallrye.stork.api.ServiceRegistrar; + +import java.util.List; + +public class AcmeServiceRegistrar implements ServiceRegistrar { + + private final String backendHost; + private final int backendPort; + + public AcmeServiceRegistrar(AcmeRegistrarConfiguration configuration) { + this.backendHost = configuration.getHost(); + this.backendPort = Integer.parseInt(configuration.getPort()); + } + + + @Override + public Uni registerServiceInstance(String serviceName, String instanceName, Metadata metadata, String ipAddress, int defaultPort) { + //instanceName is an optional identifier for this specific instance; use it as the registration ID when provided + //do whatever is needed for registering service instance + return Uni.createFrom().voidItem(); + } + + @Override + public Uni registerServiceInstance(String serviceName, String instanceName, List tags, + Metadata metadata, String ipAddress, int defaultPort) { + return Uni.createFrom().voidItem(); + } + + @Override + public Uni deregisterServiceInstance(String serviceName) { + //do whatever is needed for deregistering service instance + return Uni.createFrom().voidItem(); + } +} \ No newline at end of file diff --git a/content/docs/smallrye-stork/main/_examples/examples/AcmeServiceRegistrarProvider.java b/content/docs/smallrye-stork/main/_examples/examples/AcmeServiceRegistrarProvider.java new file mode 100644 index 000000000..52e028cfb --- /dev/null +++ b/content/docs/smallrye-stork/main/_examples/examples/AcmeServiceRegistrarProvider.java @@ -0,0 +1,28 @@ +package examples; + +import io.smallrye.stork.api.Metadata; +import io.smallrye.stork.api.ServiceRegistrar; +import io.smallrye.stork.api.config.ServiceRegistrarAttribute; +import io.smallrye.stork.api.config.ServiceRegistrarType; +import io.smallrye.stork.spi.ServiceRegistrarProvider; +import io.smallrye.stork.spi.StorkInfrastructure; +import jakarta.enterprise.context.ApplicationScoped; + +@ServiceRegistrarType(value = "acme", metadataKey = Metadata.DefaultMetadataKey.class) +@ServiceRegistrarAttribute(name = "host", + description = "Host name of the service registration server.", required = true) +@ServiceRegistrarAttribute(name = "port", + description = "Port of the service registration server.", required = false) +@ApplicationScoped +public class AcmeServiceRegistrarProvider + implements ServiceRegistrarProvider { + + @Override + public ServiceRegistrar createServiceRegistrar( + AcmeRegistrarConfiguration config, + String serviceName, + StorkInfrastructure storkInfrastructure) { + return new AcmeServiceRegistrar(config); + } + +} diff --git a/content/docs/smallrye-stork/main/_examples/examples/CachedAcmeServiceDiscovery.java b/content/docs/smallrye-stork/main/_examples/examples/CachedAcmeServiceDiscovery.java new file mode 100644 index 000000000..204aa78c9 --- /dev/null +++ b/content/docs/smallrye-stork/main/_examples/examples/CachedAcmeServiceDiscovery.java @@ -0,0 +1,31 @@ +package examples; + +import io.smallrye.mutiny.Uni; +import io.smallrye.stork.api.ServiceDiscovery; +import io.smallrye.stork.api.ServiceInstance; +import io.smallrye.stork.impl.CachingServiceDiscovery; +import io.smallrye.stork.impl.DefaultServiceInstance; +import io.smallrye.stork.utils.ServiceInstanceIds; + +import java.util.Collections; +import java.util.List; + +public class CachedAcmeServiceDiscovery extends CachingServiceDiscovery { + + private final String host; + private final int port; + + public CachedAcmeServiceDiscovery(CachedAcmeConfiguration configuration) { + super(configuration.getRefreshPeriod()); // (1) + this.host = configuration.getHost(); + this.port = Integer.parseInt(configuration.getPort()); + } + + @Override // (2) + public Uni> fetchNewServiceInstances(List previousInstances) { + // Retrieve services... + DefaultServiceInstance instance = + new DefaultServiceInstance(ServiceInstanceIds.next(), host, port, false); + return Uni.createFrom().item(() -> Collections.singletonList(instance)); + } +} diff --git a/content/docs/smallrye-stork/main/_examples/examples/CachedAcmeServiceDiscoveryProvider.java b/content/docs/smallrye-stork/main/_examples/examples/CachedAcmeServiceDiscoveryProvider.java new file mode 100644 index 000000000..56d627671 --- /dev/null +++ b/content/docs/smallrye-stork/main/_examples/examples/CachedAcmeServiceDiscoveryProvider.java @@ -0,0 +1,32 @@ +package examples; + +import io.smallrye.stork.api.ServiceDiscovery; +import io.smallrye.stork.api.config.ServiceConfig; +import io.smallrye.stork.api.config.ServiceDiscoveryAttribute; +import io.smallrye.stork.api.config.ServiceDiscoveryType; +import io.smallrye.stork.impl.CachingServiceDiscovery; +import io.smallrye.stork.spi.ServiceDiscoveryProvider; +import io.smallrye.stork.spi.StorkInfrastructure; +import jakarta.enterprise.context.ApplicationScoped; + +@ServiceDiscoveryType("cached-acme") +@ServiceDiscoveryAttribute(name = "host", + description = "Host name of the service discovery server.", required = true) +@ServiceDiscoveryAttribute(name = "port", + description = "Hort of the service discovery server.", required = false) +@ServiceDiscoveryAttribute(name = "refresh-period", + description = "Service discovery cache refresh period.", + defaultValue = CachingServiceDiscovery.DEFAULT_REFRESH_INTERVAL) +@ApplicationScoped +public class CachedAcmeServiceDiscoveryProvider + implements ServiceDiscoveryProvider { + + @Override + public ServiceDiscovery createServiceDiscovery( + CachedAcmeConfiguration config, + String serviceName, + ServiceConfig serviceConfig, + StorkInfrastructure storkInfrastructure) { + return new CachedAcmeServiceDiscovery(config); + } +} diff --git a/content/docs/smallrye-stork/main/_examples/examples/CustomExpirationCachedAcmeServiceDiscovery.java b/content/docs/smallrye-stork/main/_examples/examples/CustomExpirationCachedAcmeServiceDiscovery.java new file mode 100644 index 000000000..b2e3823c9 --- /dev/null +++ b/content/docs/smallrye-stork/main/_examples/examples/CustomExpirationCachedAcmeServiceDiscovery.java @@ -0,0 +1,44 @@ +package examples; + +import io.smallrye.mutiny.Uni; +import io.smallrye.stork.api.ServiceInstance; +import io.smallrye.stork.impl.CachingServiceDiscovery; +import io.smallrye.stork.impl.DefaultServiceInstance; +import io.smallrye.stork.utils.ServiceInstanceIds; + +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +public class CustomExpirationCachedAcmeServiceDiscovery extends CachingServiceDiscovery { + + private final String host; + private final int port; + + private AtomicBoolean invalidated = new AtomicBoolean(); + + public CustomExpirationCachedAcmeServiceDiscovery(CachedAcmeConfiguration configuration) { + super(configuration.getRefreshPeriod()); + this.host = configuration.getHost(); + this.port = Integer.parseInt(configuration.getPort()); + } + + @Override + public Uni> fetchNewServiceInstances(List previousInstances) { + // Retrieve services... + DefaultServiceInstance instance = + new DefaultServiceInstance(ServiceInstanceIds.next(), host, port, false); + return Uni.createFrom().item(() -> Collections.singletonList(instance)); + } + + @Override + public Uni> cache(Uni> uni) { + return uni.memoize().until(() -> invalidated.get()); + } + + @Override + public void invalidate() { + invalidated.set(true); + } + +} \ No newline at end of file diff --git a/content/docs/smallrye-stork/main/_examples/examples/DefinitionExample.java b/content/docs/smallrye-stork/main/_examples/examples/DefinitionExample.java new file mode 100644 index 000000000..aab2dc3d6 --- /dev/null +++ b/content/docs/smallrye-stork/main/_examples/examples/DefinitionExample.java @@ -0,0 +1,30 @@ +package examples; + +import io.smallrye.stork.Stork; +import io.smallrye.stork.api.ServiceDefinition; +import io.smallrye.stork.loadbalancer.random.RandomConfiguration; +import io.smallrye.stork.servicediscovery.staticlist.StaticConfiguration; +import io.smallrye.stork.serviceregistration.staticlist.StaticRegistrarConfiguration; + +public class DefinitionExample { + + public static void example(Stork stork) { + String example = "localhost:8080, localhost:8081"; + + // A service using a static list of locations as discovery + // As not set, it defaults to round-robin to select the instance. + stork.defineIfAbsent("my-service", + ServiceDefinition.of(new StaticConfiguration().withAddressList(example))); + + // Another service using the random selection strategy, instead of round-robin + stork.defineIfAbsent("my-second-service", + ServiceDefinition.of(new StaticConfiguration().withAddressList(example), + new RandomConfiguration())); + + // Another service using the random selection strategy, instead of round-robin + // and a static service registrar + stork.defineIfAbsent("my-second-service", + ServiceDefinition.of(new StaticConfiguration().withAddressList(example), + new RandomConfiguration(), new StaticRegistrarConfiguration())); + } +} diff --git a/content/docs/smallrye-stork/main/_examples/examples/HelloClient.java b/content/docs/smallrye-stork/main/_examples/examples/HelloClient.java new file mode 100644 index 000000000..eec581608 --- /dev/null +++ b/content/docs/smallrye-stork/main/_examples/examples/HelloClient.java @@ -0,0 +1,20 @@ +package examples; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + + + +@Path("/") +@RegisterRestClient(baseUri = "stork://hello-service/hello") +public interface HelloClient { + @POST + @Produces(MediaType.TEXT_PLAIN) + @Consumes(MediaType.TEXT_PLAIN) + String echo(String name); +} + diff --git a/content/docs/smallrye-stork/main/_examples/examples/InitializationExample.java b/content/docs/smallrye-stork/main/_examples/examples/InitializationExample.java new file mode 100644 index 000000000..cb93ca0bd --- /dev/null +++ b/content/docs/smallrye-stork/main/_examples/examples/InitializationExample.java @@ -0,0 +1,18 @@ +package examples; + +import io.smallrye.stork.Stork; +import io.smallrye.stork.api.ServiceDefinition; +import io.smallrye.stork.api.ServiceInstance; +import io.smallrye.stork.loadbalancer.random.RandomConfiguration; +import io.smallrye.stork.servicediscovery.staticlist.StaticConfiguration; + +import java.time.Duration; + +public class InitializationExample { + + public static void main(String[] args) { + Stork.initialize(); + Stork stork = Stork.getInstance(); + // ... + } +} diff --git a/content/docs/smallrye-stork/main/_examples/examples/LookupExample.java b/content/docs/smallrye-stork/main/_examples/examples/LookupExample.java new file mode 100644 index 000000000..71e232d12 --- /dev/null +++ b/content/docs/smallrye-stork/main/_examples/examples/LookupExample.java @@ -0,0 +1,27 @@ +package examples; + +import io.smallrye.mutiny.Uni; +import io.smallrye.stork.Stork; +import io.smallrye.stork.api.Service; +import io.smallrye.stork.api.ServiceDefinition; +import io.smallrye.stork.api.ServiceInstance; +import io.smallrye.stork.loadbalancer.random.RandomConfiguration; +import io.smallrye.stork.servicediscovery.staticlist.StaticConfiguration; + +import java.util.List; +import java.util.Map; + +public class LookupExample { + + public static void example(Stork stork) { + Service service = stork.getService("my-service"); + + // Gets all the available instances: + Uni> instances = service.getInstances(); + // Select one instance using the load balancing strategy + Uni instance = service.selectInstance(); + + // Gets all the managed services: + Map services = stork.getServices(); + } +} diff --git a/content/docs/smallrye-stork/main/_examples/examples/ObservableInitializationExample.java b/content/docs/smallrye-stork/main/_examples/examples/ObservableInitializationExample.java new file mode 100644 index 000000000..d18854291 --- /dev/null +++ b/content/docs/smallrye-stork/main/_examples/examples/ObservableInitializationExample.java @@ -0,0 +1,13 @@ +package examples; + +import io.smallrye.stork.Stork; +import io.smallrye.stork.integration.ObservableStorkInfrastructure; + +public class ObservableInitializationExample { + + public static void main(String[] args) { + Stork.initialize(new ObservableStorkInfrastructure(new AcmeObservationCollector())); + Stork stork = Stork.getInstance(); + // ... + } +} diff --git a/content/docs/smallrye-stork/main/_examples/examples/ObservationExample.java b/content/docs/smallrye-stork/main/_examples/examples/ObservationExample.java new file mode 100644 index 000000000..299fc5b4e --- /dev/null +++ b/content/docs/smallrye-stork/main/_examples/examples/ObservationExample.java @@ -0,0 +1,35 @@ +package examples; + +import io.smallrye.mutiny.Uni; +import io.smallrye.stork.Stork; +import io.smallrye.stork.api.Service; +import io.smallrye.stork.api.ServiceInstance; +import io.smallrye.stork.api.observability.ObservationCollector; +import io.smallrye.stork.api.observability.StorkObservation; + +import java.time.Duration; +import java.util.List; +import java.util.Map; + +import static examples.AcmeObservationCollector.*; + +public class ObservationExample { + + public static void example(Stork stork) { + Service service = stork.getService("my-service"); + + ObservationCollector observations = service.getObservations(); + + // Gets the time spent in service discovery and service selection even if any error happens + Duration overallDuration = ACME_STORK_EVENT.getOverallDuration(); + + // Gets the total number of instances discovered + int discoveredInstancesCount = ACME_STORK_EVENT.getDiscoveredInstancesCount(); + + // Gets the error raised during the process + Throwable failure = ACME_STORK_EVENT.failure(); + + // ... + + } +} diff --git a/content/docs/smallrye-stork/main/_examples/examples/SpringBootInitializationExample.java b/content/docs/smallrye-stork/main/_examples/examples/SpringBootInitializationExample.java new file mode 100644 index 000000000..d21242584 --- /dev/null +++ b/content/docs/smallrye-stork/main/_examples/examples/SpringBootInitializationExample.java @@ -0,0 +1,29 @@ +package examples; + +import io.smallrye.stork.Stork; +import io.smallrye.stork.springboot.SpringBootApplicationContextProvider; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.DependsOn; + +@SpringBootApplication +public class SpringBootInitializationExample { + + public static void main(String[] args) { + SpringApplication.run(SpringBootInitializationExample.class, args); + } + + @Bean + @DependsOn("springBootApplicationContextProvider") + public Stork stork() { + Stork.initialize(); + Stork stork = Stork.getInstance(); + return stork; + } + + @Bean + public SpringBootApplicationContextProvider springBootApplicationContextProvider() { + return new SpringBootApplicationContextProvider(); + } +} diff --git a/content/docs/smallrye-stork/main/_examples/examples/StorkApiExample.java b/content/docs/smallrye-stork/main/_examples/examples/StorkApiExample.java new file mode 100644 index 000000000..d8713eb9f --- /dev/null +++ b/content/docs/smallrye-stork/main/_examples/examples/StorkApiExample.java @@ -0,0 +1,40 @@ +package examples; + +import io.smallrye.stork.Stork; +import io.smallrye.stork.api.ServiceDefinition; +import io.smallrye.stork.api.ServiceInstance; +import io.smallrye.stork.loadbalancer.random.RandomConfiguration; +import io.smallrye.stork.servicediscovery.staticlist.StaticConfiguration; +import io.smallrye.stork.serviceregistration.staticlist.StaticRegistrarConfiguration; + +import java.time.Duration; + +public class StorkApiExample { + + public static void main(String[] args) { + Stork.initialize(); + Stork stork = Stork.getInstance(); + + String example = "localhost:8080, localhost:8082"; + + // A service using a static list of locations as discovery + // As not set, it defaults to round-robin to select the instance. + stork.defineIfAbsent("my-service", + ServiceDefinition.of(new StaticConfiguration().withAddressList(example))); + + // Another service using the random selection strategy, instead of round-robin + stork.defineIfAbsent("my-second-service", + ServiceDefinition.of(new StaticConfiguration().withAddressList(example), + new RandomConfiguration())); + + ServiceInstance instance = stork.getService("my-second-service").selectInstance() + .await().atMost(Duration.ofSeconds(1)); + System.out.println(instance.getHost() + ":" + instance.getPort()); + + // Another service using the random selection strategy, instead of round-robin + // and a static service registrar + stork.defineIfAbsent("my-third-service", + ServiceDefinition.of(new StaticConfiguration().withAddressList(example), + new RandomConfiguration(), new StaticRegistrarConfiguration())); + } +} diff --git a/content/docs/smallrye-stork/main/_examples/examples/StorkEntryPointExample.java b/content/docs/smallrye-stork/main/_examples/examples/StorkEntryPointExample.java new file mode 100644 index 000000000..99343bd51 --- /dev/null +++ b/content/docs/smallrye-stork/main/_examples/examples/StorkEntryPointExample.java @@ -0,0 +1,14 @@ +package examples; + +import io.smallrye.stork.Stork; + +public class StorkEntryPointExample { + + public static void main(String[] args) { + Stork.initialize(); + Stork stork = Stork.getInstance(); + // ... + Stork.shutdown(); + } + +} diff --git a/content/docs/smallrye-stork/main/_examples/examples/StorkServiceExample.java b/content/docs/smallrye-stork/main/_examples/examples/StorkServiceExample.java new file mode 100644 index 000000000..c0b7dbb06 --- /dev/null +++ b/content/docs/smallrye-stork/main/_examples/examples/StorkServiceExample.java @@ -0,0 +1,18 @@ +package examples; + +import io.smallrye.stork.api.Service; +import io.smallrye.stork.Stork; + +public class StorkServiceExample { + + public static void main(String[] args) { + Stork.initialize(); + Stork stork = Stork.getInstance(); + + Service service = stork.getService("my-service"); + + // ... + Stork.shutdown(); + } + +} diff --git a/content/docs/smallrye-stork/main/_examples/examples/StorkServiceLookupExample.java b/content/docs/smallrye-stork/main/_examples/examples/StorkServiceLookupExample.java new file mode 100644 index 000000000..70c03cdc1 --- /dev/null +++ b/content/docs/smallrye-stork/main/_examples/examples/StorkServiceLookupExample.java @@ -0,0 +1,24 @@ +package examples; + +import java.time.Duration; +import java.util.List; + +import io.smallrye.stork.Stork; +import io.smallrye.stork.api.Service; +import io.smallrye.stork.api.ServiceInstance; + +public class StorkServiceLookupExample { + + public static void main(String[] args) { + Stork.initialize(); + Stork stork = Stork.getInstance(); + + Service service = stork.getService("my-service"); + List instances = service.getInstances() + .await().atMost(Duration.ofSeconds(5)); + + // ... + Stork.shutdown(); + } + +} diff --git a/content/docs/smallrye-stork/main/_examples/examples/StorkServiceSelectionExample.java b/content/docs/smallrye-stork/main/_examples/examples/StorkServiceSelectionExample.java new file mode 100644 index 000000000..a9e3dcc85 --- /dev/null +++ b/content/docs/smallrye-stork/main/_examples/examples/StorkServiceSelectionExample.java @@ -0,0 +1,25 @@ +package examples; + +import java.time.Duration; + +import io.smallrye.stork.Stork; +import io.smallrye.stork.api.Service; +import io.smallrye.stork.api.ServiceInstance; + +public class StorkServiceSelectionExample { + + public static void main(String[] args) { + Stork.initialize(); + Stork stork = Stork.getInstance(); + + Service service = stork.getService("my-service"); + ServiceInstance instance = service.selectInstance() + .await().atMost(Duration.ofSeconds(5)); + + System.out.println(instance.getHost() + ":" + instance.getPort()); + + // ... + Stork.shutdown(); + } + +} diff --git a/content/docs/smallrye-stork/main/_generated/composite-sd-attributes.adoc b/content/docs/smallrye-stork/main/_generated/composite-sd-attributes.adoc new file mode 100644 index 000000000..ae556fc30 --- /dev/null +++ b/content/docs/smallrye-stork/main/_generated/composite-sd-attributes.adoc @@ -0,0 +1,5 @@ +[cols=",,,",options="header",] +|=== +|Attribute |Mandatory |Default Value |Description +|`services` |Yes | |A comma-separated list of services that this services consists of. +|=== diff --git a/content/docs/smallrye-stork/main/_generated/consul-sd-attributes.adoc b/content/docs/smallrye-stork/main/_generated/consul-sd-attributes.adoc new file mode 100644 index 000000000..007fb5d01 --- /dev/null +++ b/content/docs/smallrye-stork/main/_generated/consul-sd-attributes.adoc @@ -0,0 +1,17 @@ +[cols=",,,",options="header",] +|=== +|Attribute |Mandatory |Default Value |Description +|`consul-host` |No |`localhost` |The Consul host. +|`consul-port` |No |`8500` |The Consul port. +|`use-health-checks` |No |`true` |Whether to use health check. +|`application` |No | |The application name; if not defined Stork service name will be used. +|`refresh-period` |No |`5M` |Service discovery cache refresh period. +|`secure` |No | |whether the connection with the service should be encrypted with TLS. +|`ssl` |No |`false` |Whether to enable TLS/SSL when connecting to Consul (default: false) +|`trust-store-path` |No |++``++ |Path to the trust store file used to verify the Consul server certificate +|`trust-store-password` |No |++``++ |Password of the trust store +|`key-store-path` |No |++``++ |Path to the key store file containing the client certificate and private key +|`key-store-password` |No |++``++ |Password of the key store +|`verify-host` |No |`false` |Whether to enable hostname verification for the Consul TLS connection (default: false) +|`acl-token` |No |++``++ |Consul ACL token used for authentication when accessing the Consul API +|=== diff --git a/content/docs/smallrye-stork/main/_generated/consul-sr-attributes.adoc b/content/docs/smallrye-stork/main/_generated/consul-sr-attributes.adoc new file mode 100644 index 000000000..861f4a93b --- /dev/null +++ b/content/docs/smallrye-stork/main/_generated/consul-sr-attributes.adoc @@ -0,0 +1,16 @@ +[cols=",,,",options="header",] +|=== +|Attribute |Mandatory |Default Value |Description +|`consul-host` |No |`localhost` |The Consul host. +|`consul-port` |No |`8500` |The Consul port. +|`health-check-url` |No |++``++ |The liveness http address. +|`health-check-interval` |No |`30s` |How often Consul performs the health check +|`health-check-deregister-after` |No |`1m` |How long after the check is in critical status Consul will remove the service from the catalogue. +|`ssl` |No |`false` |Whether to enable TLS/SSL when connecting to Consul (default: false) +|`trust-store-path` |No |++``++ |Path to the trust store file used to verify the Consul server certificate +|`trust-store-password` |No |++``++ |Password of the trust store +|`key-store-path` |No |++``++ |Path to the key store file containing the client certificate and private key +|`key-store-password` |No |++``++ |Password of the key store +|`verify-host` |No |`false` |Whether to enable hostname verification for the Consul TLS connection (default: false) +|`acl-token` |No |++``++ |Consul ACL token used for authentication when accessing the Consul API +|=== diff --git a/content/docs/smallrye-stork/main/_generated/dns-sd-attributes.adoc b/content/docs/smallrye-stork/main/_generated/dns-sd-attributes.adoc new file mode 100644 index 000000000..d1e538a95 --- /dev/null +++ b/content/docs/smallrye-stork/main/_generated/dns-sd-attributes.adoc @@ -0,0 +1,14 @@ +[cols=",,,",options="header",] +|=== +|Attribute |Mandatory |Default Value |Description +|`dns-servers` |No |`none` |Comma separated list of dns servers. Servers can either be in the `server:port` or just `server` form. Use `none` to use the system resolver. +|`hostname` |No | |The hostname to look up; if not defined Stork service name will be used. +|`record-type` |No |`SRV` |Type of the DNS record. A, AAAA and SRV records are supported +|`port` |No | |Port of the service instances. Required if the record type is other than SRV. +|`refresh-period` |No |`5M` |Service discovery cache refresh period. +|`secure` |No | |Whether the connection with the service should be encrypted with TLS. +|`recursion-desired` |No |`true` |Whether DNS recursion is desired +|`resolve-srv` |No |`true` |Whether DNS resolution for SRV records is desired +|`dns-timeout` |No |`5s` |Timeout for DNS queries +|`fail-on-error` |No |`false` |Whether an error in retrieving service instances from one of the DNS servers should cause a failure of the discovery attempt. +|=== diff --git a/content/docs/smallrye-stork/main/_generated/eureka-sd-attributes.adoc b/content/docs/smallrye-stork/main/_generated/eureka-sd-attributes.adoc new file mode 100644 index 000000000..2a2581bcb --- /dev/null +++ b/content/docs/smallrye-stork/main/_generated/eureka-sd-attributes.adoc @@ -0,0 +1,13 @@ +[cols=",,,",options="header",] +|=== +|Attribute |Mandatory |Default Value |Description +|`eureka-host` |Yes | |The Eureka server host. +|`eureka-port` |No |`8761` |The Eureka server port. +|`eureka-context-path` |No |`/` |The Eureka server root context path. +|`application` |No | |The Eureka application Id; if not defined Stork service name will be used +|`eureka-trust-all` |No |`false` |Enable/Disable the TLS certificate verification +|`eureka-tls` |No |`false` |Use TLS to connect to the Eureka server +|`instance` |No | |The Eureka application instance Id +|`refresh-period` |No |`5M` |Service discovery cache refresh period. +|`secure` |No |`false` |Whether is should select the secured endpoint of the retrieved services. +|=== diff --git a/content/docs/smallrye-stork/main/_generated/eureka-sr-attributes.adoc b/content/docs/smallrye-stork/main/_generated/eureka-sr-attributes.adoc new file mode 100644 index 000000000..e18d0ba66 --- /dev/null +++ b/content/docs/smallrye-stork/main/_generated/eureka-sr-attributes.adoc @@ -0,0 +1,11 @@ +[cols=",,,",options="header",] +|=== +|Attribute |Mandatory |Default Value |Description +|`eureka-host` |No |`localhost` |The Eureka host. +|`eureka-port` |No |`8761` |The Eureka port. +|`eureka-context-path` |No |`/` |The Eureka server root context path. +|`eureka-trust-all` |No |`false` |Enable/Disable the TLS certificate verification +|`eureka-tls` |No |`false` |Use TLS to connect to the Eureka server +|`health-check-url` |No |++``++ |The liveness http address. +|`host-name` |No |++``++ |The host name to register in Eureka. If not set, the ipAddress is used. +|=== diff --git a/content/docs/smallrye-stork/main/_generated/knative-sd-attributes.adoc b/content/docs/smallrye-stork/main/_generated/knative-sd-attributes.adoc new file mode 100644 index 000000000..3b3fe0eb3 --- /dev/null +++ b/content/docs/smallrye-stork/main/_generated/knative-sd-attributes.adoc @@ -0,0 +1,9 @@ +[cols=",,,",options="header",] +|=== +|Attribute |Mandatory |Default Value |Description +|`knative-host` |No | |The Knative API host. +|`knative-namespace` |No | |The namespace of the service. Use all to discover all namespaces. +|`application` |No | |The Knative application Id; if not defined Stork service name will be used. +|`refresh-period` |No |`5M` |Service discovery cache refresh period. +|`secure` |No | |Whether the connection with the service should be encrypted with TLS. +|=== diff --git a/content/docs/smallrye-stork/main/_generated/kubernetes-sd-attributes.adoc b/content/docs/smallrye-stork/main/_generated/kubernetes-sd-attributes.adoc new file mode 100644 index 000000000..d0bbb6980 --- /dev/null +++ b/content/docs/smallrye-stork/main/_generated/kubernetes-sd-attributes.adoc @@ -0,0 +1,14 @@ +[cols=",,,",options="header",] +|=== +|Attribute |Mandatory |Default Value |Description +|`k8s-host` |No | |The Kubernetes API host. +|`k8s-namespace` |No | |The namespace of the service. Use all to discover all namespaces. +|`application` |No | |The Kubernetes application Id; if not defined Stork service name will be used. +|`port-name` |No | |The Kubernetes application port name. If not defined, when exposing multiple ports, Stork will use the first one. +|`refresh-period` |No |`5M` |Service discovery cache refresh period. +|`secure` |No | |Whether the connection with the service should be encrypted with TLS. +|`request-retry-backoff-limit` |No | |Maximum number of retry attempts allowed after a request failure. +|`request-retry-backoff-interval` |No | |Amount of time to wait between retry attempts after a request fails. +|`use-endpoint-slices` |No | |Use the EndpointSlice API instead +|`use-cluster-ip` |No |`false` |Use the Kubernetes Service ClusterIP instead of pod IPs. Stork returns a single instance with the ClusterIP and resolved port. +|=== diff --git a/content/docs/smallrye-stork/main/_generated/least-requests-lb-attributes.adoc b/content/docs/smallrye-stork/main/_generated/least-requests-lb-attributes.adoc new file mode 100644 index 000000000..60c10c523 --- /dev/null +++ b/content/docs/smallrye-stork/main/_generated/least-requests-lb-attributes.adoc @@ -0,0 +1,4 @@ +[cols=",,,",options="header",] +|=== +|Attribute |Mandatory |Default Value |Description +|=== diff --git a/content/docs/smallrye-stork/main/_generated/least-response-time-lb-attributes.adoc b/content/docs/smallrye-stork/main/_generated/least-response-time-lb-attributes.adoc new file mode 100644 index 000000000..dd977d2d0 --- /dev/null +++ b/content/docs/smallrye-stork/main/_generated/least-response-time-lb-attributes.adoc @@ -0,0 +1,7 @@ +[cols=",,,",options="header",] +|=== +|Attribute |Mandatory |Default Value |Description +|`declining-factor` |No |`0.9` |How much _score_ should decline in time, see Score calculation in the docs for details. +|`error-penalty` |No |`60s` |This load balancer treats an erroneous response as a response after this time. +|`use-secure-random` |No |`false` |Whether the load balancer should use a SecureRandom instead of a Random (default). Check https://stackoverflow.com/questions/11051205/difference-between-java-util-random-and-java-security-securerandom[this page] to understand the difference +|=== diff --git a/content/docs/smallrye-stork/main/_generated/power-of-two-choices-lb-attributes.adoc b/content/docs/smallrye-stork/main/_generated/power-of-two-choices-lb-attributes.adoc new file mode 100644 index 000000000..95a37a4df --- /dev/null +++ b/content/docs/smallrye-stork/main/_generated/power-of-two-choices-lb-attributes.adoc @@ -0,0 +1,5 @@ +[cols=",,,",options="header",] +|=== +|Attribute |Mandatory |Default Value |Description +|`use-secure-random` |No |`false` |Whether the load balancer should use a SecureRandom instead of a Random (default). Check https://stackoverflow.com/questions/11051205/difference-between-java-util-random-and-java-security-securerandom[this page] to understand the difference +|=== diff --git a/content/docs/smallrye-stork/main/_generated/random-lb-attributes.adoc b/content/docs/smallrye-stork/main/_generated/random-lb-attributes.adoc new file mode 100644 index 000000000..95a37a4df --- /dev/null +++ b/content/docs/smallrye-stork/main/_generated/random-lb-attributes.adoc @@ -0,0 +1,5 @@ +[cols=",,,",options="header",] +|=== +|Attribute |Mandatory |Default Value |Description +|`use-secure-random` |No |`false` |Whether the load balancer should use a SecureRandom instead of a Random (default). Check https://stackoverflow.com/questions/11051205/difference-between-java-util-random-and-java-security-securerandom[this page] to understand the difference +|=== diff --git a/content/docs/smallrye-stork/main/_generated/static-sd-attributes.adoc b/content/docs/smallrye-stork/main/_generated/static-sd-attributes.adoc new file mode 100644 index 000000000..469ca6230 --- /dev/null +++ b/content/docs/smallrye-stork/main/_generated/static-sd-attributes.adoc @@ -0,0 +1,7 @@ +[cols=",,,",options="header",] +|=== +|Attribute |Mandatory |Default Value |Description +|`address-list` |Yes | |A comma-separated list of addresses (host:port). The default port is 80. +|`secure` |No | |Whether the connection with the service should be encrypted with TLS. Default is false, except if the host:port uses the port is 443. +|`shuffle` |No |`false` |Whether the list of address must be shuffled to avoid using the first address on every startup. +|=== diff --git a/content/docs/smallrye-stork/main/_generated/static-sr-attributes.adoc b/content/docs/smallrye-stork/main/_generated/static-sr-attributes.adoc new file mode 100644 index 000000000..8ec570617 --- /dev/null +++ b/content/docs/smallrye-stork/main/_generated/static-sr-attributes.adoc @@ -0,0 +1,5 @@ +[cols=",,,",options="header",] +|=== +|Attribute |Mandatory |Default Value |Description +|`address-list` |Yes | |A comma-separated list of addresses (host:port). The default port is 80. +|=== diff --git a/content/docs/smallrye-stork/main/_generated/sticky-lb-attributes.adoc b/content/docs/smallrye-stork/main/_generated/sticky-lb-attributes.adoc new file mode 100644 index 000000000..e458f6468 --- /dev/null +++ b/content/docs/smallrye-stork/main/_generated/sticky-lb-attributes.adoc @@ -0,0 +1,5 @@ +[cols=",,,",options="header",] +|=== +|Attribute |Mandatory |Default Value |Description +|`failure-backoff-time` |No |`0` |After how much time, a service instance that has failed can be reused. +|=== diff --git a/content/docs/smallrye-stork/main/_nav.yml b/content/docs/smallrye-stork/main/_nav.yml new file mode 100644 index 000000000..8346a2ff0 --- /dev/null +++ b/content/docs/smallrye-stork/main/_nav.yml @@ -0,0 +1,70 @@ +title: SmallRye Stork +nav: +- title: Overview + page: index +- title: Concepts + page: concepts +- title: Programmatic API + page: programmatic-api +- title: Microprofile Configuration + page: microprofile-config +- title: Spring Boot Configuration + page: springboot-config +- title: Observability + page: observability +- title: Logging + page: logging +- title: Javadoc + url: https://javadoc.io/doc/io.smallrye.stork/smallrye-stork-api/latest/index.html +- title: Using Stork with Quarkus + page: quarkus +- title: Service Discovery + items: + - title: Overview + page: service-discovery/overview + - title: Consul + page: service-discovery/consul + - title: DNS + page: service-discovery/dns + - title: Kubernetes + page: service-discovery/kubernetes + - title: KNative + page: service-discovery/knative + - title: Eureka + page: service-discovery/eureka + - title: Composite + page: service-discovery/composite + - title: Static List + page: service-discovery/static-list + - title: Custom Service Discovery + page: service-discovery/custom-service-discovery +- title: Load-Balancing + items: + - title: Overview + page: load-balancer/overview + - title: Round Robin + page: load-balancer/round-robin + - title: Random + page: load-balancer/random + - title: Least Requests + page: load-balancer/least-requests + - title: Least Response Time + page: load-balancer/least-response-time + - title: Power Of Two Choices + page: load-balancer/power-of-two-choices + - title: Sticky + page: load-balancer/sticky + - title: Custom Load Balancer + page: load-balancer/custom-load-balancer +- title: Service Registration + items: + - title: Overview + page: service-registration/overview + - title: Consul + page: service-registration/consul + - title: Eureka + page: service-registration/eureka + - title: Static List + page: service-registration/static-list + - title: Custom Service Registration + page: service-registration/custom-service-registration diff --git a/content/docs/smallrye-stork/main/concepts.adoc b/content/docs/smallrye-stork/main/concepts.adoc new file mode 100644 index 000000000..1618802c1 --- /dev/null +++ b/content/docs/smallrye-stork/main/concepts.adoc @@ -0,0 +1,108 @@ += Concepts +include::_attributes.adoc[] + +This page presents the concepts used in Stork. +When using Stork in a _managed_ environment, such as Quarkus, all these concepts are hidden, as you only configure the lookup and selection. +However, when using the programmatic API, you will use these concepts directly. + +image::images/diagrams/plantuml-383c9b353dda.svg[stork] + +== Process overview + +When using the programmatic API of Stork, you can: + +[arabic] +. Retrieve the _singleton_ `Stork` instance. This instance is configured with the set of `Service` it manages. +. Retrieve the `Service` you want to use. Each `Service` is associated with a name. +. Retrieve the `ServiceInstance` which will provide the metadata to access the actual service. + +image::images/diagrams/plantuml-a0003ba546d7.svg[sequence] + +Behind the scenes, Stork will handle the service lookup and selection. + +[NOTE] +==== + +The service lookup and selection are asynchronous operations. +Thus, the API returns instances of `Uni`. + +==== + +== Stork + +`io.smallrye.stork.Stork` is the entry-point of the API. +The `Stork` instance is a _singleton_. +It needs to be initialized once (when the application starts) and shutdown when the application stops: + +[source,java] +---- +include::{examples-dir}/examples/StorkEntryPointExample.java[indent=0] +---- + +During the initialization, Stork looks for `io.smallrye.stork.config.ConfigProvider` SPI provider and CDI beans (from 2.x version) and retrieves the list of managed services: + +* A service is identified by a *name*. +* A service has a service discovery configuration indicating how Stork will look for service instances +* A service can have a load-balancer configuration indicating how Stork can select the most appropriate instance. + +== Service + +A `io.smallrye.stork.Service` is the structure representing a _service_ used by the application. +Services are pre-configured with their name, service discovery, and optionally, their load-balancer. +You retrieve a `Service` using the `Stork#getService(String name)` method. + +[source,java] +---- +include::{examples-dir}/examples/StorkServiceExample.java[indent=0] +---- + +The `Service` lets you retrieve the list of `ServiceInstance`, or select a single one, when a load-balancer is configured. + +== Service Instance + +The `io.smallrye.stork.api.ServiceInstance` represents an actual instance of the service. +It provides the metadata to configure a _client_ to interact with that specific instance of service. + +[source,java] +---- +include::{examples-dir}/examples/StorkServiceLookupExample.java[indent=0] +---- + +The service selection is a two-steps process: + +[arabic] +. Service lookup - using the service discovery +. Service selection - using the load balancer + +[source,java] +---- +include::{examples-dir}/examples/StorkServiceSelectionExample.java[indent=0] +---- + +== Service Discovery + +The `io.smallrye.stork.api.ServiceDiscovery` represents a service discovery mechanism, such as DNS, Consul, or Eureka. + +You can implement a custom service discovery for Stork by implementing the `ServiceDiscoveryProvider` interface. +The corresponding `ServiceRegistrarProviderLoader` and `RegistrarConfiguration` classes will be automatically generated during compilation time. + +Please note that the `ServiceDiscovery` implementation must be non-blocking. + +== Load Balancer + +The `io.smallrye.stork.api.LoadBalancer` represents a load-balancer strategy, such as round-robin. + +To implement a custom load balancer for Stork, implement the `LoadBalancerProvider` interface. +The corresponding `LoadBalancerProviderLoader` and `Configuration` classes will be automatically generated during compilation time. + +Please note that the `LoadBalancer` implementation, similarly to `ServiceDiscovery` +must be non-blocking. + +== Service registration + +The `io.smallrye.stork.api.ServiceRegistrar` represents a service registration mechanism for Consul and Eureka. + +You can implement a custom service registrar for Stork by implementing the `ServiceRegistrarProvider` interface. +The corresponding `ServiceRegistrarProviderLoader` and `RegistrarConfiguration` classes will be automatically generated during compilation time. + +Please note that the `ServiceRegistrar` implementation must be non-blocking. diff --git a/content/docs/smallrye-stork/main/images/diagrams/plantuml-383c9b353dda.svg b/content/docs/smallrye-stork/main/images/diagrams/plantuml-383c9b353dda.svg new file mode 100644 index 000000000..0e4b865f3 --- /dev/null +++ b/content/docs/smallrye-stork/main/images/diagrams/plantuml-383c9b353dda.svg @@ -0,0 +1 @@ +«entry-point»StorkServiceDiscoveryServiceInstanceLoadBalancer«named»ServiceKubernetesConsulCustom Service Discovery«default»RoundRobinLeastResponseTimeCustom Load Balancer*selectServiceInstance \ No newline at end of file diff --git a/content/docs/smallrye-stork/main/images/diagrams/plantuml-3c6c7f0192c2.svg b/content/docs/smallrye-stork/main/images/diagrams/plantuml-3c6c7f0192c2.svg new file mode 100644 index 000000000..59018984f --- /dev/null +++ b/content/docs/smallrye-stork/main/images/diagrams/plantuml-3c6c7f0192c2.svg @@ -0,0 +1 @@ +ApplicationApplicationObservableStorkInfrastructureObservableStorkInfrastructureObservationCollectorObservationCollectorStorkStorkServiceServiceApplicationObservableStorkInfrastructureObservationCollectorStorkServiceApplicationObservableStorkInfrastructureObservationCollectorStorkService(1)instantiates(2)instantiates(3)ObservationCollector(4)ObservableStorkInfrastructure (5)initialize(observableInfrastructure)(6)instantiates (..., ObservationCollector, ...) \ No newline at end of file diff --git a/content/docs/smallrye-stork/main/images/diagrams/plantuml-a0003ba546d7.svg b/content/docs/smallrye-stork/main/images/diagrams/plantuml-a0003ba546d7.svg new file mode 100644 index 000000000..9c1a99915 --- /dev/null +++ b/content/docs/smallrye-stork/main/images/diagrams/plantuml-a0003ba546d7.svg @@ -0,0 +1 @@ +ApplicationApplicationApplicationStorkStorkStorkConfigProviderConfigProviderConfigProviderServiceServiceServiceService DiscoveryService DiscoveryService DiscoveryLoad BalancerLoad BalancerLoad BalancerApplicationStorkConfigProviderServiceService DiscoveryLoad BalancerApplicationStorkConfigProviderServiceService DiscoveryLoad Balancer(1)initialize()(2)getConfig()(3): List<ServiceConfig> (4)getService("name")(5)Service  (6)selectServiceInstance()(7)getServiceInstances()(8)Uni<List<ServiceInstance>>(9)selectServiceInstance()(10)Uni<ServiceInstance>(11)Uni<ServiceInstance> \ No newline at end of file diff --git a/content/docs/smallrye-stork/main/images/diagrams/plantuml-bc915c97a9fa.svg b/content/docs/smallrye-stork/main/images/diagrams/plantuml-bc915c97a9fa.svg new file mode 100644 index 000000000..39d2fba6a --- /dev/null +++ b/content/docs/smallrye-stork/main/images/diagrams/plantuml-bc915c97a9fa.svg @@ -0,0 +1 @@ +ApplicationStorkDNS ServerApplicationStorkDNS ServerApplicationStorkDNS Serverget service instancesget SRV records for hostnamelist of SRV recordslist of SRV record targets if no resolution desiredget A/AAAA records for each SRV record targetlist of A/AAAAlist of ServiceInstances \ No newline at end of file diff --git a/content/docs/smallrye-stork/main/images/diagrams/plantuml-c8137b2f4bb7.svg b/content/docs/smallrye-stork/main/images/diagrams/plantuml-c8137b2f4bb7.svg new file mode 100644 index 000000000..c2ebac7a1 --- /dev/null +++ b/content/docs/smallrye-stork/main/images/diagrams/plantuml-c8137b2f4bb7.svg @@ -0,0 +1 @@ +ServiceServiceServiceServiceServiceObservationCollectorObservationCollectorObservationCollectorObservationCollectorObservationCollectorStorkObservationStorkObservationStorkObservationStorkObservationStorkObservationStorkEventHandlerStorkEventHandlerStorkEventHandlerStorkEventHandlerStorkEventHandlerServiceObservationCollectorStorkObservationStorkEventHandlerServiceObservationCollectorStorkObservationStorkEventHandler(1)create("serviceName", sd type, ss type)(2)instantiates(3)Registers start time(4)StorkObservation (5)onServiceDiscoverySuccess(List<ServiceInstance>)(6)Registers end servicediscovery time.Registers instances count  (7)onServiceDiscoveryFailure(Throwable)(8)Registers end servicediscovery time.Registers failure cause  (9)onServiceSelectionSuccess(instanceId)(10)Registers end serviceselection time.Registers instance idRegisters overall duration (11)complete(this) (12)onServiceSelectionFailure(Throwable))(13)Registers overall duration.Registers failure cause (14)complete(this) \ No newline at end of file diff --git a/content/docs/smallrye-stork/main/images/problem-dark.png b/content/docs/smallrye-stork/main/images/problem-dark.png new file mode 100644 index 000000000..4c601386a Binary files /dev/null and b/content/docs/smallrye-stork/main/images/problem-dark.png differ diff --git a/content/docs/smallrye-stork/main/images/problem-light.png b/content/docs/smallrye-stork/main/images/problem-light.png new file mode 100644 index 000000000..cc0cff154 Binary files /dev/null and b/content/docs/smallrye-stork/main/images/problem-light.png differ diff --git a/content/docs/smallrye-stork/main/images/redhat_reversed.svg b/content/docs/smallrye-stork/main/images/redhat_reversed.svg new file mode 100644 index 000000000..37fe83c48 --- /dev/null +++ b/content/docs/smallrye-stork/main/images/redhat_reversed.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/content/docs/smallrye-stork/main/images/solution-dark.png b/content/docs/smallrye-stork/main/images/solution-dark.png new file mode 100644 index 000000000..800d0f978 Binary files /dev/null and b/content/docs/smallrye-stork/main/images/solution-dark.png differ diff --git a/content/docs/smallrye-stork/main/images/solution-light.png b/content/docs/smallrye-stork/main/images/solution-light.png new file mode 100644 index 000000000..044bd3786 Binary files /dev/null and b/content/docs/smallrye-stork/main/images/solution-light.png differ diff --git a/content/docs/smallrye-stork/main/images/stork-white.png b/content/docs/smallrye-stork/main/images/stork-white.png new file mode 100644 index 000000000..8ceae5659 Binary files /dev/null and b/content/docs/smallrye-stork/main/images/stork-white.png differ diff --git a/content/docs/smallrye-stork/main/index.adoc b/content/docs/smallrye-stork/main/index.adoc new file mode 100644 index 000000000..094027edf --- /dev/null +++ b/content/docs/smallrye-stork/main/index.adoc @@ -0,0 +1,45 @@ += Overview +include::_attributes.adoc[] + +SmallRye Stork `{project-version}` is a service discovery and client-side load-balancing framework. + +The essence of distributed systems resides in the interaction between _services_. +In modern architecture, you often have multiple instances of your service to share the load or improve the resilience by redundancy. +But, how do you select the _best_ instance of your service? +That's where Stork helps. +Stork is going to select the most appropriate instance. +It offers: + +* Extensible service discovery mechanisms +* Built-in support for Consul and Kubernetes +* Customizable client load-balancing strategies +* A programmatic API and a _managed_ approach +* A Quarkus integration, but Stork can be used in any environment + +== The problem + +In distributed systems, applications typically need to call one another. +In a monolithic application, components invoke one another through language-level method or procedure calls. +In a traditional distributed system deployment, services run at fixed, well-known locations (schemes, hosts, and ports) and can call one another using HTTP/REST or some RPC mechanism. The service locations are often hardcoded in the application configuration. +Nevertheless, a modern distributed system or microservice-based application typically runs in virtualized or containerized environments where the number of instances of a service and their locations change dynamically. +IPs get randomly assigned, and instances can be created or destroyed at any time. +With such dynamics, hard-coded locations are a dead-end. + +.the problem +image::images/problem-light.png[the problem] + +== The solution + +Stork handles the service lookup and selection. +It proposes an extensible set of service discovery mechanisms and load-balancing strategies. + +.the solution +image::images/solution-light.png[the solution] + +== What if the infrastructure provides such a feature? + +Some infrastructure, such as Kubernetes, provides service discoveries and load-balancing features. +However, these mechanisms often lack flexibility. You cannot influence the service instance selection, and the load-balancing strategy is generally a simple _round-robin_. + +Stork provides more flexibility in the service instance selection. For example, it can select the fastest instance (based on the previous calls) to improve the response time. +However, if you don't need that flexibility, just use the infrastructure layer. diff --git a/content/docs/smallrye-stork/main/load-balancer/custom-load-balancer.adoc b/content/docs/smallrye-stork/main/load-balancer/custom-load-balancer.adoc new file mode 100644 index 000000000..c8d02933b --- /dev/null +++ b/content/docs/smallrye-stork/main/load-balancer/custom-load-balancer.adoc @@ -0,0 +1,145 @@ += Implement your own load balancer mechanism +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Stork is extensible, and you can implement your service selection (load-balancer) mechanism. + +== Dependencies + +To implement your _Load Balancer Provider_, make sure your project depends on Core and Configuration Generator. +The former brings classes necessary to implement custom load balancer, the latter contains an annotation processor that generates classes needed by Stork. + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-core + {project-version} + + + io.smallrye.stork + stork-configuration-generator + provided + + {project-version} + +---- + +== Implementing a load balancer provider + +Load balancer implementation consists of three elements: + +* `LoadBalancer` which is responsible for selecting service instances for a single Stork service, +* `LoadBalancerProvider` which creates instances of `LoadBalancer` for a given load balancer _type_, +* `$typeConfiguration` which is a configuration for the load balancer. This class is automatically generated. + +A _type_, for example `acme-load-balancer`, identifies each provider. +This _type_ is used in the configuration to reference the provider: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.load-balancer.type=acme-load-balancer +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.load-balancer.type=acme-load-balancer +---- + +-- + +==== + +A `LoadBalancerProvider` implementation needs to be annotated with `@LoadBalancerType` that defines the _type_. +Any configuration properties that the provider expects should be defined with `@LoadBalancerAttribute` annotations placed on the provider. +Optionally, you can also add `@ApplicationScoped` annotation in order to provide the load balancer implementation as CDI bean. + +A load balancer provider class should look as follows: + +[source,java] +---- +include::{examples-dir}/examples/AcmeLoadBalancerProvider.java[indent=0] +---- + +Note, that the `LoadBalancerProvider` interface takes a configuration class as a parameter. +This configuration class is generated automatically by the _Configuration Generator_. +Its name is created by appending `Configuration` to the load balancer type, like `AcmeLoadBalancerConfiguration`. + +The next step is to implement the `LoadBalancer` interface. + +The essence of load balancers' work happens in the `selectServiceInstance` method. The method returns a single `ServiceInstance` from a collection. + +[source,java] +---- +include::{examples-dir}/examples/AcmeLoadBalancer.java[indent=0] +---- + +This implementation is simplistic and just picks a random instance from the received list. + +Some load balancers make the pick based on statistics such as calls in progress or response times, or amount of errors of a service instance. +To collect this information in your load balancer, you can wrap the selected service instance into `ServiceInstanceWithStatGathering`. + +Load balancers based on statistics often expect that an operation using a selected service instance is marked as started before the next selection. +By default, Stork assumes that a `LoadBalancer` requires this and guards the calls accordingly. +If this is not the case for your implementation, override the `requiresStrictRecording()` method to return `false`. + +== Using your load balancer + +In the project using it, don't forget to add the dependency on the module providing your implementation. +Then, in the configuration, just add: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-discovery.type=... +stork.my-service.load-balancer.type=acme-load-balancer\ +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.type=... +quarkus.stork.my-service.load-balancer.type=acme-load-balancer +---- + +-- + +==== + +Then, Stork will use your implementation to select the `my-service` service instance. + +== Using your load balancer using the programmatic API + +When building your load balancer project, the configuration generator creates a configuration class. +This class can be used to configure your load balancer using the Stork programmatic API. + +[source,java] +---- +include::{examples-dir}/examples/AcmeSelectorApiUsage.java[indent=0] +---- + +Remember that attributes, like `my-attribute`, are declared using the `@LoadBalancerAttribute` annotation on the `LoadBalancerProvider` implementation. diff --git a/content/docs/smallrye-stork/main/load-balancer/least-requests.adoc b/content/docs/smallrye-stork/main/load-balancer/least-requests.adoc new file mode 100644 index 000000000..6a8babbc6 --- /dev/null +++ b/content/docs/smallrye-stork/main/load-balancer/least-requests.adoc @@ -0,0 +1,59 @@ += Least Requests Load Balancing +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The `least-requests` load balancing strategy monitors the number of inflight calls and selects the less-used instance. + +This strategy keeps track of the inflight calls made by the application and picks the service instance with the smallest number of inflight requests: + +[arabic] +. when the selection happens, the service instance with the smallest number of inflight requests is selected, and this number is incremented +. when the operation completes, successfully or not, the number of inflight requests is decremented + +== Dependency + +First, you need to add the `least-requests` load-balancer to your project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-load-balancer-least-requests + {project-version} + +---- + +== Configuration + +For each service expected to use a least-response-time selection, configure the `load-balancer` to be `least-requests`: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.load-balancer.type=least-requests +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.load-balancer.type=least-requests +---- + +-- + +==== + +Supported configuration properties are the following: + +include::{generated-dir}/least-requests-lb-attributes.adoc[] diff --git a/content/docs/smallrye-stork/main/load-balancer/least-response-time.adoc b/content/docs/smallrye-stork/main/load-balancer/least-response-time.adoc new file mode 100644 index 000000000..9742a0b23 --- /dev/null +++ b/content/docs/smallrye-stork/main/load-balancer/least-response-time.adoc @@ -0,0 +1,94 @@ += Least Response Time Load Balancing +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The `least-response-time` load balancing strategy collects response times of the calls made with service instances and picks an instance based on this information. + +Erroneous responses are treated as responses with a long response time, by default 60 seconds. This can be controlled with the `error-penalty` attribute. + +The algorithm for service instance selection is as follows: + +* if there is a service instance that wasn't used before - use it, otherwise: +* if there are any service instances with collected response times - select the one for which _score_ is the lowest, otherwise: +* select a random instance + +The _score_ for an instance decreases in time if an instance is not used. This way we ensure that instances that haven't been used in a long time, are retried. + +For the details on the _score_ calculation, see xref:#score-calculation[Score calculation] + +== Dependency + +To use this load balancer, start with adding the least-response-time load-balancer dependency to your project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-load-balancer-least-response-time + {project-version} + +---- + +== Configuration + +For each service expected to use a least-response-time selection, configure the `load-balancer` to be `least-response-time`: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.load-balancer.type=least-response-time +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.load-balancer.type=least-response-time +---- + +-- + +==== + +The following attributes are supported: + +include::{generated-dir}/least-response-time-lb-attributes.adoc[] + +== Score calculation + +The _score_ of a service instance is calculated by dividing a weighted sum of response times by sum of the weighs. The result is additionally adjusted to account for instances that haven't been used for a long time. + +Let: + +* $n$ denote how many instance selections were made so far +* $t++_++i$ denote the response time for call $i$ +* $n++_++i$ denote the number of instance selections done until the moment of recording the response time for call $i$ +* $n++_{++max}$ denote the number of instance selections at the moment of last call recorded with this instance +* $$ denote a configurable `declining-factor` + +The idea for the weight is to decrease the importance of the data collected long time (many calls) ago. For call $i$, the weight is calculated as follows: +$$ +w++_++i = ^ ++{++(n - n++_++i)} +$$ + +The score of a service instance is calculated as: +$$ +score(n) = ^++{++n - n++_{++max}} ++*++ = +^++{++n - n++_{++max}} ++*++ +$$ + +The `declining-factor` should be in $(0, 1++]++$ , the default is $0.9$. Using a lower value makes the older response times less important. + +Supported configuration properties are the following: + +include::{generated-dir}/least-response-time-lb-attributes.adoc[] diff --git a/content/docs/smallrye-stork/main/load-balancer/overview.adoc b/content/docs/smallrye-stork/main/load-balancer/overview.adoc new file mode 100644 index 000000000..62c2f482c --- /dev/null +++ b/content/docs/smallrye-stork/main/load-balancer/overview.adoc @@ -0,0 +1,22 @@ += Overview +:version-root: ../ +include::{version-root}_attributes.adoc[] + +== Load Balancer / Service Selection in SmallRye Stork + +Once services are registered and discovered, the next critical step is selecting which service instance will handle a given request. +SmallRye Stork provides flexible load balancing strategies to efficiently distribute requests across multiple instances of a service. +This ensures optimal resource usage, improved performance, and high availability. + +=== Key Features: + +* *Multiple Load Balancing Strategies*: SmallRye Stork supports several built-in strategies for selecting service instances. +Check them out in the following dedicated sections. +* *Customizable Strategies*: You can define custom service selection strategies based on your unique use case or performance requirements, ensuring that the load balancer can adapt to specific needs. + +==== How it Works: + +Once a service has been registered and discovered, the load balancer comes into play when a client makes a request to that service. +Stork applies the configured load balancing strategy to select an instance from the available pool of discovered services. + +This feature ensures that your services remain responsive, scalable, and resilient, providing a smooth experience for both users and developers. diff --git a/content/docs/smallrye-stork/main/load-balancer/power-of-two-choices.adoc b/content/docs/smallrye-stork/main/load-balancer/power-of-two-choices.adoc new file mode 100644 index 000000000..9e73e9897 --- /dev/null +++ b/content/docs/smallrye-stork/main/load-balancer/power-of-two-choices.adoc @@ -0,0 +1,63 @@ += Power Of Two Choices Load Balancing +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The `power-of-two-choices` load balancing selects two random service instances and then chooses the one with the least inflight requests. +It avoids the overhead of `least-requests` and the worst case for `random` where it selects a busy destination. + +This strategy acts as follows: + +[arabic] +. when the selection happens, it picks two random instances from the list, +. it returns the least loaded instance (based on the number of inflight requests), +. when the operation completes, successfully or not, the number of inflight requests for the instance is decremented. + +Check http://www.eecs.harvard.edu/~michaelm/NEWWORK/postscripts/twosurvey.pdf[The Power of Two Random Choices] paper to learn more about this pattern and the benefits. + +== Dependency + +First, you need to add the random load-balancer to your project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-load-balancer-power-of-two-choices + {project-version} + +---- + +== Configuration + +For each service expected to use a random service selection, configure the `load-balancer` to be `power-of-two-choices`: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.load-balancer.type=power-of-two-choices +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.load-balancer.type=power-of-two-choices +---- + +-- + +==== + +Supported configuration properties are the following: + +include::{generated-dir}/power-of-two-choices-lb-attributes.adoc[] diff --git a/content/docs/smallrye-stork/main/load-balancer/random.adoc b/content/docs/smallrye-stork/main/load-balancer/random.adoc new file mode 100644 index 000000000..f721585d7 --- /dev/null +++ b/content/docs/smallrye-stork/main/load-balancer/random.adoc @@ -0,0 +1,53 @@ += Random Load Balancing +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The `random` load balancing is a straightforward service instance selection solution that picks a random instance every time. + +== Dependency + +First, you need to add the random load-balancer to your project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-load-balancer-random + {project-version} + +---- + +== Configuration + +For each service expected to use a random service selection, configure the `load-balancer` to be `random`: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.load-balancer.type=random +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.load-balancer.type=random +---- + +-- + +==== + +Supported attributes are the following: + +include::{generated-dir}/random-lb-attributes.adoc[] diff --git a/content/docs/smallrye-stork/main/load-balancer/round-robin.adoc b/content/docs/smallrye-stork/main/load-balancer/round-robin.adoc new file mode 100644 index 000000000..e1f768f10 --- /dev/null +++ b/content/docs/smallrye-stork/main/load-balancer/round-robin.adoc @@ -0,0 +1,43 @@ += Round-Robin Load Balancing +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The round-robin is a straightforward load-balancing solution that just iterates over the set of service instances. +While being simple, this solution shares the load among the instances and may be sufficient in many cases. + +The round-robin strategy is the default load-balancing strategy. +It is provided by Stork itself, and so does not require an additional dependency. + +== Configuration + +There is no need to configure the load-balancing strategy to be `round-robin`. +Stork automatically uses this strategy when none are configured. + +However, you can also configure it explicitly as follows: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.load-balancer.type=round-robin +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.load-balancer.type=round-robin +---- + +-- + +==== diff --git a/content/docs/smallrye-stork/main/load-balancer/sticky.adoc b/content/docs/smallrye-stork/main/load-balancer/sticky.adoc new file mode 100644 index 000000000..8af192d26 --- /dev/null +++ b/content/docs/smallrye-stork/main/load-balancer/sticky.adoc @@ -0,0 +1,52 @@ += Sticky Load Balancing +:version-root: ../ +include::{version-root}_attributes.adoc[] + +The sticky load balancer selects a single service instance and keeps using it until it fails. +Then, it selects another one. + +It is possible to configure a backoff time to specify for how long a failing service instance should not be retried. + +Precisely, the load balancer works as follows: + +* if no service instance has been selected so far, select the first instance from the collection; +* else if the previously selected service instance has not failed, and is still available, return it; +* else return the first available service instance that has no recorded failure, if one exists; +* else, find the available instance for which the time since the last failure is the longest, and +** if the backoff time since the failure passed, return it; +** or, throw an `NoAcceptableServiceInstanceFoundException` as no acceptable instances are available. + +== Configuration + +To use the `sticky` load service selection strategy, set the load balancer type to `sticky`: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.load-balancer.type=sticky +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.load-balancer.type=sticky +---- + +-- + +==== + +The following attributes are supported: + +include::{generated-dir}/sticky-lb-attributes.adoc[] diff --git a/content/docs/smallrye-stork/main/logging.adoc b/content/docs/smallrye-stork/main/logging.adoc new file mode 100644 index 000000000..2f0a9ce7c --- /dev/null +++ b/content/docs/smallrye-stork/main/logging.adoc @@ -0,0 +1,78 @@ += Logging Configuration +include::_attributes.adoc[] + +Stork uses https://github.com/jboss-logging/jboss-logging[JBoss Logging] for all its logging needs. JBoss Logging is a logging facade that can delegate to various logging frameworks such as Log4j, SLF4J, or JDK logging. + +== Logger Categories + +Stork uses the following main logger categories: + +=== Core Loggers + +* `io.smallrye.stork.Stork` - Main Stork class, logs service initialization and configuration +* `io.smallrye.stork.impl.CachingServiceDiscovery` - Service discovery caching operations +* `io.smallrye.stork.utils.StorkConfigUtils` - Configuration parsing and validation + +=== Service Discovery Loggers + +* `io.smallrye.stork.servicediscovery.kubernetes.KubernetesServiceDiscovery` - Kubernetes service discovery events +* `io.smallrye.stork.servicediscovery.dns.DnsServiceDiscovery` - DNS-based service discovery +* `io.smallrye.stork.servicediscovery.knative.KnativeServiceDiscovery` - KNative service discovery events + +=== Load Balancer Loggers + +* `io.smallrye.stork.loadbalancer.leastresponsetime.LeastResponseTimeLoadBalancer` - Least response time load balancer selection logic + +=== Service Registration Loggers + +* `io.smallrye.stork.serviceregistration.consul.ConsulServiceRegistrar` - Consul service registration/deregistration +* `io.smallrye.stork.serviceregistration.consul.ConsulServiceRegistrarProvider` - Consul registrar provider initialization +* `io.smallrye.stork.serviceregistration.staticlist.StaticListServiceRegistrar` - Static list service registration +* `io.smallrye.stork.serviceregistration.staticlist.StaticListServiceRegistrarProvider` - Static list registrar provider +* `io.smallrye.stork.serviceregistration.eureka.EurekaServiceRegistrar` - Eureka service registration/deregistration + +=== Configuration Provider Loggers + +* `io.smallrye.stork.microprofile.MicroProfileConfigProvider` - MicroProfile configuration loading + +== Configuring Log Levels with Quarkus + +In your `application.properties`: + +[source,properties] +---- +# Set global Stork log level +quarkus.log.category."io.smallrye.stork".level=DEBUG + +# Service discovery logging +quarkus.log.category."io.smallrye.stork.servicediscovery".level=DEBUG + +# Specific implementations +quarkus.log.category."io.smallrye.stork.servicediscovery.kubernetes.KubernetesServiceDiscovery".level=TRACE +quarkus.log.category."io.smallrye.stork.servicediscovery.dns.DnsServiceDiscovery".level=DEBUG + +# Load balancer logging +quarkus.log.category."io.smallrye.stork.loadbalancer".level=DEBUG + +# Service registration logging +quarkus.log.category."io.smallrye.stork.serviceregistration".level=INFO +quarkus.log.category."io.smallrye.stork.serviceregistration.consul.ConsulServiceRegistrar".level=DEBUG + +# Console output format +quarkus.log.console.format=%d{HH:mm:ss} %-5p [%c{2.}] (%t) %s%e%n +---- + +== Troubleshooting + +=== Too Many Logs + +If you're getting too many debug logs in production: + +[arabic] +. Set the root Stork logger to INFO or WARN: ++ +[source,properties] +---- +quarkus.log.category."io.smallrye.stork".level=INFO +---- +. Selectively enable DEBUG only for components you're troubleshooting diff --git a/content/docs/smallrye-stork/main/microprofile-config.adoc b/content/docs/smallrye-stork/main/microprofile-config.adoc new file mode 100644 index 000000000..48ba3a487 --- /dev/null +++ b/content/docs/smallrye-stork/main/microprofile-config.adoc @@ -0,0 +1,30 @@ += Stork MicroProfile Config +include::_attributes.adoc[] + +Stork integrates with MicroProfile Configuration out of the box, enabling seamless access to configuration properties. +This documentation explains how Stork can retrieve configuration details from the MicroProfile Config file present in the classpath. +Quarkus uses this approach for reading configuration details from the MicroProfile Config file located within the classpath. + +== Dependency setup + +To enable MicroProfile Config integration in Stork, you need to include the following dependency: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + smallrye-stork-microprofile + {project-version} + +---- + +== Initializing Stork + +If your framework lacks a pre-configured Stork instance, you'll need to perform initialization: + +[source,java] +---- +include::{examples-dir}/examples/InitializationExample.java[indent=0] +---- + +Upon initialization, Stork scans for the `io.smallrye.stork.config.MicroProfileConfigProvider` SPI provider and CDI beans (from version 2.x onwards). It then builds a comprehensive list of managed services by parsing the properties configuration files. diff --git a/content/docs/smallrye-stork/main/observability.adoc b/content/docs/smallrye-stork/main/observability.adoc new file mode 100644 index 000000000..3409d5a75 --- /dev/null +++ b/content/docs/smallrye-stork/main/observability.adoc @@ -0,0 +1,112 @@ += Stork Observability API +include::_attributes.adoc[] + +Stork proposes an observability API that automatically observes some parameters to show how the Stork service discovery and selection are behaving. + +For any _observation_ to happen, you need to provide your own implementation of an `ObservationCollector.` By default, Stork provides a no-op implementation. + +The `ObservationCollector` is responsible for instantiating the `StorkObservation`. + +The `StorkObservation` reacts to Stork events thanks to a `StorkEventHandler`. + +You can extend the metrics collection by extending the `StorkEventHandler` interface. + +The following sequence diagram shows how the observability is initialized : + +image::images/diagrams/plantuml-3c6c7f0192c2.svg[observability_sequence] + +The `StorkObservation` registers times, number of discovered instances, the selected instance and failures by reacting to the lifecycle of a Stork event such as: + +* start : Observation has been started. +The beginning time is registered. +It happens when the `ObservationCollector#create()` method gets called. +* service discovery success: a collection of instances has been successfully discovered for a service. +The end discovery time and number of instances are recorded. +It happens when the `StorkObservation#onServiceDiscoverySuccess` gets called. +* service discovery error: an error occurs when discovering a service. +The end discovery time and failure cause are captured. +It happens when the `StorkObservation#onServiceDiscoveryFailure` gets called. +* service selection success: an instance has been successfully selected from the collection. +The end selection time and selected instance ID are registered. +It happens when the `StorkObservation#onServiceSelectionSuccess` gets called. +* service selection error: an error occurred during selecting the instance. +End selection time and failure cause are registered. +It happens when the `StorkObservation#onServiceSelectionFailure` gets called. +* end: Observation has finished. Overall duration is registered. +It happens when the `StorkObservation#onServiceSelectionSuccess` gets called. + +The following sequence diagram represents the described observation process above: + +image::images/diagrams/plantuml-c8137b2f4bb7.svg[observation_sequence] + +== Implementing an observation collector + +An `ObservationCollector` implementation must override the `create` method to provide an instance of StorkObservation. +In addition, the user can access and enrich the observation data through the `StorkEventHandler`. + +A custom observation collector class should look as follows: + +[source,java] +---- +include::{examples-dir}/examples/AcmeObservationCollector.java[indent=0] +---- + +The next step is to initialize Stork with an `ObservableStorkInfrastructure`, taking an instance of your `ObservationCollector` as parameter. + +[source,java] +---- +include::{examples-dir}/examples/ObservableInitializationExample.java[indent=0] +---- + +Then, Stork uses your implementation to register metrics. + +== Observing service discovery and selection behaviours + +To access metrics registered by `StorkObservation`, use the following code: + +[source,java] +---- +include::{examples-dir}/examples/ObservationExample.java[indent=0] +---- + +Stork Observability with Quarkus + +Stork metrics are automatically enabled when using Stork together with the Micrometer extension in a Quarkus application. + +Micrometer collects the metrics of the rest and grpc client using Stork, as well as when using the Stork API. + +As an example, if you export the metrics to Prometheus, you will get: + +[source,text] +---- +# HELP stork_load_balancer_failures_total The number of failures during service selection. +# TYPE stork_load_balancer_failures_total counter +stork_load_balancer_failures_total{service_name="hello-service",} 0.0 +# HELP stork_service_selection_duration_seconds The duration of the selection operation +# TYPE stork_service_selection_duration_seconds summary +stork_service_selection_duration_seconds_count{service_name="hello-service",} 13.0 +stork_service_selection_duration_seconds_sum{service_name="hello-service",} 0.001049291 +# HELP stork_service_selection_duration_seconds_max The duration of the selection operation +# TYPE stork_service_selection_duration_seconds_max gauge +stork_service_selection_duration_seconds_max{service_name="hello-service",} 0.0 +# HELP stork_overall_duration_seconds_max The total duration of the Stork service discovery and selection operations +# TYPE stork_overall_duration_seconds_max gauge +stork_overall_duration_seconds_max{service_name="hello-service",} 0.0 +# HELP stork_overall_duration_seconds The total duration of the Stork service discovery and selection operations +# TYPE stork_overall_duration_seconds summary +stork_overall_duration_seconds_count{service_name="hello-service",} 13.0 +stork_overall_duration_seconds_sum{service_name="hello-service",} 0.001049291 +# HELP stork_service_discovery_failures_total The number of failures during service discovery +# TYPE stork_service_discovery_failures_total counter +stork_service_discovery_failures_total{service_name="hello-service",} 0.0 +# HELP stork_service_discovery_duration_seconds_max The duration of the discovery operation +# TYPE stork_service_discovery_duration_seconds_max gauge +stork_service_discovery_duration_seconds_max{service_name="hello-service",} 0.0 +# HELP stork_service_discovery_duration_seconds The duration of the discovery operation +# TYPE stork_service_discovery_duration_seconds summary +stork_service_discovery_duration_seconds_count{service_name="hello-service",} 13.0 +stork_service_discovery_duration_seconds_sum{service_name="hello-service",} 6.585046209 +# HELP stork_instances_count_total The number of service instances discovered +# TYPE stork_instances_count_total counter +stork_instances_count_total{service_name="hello-service",} 26.0 +---- diff --git a/content/docs/smallrye-stork/main/programmatic-api.adoc b/content/docs/smallrye-stork/main/programmatic-api.adoc new file mode 100644 index 000000000..32f17e23b --- /dev/null +++ b/content/docs/smallrye-stork/main/programmatic-api.adoc @@ -0,0 +1,54 @@ += Stork Programmatic API +include::_attributes.adoc[] + +Stork proposes a programmatic API that lets you register new service Definitions and do manual lookup and selection. +When using the programmatic API of Stork, you can: +Retrieve the singleton Stork instance. This instance is configured with the set of Services it manages. +Register new service definition. +Retrieve the Service you want to use. Each Service is associated with a name. +Retrieve the ServiceInstance, which will provide the metadata to access the actual instance. + +== Initializing Stork + +If your framework does not already provide a configured `Stork` instance, you need to do: + +[source,java] +---- +include::{examples-dir}/examples/InitializationExample.java[indent=0] +---- + +== Adding service dynamically + +To register a new `ServiceDefinition`, use the `defineIfAbsent` method: + +[source,java] +---- +include::{examples-dir}/examples/DefinitionExample.java[indent=0] +---- + +The `ServiceDefinition` instances can be created from: + +* A service discovery configuration - these classes are provided by the service discovery implementations, +* An optional load balancer configuration - these classes are provided by the load balancer implementations + +Attributes from the service discovery and load balancer can be configured from the `Configuration` classes. + +== Looking for service instances + +To list the service instances for a given service, or to select an instance according to the load balancer strategy, use the following code: + +[source,java] +---- +include::{examples-dir}/examples/LookupExample.java[indent=0] +---- + +The lookup and selection methods are returning Uni as these processes are asynchronous. + +== All in one example + +The following snippet provides an _all in one_ example of the Stork programmatic API: + +[source,java] +---- +include::{examples-dir}/examples/StorkApiExample.java[indent=0] +---- diff --git a/content/docs/smallrye-stork/main/quarkus.adoc b/content/docs/smallrye-stork/main/quarkus.adoc new file mode 100644 index 000000000..08c9b3186 --- /dev/null +++ b/content/docs/smallrye-stork/main/quarkus.adoc @@ -0,0 +1,79 @@ += Using Stork with Quarkus +include::_attributes.adoc[] + +https://quarkus.io[Quarkus] is a Kubernetes Native Java stack tailored for OpenJDK HotSpot and GraalVM. + +Quarkus REST Client Reactive and gRPC extensions come with built-in integration with SmallRye Stork. + +This page describes how to use REST Client Reactive with Stork. Using gRPC with Stork is similar. + +We will use the Consul service discovery and the round-robin load balancer as examples. + +== The project + +You can create a Quarkus project with the REST Client Reactive extension using https://code.quarkus.io[code.quarkus.io]. + +The corresponding https://quarkus.io/guides/rest-client-reactive[Quarkus guide] describes the extension in more detail. + +== The client + +To use the REST client to communicate with a remote endpoint, you need to create an interface that describes how the communication should work. +The client requires `baseUri` (or `baseUrl`) pointing to the address of the remote endpoint. + +To use Stork to determine the actual address, set the scheme of the URI to `stork` and the hostname of the URI to the name of the Stork service. + +For example, the `HelloClient` below will use the Stork service called `hello-service` to determine the address of the destination, and `/hello` as the base path for queries: + +[source,java] +---- +include::{examples-dir}/examples/HelloClient.java[indent=0] +---- + +== The service + +In Stork, a `Service` consists of service discovery and a load balancer. +The Service discovery is responsible for determining the `ServiceInstance`s, that is, available addresses for a service. +The load balancer picks a single `ServiceInstance` for a call. + +=== Dependencies + +To use the service discovery and the load balancer of your choosing, you need to add the appropriate dependencies to your application. +For example, if you wish to use Consul and load-balance the calls with round-robin, add the following to your `pom.xml`: + +[source,xml] +---- + + io.smallrye.stork + stork-service-discovery-consul + +---- + +[NOTE] +==== + +No need to add a dependency for the round-robin load-balancer. This strategy is built-in. +Also, no need to specify the stork version, Quarkus imports the Stork _BOM_. + +==== + +When using Eureka, Kubernetes, or any other service discovery mechanism, or a different load balancer, replace the dependencies above with the ones you need. +Based on the defined dependencies, Stork automatically registers providers for service discovery mechanisms and load balancers. + +=== The config + +The last piece of the puzzle is the actual service configuration. +If your Consul instance is running on `localhost` on port `8500`, service discovery configuration should look as follows: + +[source,properties] +---- +quarkus.stork.hello-service.service-discovery.type=consul +quarkus.stork.hello-service.service-discovery.consul-host=localhost +quarkus.stork.hello-service.service-discovery.consul-port=8500 +---- + +[NOTE] +==== + +When no load-balancing is configured, Stork uses a round-robin. + +==== diff --git a/content/docs/smallrye-stork/main/service-discovery/composite.adoc b/content/docs/smallrye-stork/main/service-discovery/composite.adoc new file mode 100644 index 000000000..7be362c60 --- /dev/null +++ b/content/docs/smallrye-stork/main/service-discovery/composite.adoc @@ -0,0 +1,71 @@ += Composite Service Discovery +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Some organizations use hybrid infrastructure. In such an infrastructure, different service instances may be discoverable via different service discovery providers. E.g. when _serviceA_ is being migrated from a VM to Kubernetes, its older versions can be discovered via Consul and newer via Kubernetes. + +`composite` service discovery addresses this problem by letting you define a service that consists of multiple services. + +== Dependency + +To use the `composite` service discovery, first add the appropriate Service Discovery provider dependency to your project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-service-discovery-composite + {project-version} + +---- + +== Configuration + +For each service that should consist of multiple services, configure the service discovery `type`, and set the `services` property to a comma separated list of services: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-discovery.type=composite +stork.my-service.service-discovery.services=serviceA,serviceB + +stork.serviceA.service-discovery.type=... +stork.serviceB.service-discovery.type=... +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.type=composite +quarkus.stork.my-service.service-discovery.services=serviceA,serviceB + +quarkus.stork.serviceA.service-discovery.type=... +quarkus.stork.serviceB.service-discovery.type=... +---- + +-- + +==== + +Remember to define the services that make up your composite service. + +Be aware that Stork doesn't work as a standalone service discovery cluster. +Instead, it processes composite configurations—meaning it handles multiple configurations that may use different service discovery implementation. +For each specific configuration, Stork delegates the service discovery task to the appropriate service discovery provider. +If one of these providers doesn't respond or fails, it affects Stork's ability to resolve that specific configuration, but it doesn't mean Stork itself is faulty; +it relies on the performance of the service discovery systems it’s configured to work with. + +These are all the parameters of the composite service discovery: + +include::{generated-dir}/composite-sd-attributes.adoc[] diff --git a/content/docs/smallrye-stork/main/service-discovery/consul.adoc b/content/docs/smallrye-stork/main/service-discovery/consul.adoc new file mode 100644 index 000000000..210076247 --- /dev/null +++ b/content/docs/smallrye-stork/main/service-discovery/consul.adoc @@ -0,0 +1,167 @@ += Consul Service Discovery +:version-root: ../ +include::{version-root}_attributes.adoc[] + +https://www.consul.io/[Consul] is a distributed, highly available, and data center aware solution to connect and configure applications across dynamic, distributed infrastructure. +It's often used as service discovery backend to register and locate the services composing your system. +Consul makes it simple for services to register themselves and to discover other services via a DNS or HTTP interface. +External services can be registered as well. + +As https://developer.hashicorp.com/consul/api-docs/agent/service#address[specified] in the Consul documentation, if the host address is not provided, Stork will automatically use the Consul node address for the instance. + +This page explains how Stork can use Consul to handle the service discovery and service registration. + +== Dependency + +First, you need to add the Stork Consul Service Discovery provider: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-service-discovery-consul + {project-version} + +---- + +=== Service discovery configuration + +For each service that should get the service instances from Consul, configure the service discovery `type`: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-discovery.type=consul +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.type=consul +---- + +-- + +==== + +== Secure communication with Consul + +Stork supports SSL/TLS encryption when connecting to a Consul server. This is useful when Consul is configured with HTTPS or when you need to authenticate using client certificates. + +To enable SSL/TLS, set the `ssl` attribute to `true` and configure the trust store and optionally the key store: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-discovery.type=consul +stork.my-service.service-discovery.consul-host=consul.example.com +stork.my-service.service-discovery.consul-port=8501 +stork.my-service.service-discovery.ssl=true +stork.my-service.service-discovery.trust-store-path=/path/to/truststore.jks +stork.my-service.service-discovery.trust-store-password=changeit +stork.my-service.service-discovery.verify-host=true +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.type=consul +quarkus.stork.my-service.service-discovery.consul-host=consul.example.com +quarkus.stork.my-service.service-discovery.consul-port=8501 +quarkus.stork.my-service.service-discovery.ssl=true +quarkus.stork.my-service.service-discovery.trust-store-path=/path/to/truststore.jks +quarkus.stork.my-service.service-discovery.trust-store-password=changeit +quarkus.stork.my-service.service-discovery.verify-host=true +---- + +-- + +==== + +For mutual TLS (mTLS) authentication, also configure the key store containing the client certificate: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-discovery.key-store-path=/path/to/keystore.jks +stork.my-service.service-discovery.key-store-password=changeit +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.key-store-path=/path/to/keystore.jks +quarkus.stork.my-service.service-discovery.key-store-password=changeit +---- + +-- + +==== + +== ACL Token Authentication + +If your Consul cluster has ACL enabled, you can provide an ACL token for authentication: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-discovery.acl-token=your-consul-acl-token +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.acl-token=your-consul-acl-token +---- + +-- + +==== + +Consul service discovery is configured with the following parameters: + +include::{generated-dir}/consul-sd-attributes.adoc[] diff --git a/content/docs/smallrye-stork/main/service-discovery/custom-service-discovery.adoc b/content/docs/smallrye-stork/main/service-discovery/custom-service-discovery.adoc new file mode 100644 index 000000000..04945b53b --- /dev/null +++ b/content/docs/smallrye-stork/main/service-discovery/custom-service-discovery.adoc @@ -0,0 +1,202 @@ += Implement your own service discovery mechanism +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Stork is extensible, and you can implement your own service discovery mechanism. + +== Dependencies + +To implement your _Service Discovery Provider_, make sure your project depends on Core and Configuration Generator. +The former brings classes necessary to implement custom discovery, the latter contains an https://docs.oracle.com/en/java/javase/11/docs/api/java.compiler/javax/annotation/processing/Processor.html[annotation processor] that generates classes needed by Stork. + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-core + {project-version} + + + io.smallrye.stork + stork-configuration-generator + provided + + {project-version} + +---- + +== Implementing a service discovery provider + +Service discovery implementation consists of three elements: + +* `ServiceDiscovery` which is responsible for locating service instances for a single Stork service. +* `ServiceDiscoveryProvider` which creates instances of `ServiceDiscovery` for a given service discovery _type_. +* `$typeConfiguration` which is a configuration for the discovery. This class is automatically generated during the compilation (using an annotation processor). + +A _type_, for example, `acme`, identifies each provider. +This _type_ is used in the configuration to reference the provider: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-discovery.type=acme +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.type=acme +---- + +-- + +==== + +A `ServiceDiscoveryProvider` implementation needs to be annotated with `@ServiceDiscoveryType` that defines the _type_. +Any configuration properties that the provider expects should be defined with `@ServiceDiscoveryAttribute` annotations placed on the provider. +Optionally, you can also add `@ApplicationScoped` annotation in order to provide the service discovery implementation as CDI bean. + +A service discovery provider class should look as follows: + +[source,java] +---- +include::{examples-dir}/examples/AcmeServiceDiscoveryProvider.java[indent=0] +---- + +Note, that the `ServiceDiscoveryProvider` interface takes a configuration class as a parameter. This configuration class +is generated automatically by the _Configuration Generator_. +Its name is created by appending `Configuration` to the service discovery type, such as `AcmeConfiguration`. + +The next step is to implement the `ServiceDiscovery` interface: + +[source,java] +---- +include::{examples-dir}/examples/AcmeServiceDiscovery.java[indent=0] +---- + +This implementation is simplistic. +Typically, instead of creating a service instance with values from the configuration, you would connect to a service discovery backend, look for the service and build the list of service instance accordingly. +That's why the method returns a `Uni`. +Most of the time, the lookup is a remote operation. + +As you can see, the `AcmeConfiguration` class gives access to the configuration attribute. + +== Using your service discovery + +In the project using it, don't forget to add the dependency on the module providing your implementation. +Then, in the configuration, just add: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-discovery.type=acme +stork.my-service.service-discovery.host=localhost +stork.my-service.service-discovery.port=1234 +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.type=acme +quarkus.stork.my-service.service-discovery.host=localhost +quarkus.stork.my-service.service-discovery.port=1234 +---- + +-- + +==== + +Then, Stork will use your implementation to locate the `my-service` service. + +== Using your service discovery using the programmatic API + +When building your service discovery project, the configuration generator creates a configuration class. +This class can be used to configure your service discovery using the Stork programmatic API. + +[source,java] +---- +include::{examples-dir}/examples/AcmeDiscoveryApiUsage.java[indent=0] +---- + +Remember that attributes, like `host`, are declared using the `@ServiceDiscoveryAttribute` annotation on the `ServiceDiscoveryProvider` implementation. + +== Caching the service instances + +Your `ServiceDiscovery` implementation can extend `io.smallrye.stork.impl.CachingServiceDiscovery` to automatically _cache_ the service instances. +In this case, the retrieved set of `ServiceInstance` is cached and only updated after some time. +This duration is an additional configuration attribute. +For homogeneity, we recommend the following attribute: + +[source,java] +---- +@ServiceDiscoveryAttribute(name = "refresh-period", description = "Service discovery cache refresh period.", + defaultValue = CachingServiceDiscovery.DEFAULT_REFRESH_INTERVAL) +---- + +The following snippet extends the _acme_ service discovery with the `refresh-period` attribute: + +[source,java] +---- +include::{examples-dir}/examples/CachedAcmeServiceDiscoveryProvider.java[indent=0] +---- + +Extending `io.smallrye.stork.impl.CachingServiceDiscovery` changes the structure of the service discovery implementation: + +[source,java] +---- +include::{examples-dir}/examples/CachedAcmeServiceDiscovery.java[indent=0] +---- + +[arabic] +. Call the `super` constructor with the `refresh-period` value +. Implement `fetchNewServiceInstances` instead of `getServiceInstances`. +The method is called periodically, and the retrieved instances are cached. +This implementation is simplistic. + +If the retrieval fails, the error is reported, and Stork keeps the previously retrieved list of instances. + +=== Customizing the caching strategy + +Sometimes it can be useful to change this behaviour and customize the cache expiration strategy. + +For example, imagine you are using a backend service discovery where service instances can change very frequently. + +Moreover, contacting the backend service discovery can be expensive in terms of computing, +thus finding a good value for the refreshing time can be mission impossible. + +For these situations, Stork allows to implement a better expiration strategy for the cache. + +If you want to customize the expiration strategy, you need: +1. Implement the `cache` method where the expiration strategy should be defined. +2. Invalidate the cache when expiration condition evaluates to true. + +Look at the example bellow: + +[source,java] +---- +include::{examples-dir}/examples/CustomExpirationCachedAcmeServiceDiscovery.java[indent=0] +---- + +Additionally, you can check the xref:service-discovery/kubernetes.adoc#caching-the-service-instances[Kubernetes Service Discovery] for further details about an event-based invalidation example. diff --git a/content/docs/smallrye-stork/main/service-discovery/dns.adoc b/content/docs/smallrye-stork/main/service-discovery/dns.adoc new file mode 100644 index 000000000..7e32934bc --- /dev/null +++ b/content/docs/smallrye-stork/main/service-discovery/dns.adoc @@ -0,0 +1,76 @@ += DNS Service Discovery +:version-root: ../ +include::{version-root}_attributes.adoc[] + +DNS is a name resolution protocol used to determine IP addresses for hostnames. +That makes it a natural fit for service discovery. +Consul and AWS Cloud Map provide DNS resolutions for service discovery. + +This page explains how Stork can use DNS to handle the service discovery. + +== DNS records + +DNS supports a https://en.wikipedia.org/wiki/List_of_DNS_record_types[variety of record types]. Stork can resolve hostnames to addresses based on https://en.wikipedia.org/wiki/SRV_record[_SRV_], A and AAAA records. All these types of records may return multiple addresses for a single hostname. You may choose to not resolve target hostnames. + +While _A_ and _AAAA_ records are quite similar, they just carry an IP (_v4_ for _A_ and _v6_ for _AAAA_), the _SRV_ records are different. +They contain a _weight_, a _target_ and a _port_ for a service instance. +The _target_ returned in an _SRV_ record needs to be resolved further by an _A_ or an _AAAA_ record. + +In short, it works as follows: + +image::images/diagrams/plantuml-bc915c97a9fa.svg[srv_sequence] + +== Dependency + +To use the DNS service discovery, you need to add the Stork DNS Service Discovery provider dependency to your project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-service-discovery-dns + {project-version} + +---- + +== Configuration + +Next, set the service discovery `type` to `dns`. +Additionally, you would usually specify the DNS server, or servers, to use for the discovery. +All in all, your configuration could look as follows: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-discovery.type=dns +# optional dns servers: +stork.my-service.service-discovery.dns-servers=my-dns-server:8221,my-dns-server2 +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.type=dns + +# optional dns servers: +quarkus.stork.my-service.service-discovery.dns-servers=my-dns-server:8221,my-dns-server2 +---- + +-- + +==== + +All the available parameters are as follows: + +include::{generated-dir}/dns-sd-attributes.adoc[] diff --git a/content/docs/smallrye-stork/main/service-discovery/eureka.adoc b/content/docs/smallrye-stork/main/service-discovery/eureka.adoc new file mode 100644 index 000000000..87b5c9208 --- /dev/null +++ b/content/docs/smallrye-stork/main/service-discovery/eureka.adoc @@ -0,0 +1,71 @@ += Eureka Service Discovery +:version-root: ../ +include::{version-root}_attributes.adoc[] + +https://github.com/Netflix/eureka[Eureka] is a RESTful service that is primarily used in the AWS cloud for the purpose of discovery, load balancing, and failover of middle-tier servers. + +This page explains how Stork can use Eureka to handle the service discovery and service registration. + +== Dependency + +First, you need to add the Stork Eureka Service Discovery provider: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-service-discovery-eureka + {project-version} + +---- + +=== Service discovery configuration + +For each application instance expected to be registered in Eureka, configure the lookup: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-discovery.type=eureka +stork.my-service.service-discovery.eureka-host=localhost +stork.my-service.service-discovery.eureka-port=8761 +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.type=eureka +quarkus.stork.my-service.service-discovery.eureka-host=localhost +quarkus.stork.my-service.service-discovery.eureka-port=8761 +---- + +-- + +==== + +Stork looks for the service with the given name (`my-service` in the previous example). + +Supported attributes are the following: + +include::{generated-dir}/eureka-sd-attributes.adoc[] + +The `application` attribute is optional. +It uses the Stork service name (`my-service` in the previous configuration) if not set. + +The `instance` attribute allows selecting a specific instance. +Using this attribute prevents load-balancing as you will always select a single instance. + +The `secure` attribute indicates whether to connect to instances over TLS. +If set to `true`, instances without a secure port enabled are filtered out. +The connection host is always resolved from the `hostName` field of the Eureka instance (falling back to `ipAddr`), regardless of this setting. diff --git a/content/docs/smallrye-stork/main/service-discovery/knative.adoc b/content/docs/smallrye-stork/main/service-discovery/knative.adoc new file mode 100644 index 000000000..539fcdb5b --- /dev/null +++ b/content/docs/smallrye-stork/main/service-discovery/knative.adoc @@ -0,0 +1,129 @@ += Knative Service Discovery +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Knative is a Kubernetes-based platform for serverless workloads. Knative provides a set of objects as Kubernetes Custom Resource Definitions (CRDs). +These resources are used to define and control how your serverless workload behaves on the cluster. +The Stork Knative service discovery implementation is very similar to the Kubernetes one. +Stork will ask for https://knative.dev/docs/serving/reference/serving-api/#serving.knative.dev/v1.Service[Knative services] to the cluster instead of vanilla https://kubernetes.io/docs/concepts/services-networking/service/#service-resource[Kubernetes services] used by the Kubernetes implementation. +To do so, Stork uses https://github.com/fabric8io/kubernetes-client/blob/master/extensions/knative/client/src/main/java/io/fabric8/knative/client/KnativeClient.java[Fabric 8 Knative Client] which is just an extension of Fabric8 Kubernetes Client. + +== Dependency + +First, you need to add the Stork Knative Service Discovery provider: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-service-discovery-knative + {project-version} + +---- + +=== A few words about server authentication. + +Stork uses Fabric8 Knative Client which is a https://github.com/fabric8io/kubernetes-client#readme[Fabric8 Kubernetes Client] extension to access the Kubernetes resources, concretely the `DefaultKnativeClient` implementation. + +Since Knative Client is just an extension of Fabric8 Kubernetes Client, it’s also possible to get an instance of KnativeClient from KubernetesClient. + +`DefaultKubernetesClient` will try to read the `~/.kube/config` file from your local machine and load the token for authenticating with the Kubernetes API server. + +The level of access (Roles) depends on the configured `ServiceAccount`. + +You can override this configuration if you want fine-grain control. + +==== Role-based access control (RBAC) + +If you're using a Kubernetes cluster with Role-Based Access Control (RBAC) enabled, the default permissions for a ServiceAccount don't allow it to list or modify any resources. +A `ServiceAccount`, a `Role` and a `RoleBinding` are needed in order to allow Stork to list the available service instances from the cluster or the namespace. + +An example that allows listing all endpoints could look something like this: + +[source,yaml] +---- +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: + namespace: +--- +kind: Role +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: + namespace: +rules: + - apiGroups: [""] # "" indicates the core API group + resources: ["endpoints", "pods"] # stork queries service endpoints and pods + verbs: ["get", "list"] +--- +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: RoleBinding +metadata: + name: + namespace: +subjects: + - kind: ServiceAccount + # Reference to upper's `metadata.name` + name: + # Reference to upper's `metadata.namespace` + namespace: +roleRef: + kind: Role + name: + apiGroup: rbac.authorization.k8s.io +---- + +== Configuration + +For each service expected to be exposed as _Kubernetes Service_, configure the lookup: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-knservice.service-discovery.type=knative +stork.my-knservice.service-discovery.knative-namespace=my-namespace +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-knservice.service-discovery.type=knative +quarkus.stork.my-knservice.service-discovery.knative-namespace=my-namespace +---- + +-- + +==== + +Stork looks for the _Knative Service_ with the given name (`my-knservice` in the previous example) in the specified namespace. + +Stork inspects the _Knative Service_ and retrieves the url of the service. + +Supported attributes are the following: + +include::{generated-dir}/knative-sd-attributes.adoc[] + +== Caching the service instances + +Contacting the cluster too much frequently can result in performance problems. It's why Knative Service discovery extends `io.smallrye.stork.impl.CachingServiceDiscovery` to automatically _cache_ the service instances. +Moreover, the caching expiration has been also improved in order to only update the retrieved set of `ServiceInstance` if some of them changes and an event is emitted. +This is done by creating an https://www.javadoc.io/static/io.fabric8/kubernetes-client-api/6.1.1/io/fabric8/kubernetes/client/informers/SharedIndexInformer.html[Informer], similar to a https://www.javadoc.io/static/io.fabric8/kubernetes-client-api/6.1.1/io/fabric8/kubernetes/client/Watch.html[Watch], able to observe the events on the Knative Service instances resources. + +Note that: +- the cache is invalidated when an event is received. +- the cache is validated once the instances are retrieved from the cluster, in the `fetchNewServiceInstances` method. +- the `cache` method is overrided to customize the expiration strategy. In this case the collection of service instances will be kept until an event occurs. diff --git a/content/docs/smallrye-stork/main/service-discovery/kubernetes.adoc b/content/docs/smallrye-stork/main/service-discovery/kubernetes.adoc new file mode 100644 index 000000000..1bc26e0a4 --- /dev/null +++ b/content/docs/smallrye-stork/main/service-discovery/kubernetes.adoc @@ -0,0 +1,335 @@ += Kubernetes Service Discovery +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Kubernetes has a built-in support for service discovery and load-balancing. +However, you may need more flexibility to carefully select the service instance you want. + +This page explains how Stork can use the Kubernetes API to handle the service discovery. + +== Dependency + +First, you need to add the Stork Kubernetes Service Discovery provider: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-service-discovery-kubernetes + {project-version} + +---- + +=== A few words about server authentication. + +Stork uses https://github.com/fabric8io/kubernetes-client#readme[Fabric8 Kubernetes Client] to access the Kubernetes resources, concretely the `DefaultKubernetesClient` implementation. + +It will try to read the `~/.kube/config` file from your local machine and load the token for authenticating with the Kubernetes API server. + +If you are using the Stork Kubernetes discovery provider from inside a _Pod_, it loads `~/.kube/config` from the container file system. + +This file is automatically mounted inside the Pod. + +The level of access (Roles) depends on the configured `ServiceAccount`. + +You can override this configuration if you want fine-grain control. + +==== Role-based access control (RBAC) + +If you're using a Kubernetes cluster with Role-Based Access Control (RBAC) enabled, the default permissions for a ServiceAccount don't allow it to list or modify any resources. +A `ServiceAccount`, a `Role` and a `RoleBinding` are needed in order to allow Stork to list the available service instances from the cluster or the namespace. + +An example that allows listing all endpoints could look something like this: + +[source,yaml] +---- +------ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: + namespace: +--- +kind: Role +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: + namespace: +rules: + - apiGroups: [""] # "" indicates the core API group + resources: ["endpoints", "pods", "services"] # stork queries service endpoints, pods and services + verbs: ["get", "list"] +--- +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: RoleBinding +metadata: + name: + namespace: +subjects: + - kind: ServiceAccount + # Reference to upper's `metadata.name` + name: + # Reference to upper's `metadata.namespace` + namespace: +roleRef: + kind: Role + name: + apiGroup: rbac.authorization.k8s.io +---- + +== Configuration + +For each service expected to be exposed as _Kubernetes Service_, configure the lookup: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-discovery.type=kubernetes +stork.my-service.service-discovery.k8s-namespace=my-namespace +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.type=kubernetes +quarkus.stork.my-service.service-discovery.k8s-namespace=my-namespace +---- + +-- + +==== + +Stork looks for the _Kubernetes Service_ with the given name (`my-service` in the previous example) in the specified namespace. + +Instead of using the _Kubernetes Service_ IP directly, and let Kubernetes handle the selection and balancing, Stork inspects the service and retrieves the list of _pods_ providing the service. +Then, it can select the instance. + +== EndpointSlice support (Experimental) + +This feature is experimental. It is opt-in, and the API or behavior may change based on user feedback and Kubernetes evolution. +Recent Kubernetes versions expose service endpoints using *EndpointSlices* (`discovery.k8s.io/v1`), +which are the recommended and scalable replacement for classic `Endpoints`. + +Stork supports EndpointSlices and can use them transparently for service discovery. + +=== Why EndpointSlices + +Compared to classic `Endpoints`, EndpointSlices: + +* Scale better for services with many pods. +* Reduce load on the Kubernetes API server. +* Produce smaller and more frequent updates. +* Are the preferred API in modern Kubernetes clusters. + +When available, EndpointSlices are the recommended source for service discovery. + +== Configuration + +EndpointSlices can be enabled or disabled explicitly. By default, Stork uses automatic detection. + +=== Explicit configuration + +You can force the use of EndpointSlices per service (note that it will still fallback to Endpoints if EndpointSlices are not available for the service): + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.use-endpoint-slices=true +---- + +Or explicitly disable them: + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.use-endpoint-slices=false +---- + +=== Automatic detection (default behavior) + +If `use-endpoint-slices` is not configured, Stork applies the following logic: + +[arabic] +. If the Kubernetes cluster exposes the `discovery.k8s.io/v1` API *and* +. EndpointSlices exist for the target service + +→ Stork uses EndpointSlices. + +Otherwise, it falls back to classic Endpoints. + +This ensures backward compatibility with older clusters and services while automatically benefiting from EndpointSlices when available. + +The detection is performed *asynchronously* (off the event loop) and its result is cached for the lifetime of the service discovery instance. +If the cluster API is temporarily unreachable during detection, Stork retries a few times before falling back to classic Endpoints. +This means a transient connectivity issue at startup does not permanently disable EndpointSlice discovery, but it may cause Stork to fall back to Endpoints for the lifetime of that instance. + +Falling back to classic Endpoints is safe even on modern clusters: Kubernetes maintains `Endpoints` objects alongside `EndpointSlices` for backward compatibility, so both APIs reflect the same service instances. + +== Service instance resolution with EndpointSlices + +When using EndpointSlices, Stork does *not* resolve Pods. + +Instead, service instances are derived directly from the EndpointSlice objects: + +* `endpoint.addresses` → instance host +* `endpointSlice.ports` → instance port +* `endpointSlice.labels` → instance metadata + +Each EndpointSlice represents a homogeneous group of endpoints. +All addresses in a slice share the same set of ports. +If different port combinations exist, Kubernetes exposes them as separate EndpointSlices. + +This guarantees that all `(address, port)` combinations returned by Stork are valid. + +== ClusterIP mode + +By default, Stork resolves individual *pod IPs* from Kubernetes Endpoints (or EndpointSlices). +In ClusterIP mode, Stork resolves to the *ClusterIP and port* of the Kubernetes Service instead, without inspecting the backing pods. + +The `use-cluster-ip` attribute enables this mode. + +=== When to use ClusterIP mode + +ClusterIP mode is useful when: + +* You have a *multi-port Service* and want Stork to resolve the port by name, without needing to know actual port numbers. +* You don't need client-side load balancing — for example, because `kube-proxy`, a service mesh, or another network-level mechanism already handles traffic distribution. +* You want to *control the resolution strategy through configuration*, without changing application code. + +=== Configuration + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-discovery.type=kubernetes +stork.my-service.service-discovery.k8s-namespace=my-namespace +stork.my-service.service-discovery.use-cluster-ip=true +stork.my-service.service-discovery.port-name=http +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.type=kubernetes +quarkus.stork.my-service.service-discovery.k8s-namespace=my-namespace +quarkus.stork.my-service.service-discovery.use-cluster-ip=true +quarkus.stork.my-service.service-discovery.port-name=http +---- + +-- + +==== + +In this mode, Stork queries the Kubernetes `Service` resource directly and returns a single `ServiceInstance` with: + +* *Host* — the Service's ClusterIP (e.g., `10.96.0.15`) +* *Port* — resolved from the Service spec, matching `port-name` when multiple ports are defined + +Since only one instance is returned, any configured Stork load balancer becomes a passthrough. + +[NOTE] +==== + +ClusterIP mode does not support headless Services (`clusterIP: None`). +If the target Service is headless, Stork returns an empty list of service instances. + +==== + +=== Multi-port Services + +ClusterIP mode works well with multi-port Services. +Use the `port-name` attribute to select the desired port: + +[source,yaml] +---- +apiVersion: v1 +kind: Service +metadata: + name: my-service +spec: + ports: + - name: http + port: 8080 + - name: grpc + port: 50051 +---- + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-discovery.type=kubernetes +stork.my-service.service-discovery.use-cluster-ip=true +stork.my-service.service-discovery.port-name=grpc +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.type=kubernetes +quarkus.stork.my-service.service-discovery.use-cluster-ip=true +quarkus.stork.my-service.service-discovery.port-name=grpc +---- + +-- + +==== + +This resolves to `10.96.x.x:50051` — the ClusterIP with the `grpc` port without the application needing to know the actual port number. +If `port-name` is not set, Stork uses the first port defined in the Service spec. + +Supported attributes are the following: + +include::{generated-dir}/kubernetes-sd-attributes.adoc[] + +== Caching the service instances + +Contacting the cluster too much frequently can result in performance problems. It's why Kubernetes Service discovery extends `io.smallrye.stork.impl.CachingServiceDiscovery` to automatically _cache_ the service instances. +Moreover, the caching expiration has been also improved in order to only update the retrieved set of `ServiceInstance` if some of them changes and an event is emitted. +This is done by creating an https://www.javadoc.io/static/io.fabric8/kubernetes-client-api/6.1.1/io/fabric8/kubernetes/client/informers/SharedIndexInformer.html[Informer], similar to a https://www.javadoc.io/static/io.fabric8/kubernetes-client-api/6.1.1/io/fabric8/kubernetes/client/Watch.html[Watch], able to observe the events on the service instances resources. + +Note that: +- the cache is invalidated when an event is received. +- the cache is validated, if and only if, the instances are retrieved successfully from the cluster, in the `fetchNewServiceInstances` method. +- In case of an error contacting the cluster (e.g., network failure, API server unreachable), a retry mechanism is triggered. +The system will attempt to contact the cluster up to the number of times specified by request-retry-backoff-limit, waiting request-retry-backoff-interval milliseconds between each attempt. +By default, retries are disabled to prevent the system from entering an infinite loop of calls to an unresponsive cluster. +- If no service instances are found (e.g., no matching endpoints, headless service in ClusterIP mode), Stork returns an empty list. +- the `cache` method is overridden to customize the expiration strategy. In this case the collection of service instances will be kept until an event occurs. + +When EndpointSlice are selected, Stork creates informers on the `EndpointSlice` resources instead of classic `Endpoints`. +The caching behavior remains unchanged. + +In ClusterIP mode, Stork creates an Informer on the Kubernetes `Service` resource (instead of Endpoints or EndpointSlices). +The cache is invalidated when the Service is added, updated, or deleted. +The same caching behavior described above applies. diff --git a/content/docs/smallrye-stork/main/service-discovery/overview.adoc b/content/docs/smallrye-stork/main/service-discovery/overview.adoc new file mode 100644 index 000000000..42531f0d8 --- /dev/null +++ b/content/docs/smallrye-stork/main/service-discovery/overview.adoc @@ -0,0 +1,34 @@ += Overview +:version-root: ../ +include::{version-root}_attributes.adoc[] + +== Service Discovery in SmallRye Stork + +As already introduced, service discovery is a crucial part of modern microservices architectures. +It allows services to dynamically discover the location of other services at runtime, which is particularly useful in distributed systems where services may scale up or down, +or change their network addresses. + +SmallRye Stork provides a flexible and extensible mechanism for service discovery. +It supports out of the box some service discovery such as Kubernetes or Consul but the main strength of it is customization so you can easily create your own implementation related on your business for example. +Stork allows services to communicate with each other without requiring hardcoded addresses, making it an ideal solution for microservices deployments. +SmallRye Stork brings this capability to clients for Quarkus applications but it's vendor agnostic so you easily use it with other solutions and even in standalone mode. + +== Service discovery caching overview + +Stork provides a built-in caching mechanism for service discovery results. +By default, each service discovery implementation may repeatedly contact its backend (e.g., Consul, Kubernetes, Eureka) to retrieve the current list of service instances. +Since these lookups can be expensive or frequent, Stork allows service discovery providers to cache the retrieved instances for a configurable period of time. + +* Cached results are reused for subsequent lookups, avoiding unnecessary backend calls. +* The cache is automatically refreshed after the configured `refresh-period`. +* If a refresh fails, Stork keeps serving the previously retrieved list of instances until a new successful refresh occurs. + +In addition to the time-based refresh mechanism, Stork also exposes an *cache invalidation API* . +This API can be used to explicitly clear the cached results, forcing the next lookup to perform a new service discovery call. +This is especially useful when the service discovery backend supports event-based notifications (for example, Consul blocking queries), allowing the cache to be invalidated as soon as a change is detected. + +____ +For more details about how to enable caching in a custom service discovery implementation, see the xref:service-discovery/custom-service-discovery.adoc#caching-the-service-instances[Caching the service instances] section. +____ + +You can explore the different implementations and learn how to create your own in the following sections. diff --git a/content/docs/smallrye-stork/main/service-discovery/static-list.adoc b/content/docs/smallrye-stork/main/service-discovery/static-list.adoc new file mode 100644 index 000000000..4e2b066f3 --- /dev/null +++ b/content/docs/smallrye-stork/main/service-discovery/static-list.adoc @@ -0,0 +1,56 @@ += Static List Service Discovery +:version-root: ../ +include::{version-root}_attributes.adoc[] + +In some situations, such as demos, development, or testing, you may want to mock the service discovery by providing a predefined list of service instances. +For this purpose, Stork comes with a `static` service discovery type. + +== Dependency + +To use the `static` service discovery, first add the appropriate Service Discovery provider dependency to your project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-service-discovery-static-list + {project-version} + +---- + +== Configuration + +For each service that should use the static list of service instances configure the service registrar `type`: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-discovery.type=static +stork.my-service.service-discovery.address-list=localhost:8080,localhost:8081 +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-discovery.type=static +quarkus.stork.my-service.service-discovery.address-list=localhost:8080,localhost:8081 +---- + +-- + +==== + +These are all the static service discovery parameters: + +include::{generated-dir}/static-sd-attributes.adoc[] diff --git a/content/docs/smallrye-stork/main/service-registration/consul.adoc b/content/docs/smallrye-stork/main/service-registration/consul.adoc new file mode 100644 index 000000000..10fe6d6ea --- /dev/null +++ b/content/docs/smallrye-stork/main/service-registration/consul.adoc @@ -0,0 +1,228 @@ += Consul Service Registration +:version-root: ../ +include::{version-root}_attributes.adoc[] + +https://www.consul.io/[Consul] is a distributed, highly available, and data center aware solution to connect and configure applications across dynamic, distributed infrastructure. +It's often used as service discovery backend to register and locate the services composing your system. +Consul makes it simple for services to register themselves and to discover other services via a DNS or HTTP interface. +External services can be registered as well. + +This page explains how Stork can use Consul to handle the service registration. + +== Dependency + +First, you need to add the Stork Consul Service Registration provider: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-service-registration-consul + {project-version} + +---- + +== Service registration configuration + +For each service that should register the service instances in Consul, configure the service registrar `type`: + +[.tabs] +==== + +stork standalone:: ++ +-- + +-- + +==== + +[source,properties] +---- +stork.my-service.service-registrar.type=consul +---- + +[.tabs] +==== + +stork in quarkus:: ++ +-- + +-- + +==== + +[source,properties] +---- +quarkus.stork.my-service.service-registrar.type=consul +---- + +Consul service registrar is configured with the following parameters: + +include::{generated-dir}/consul-sr-attributes.adoc[] + +== Service deregistration configuration + +There is no specific configuration required to enable deregistration; however, you must ensure that a consul service registrar is configured for the service: + +[.tabs] +==== + +stork standalone:: ++ +-- + +-- + +==== + +[source,properties] +---- +stork.my-service.service-registrar.type=consul +---- + +[.tabs] +==== + +stork in quarkus:: ++ +-- + +-- + +==== + +[source,properties] +---- +quarkus.stork.my-service.service-registrar.type=consul +---- + +As with registration, deregistration relies on the service name. + +== Secure communication with Consul + +When your Consul cluster uses TLS/SSL encryption, you can configure Stork to establish secure connections. Enable SSL and provide the necessary trust store configuration: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-registrar.type=consul +stork.my-service.service-registrar.consul-host=localhost +stork.my-service.service-registrar.consul-port=8501 +stork.my-service.service-registrar.ssl=true +stork.my-service.service-registrar.trust-store-path=/path/to/truststore.jks +stork.my-service.service-registrar.trust-store-password=changeit +stork.my-service.service-registrar.verify-host=true +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-registrar.type=consul +quarkus.stork.my-service.service-registrar.consul-host=localhost +quarkus.stork.my-service.service-registrar.consul-port=8501 +quarkus.stork.my-service.service-registrar.ssl=true +quarkus.stork.my-service.service-registrar.trust-store-path=/path/to/truststore.jks +quarkus.stork.my-service.service-registrar.trust-store-password=changeit +quarkus.stork.my-service.service-registrar.verify-host=true +---- + +-- + +==== + +For mutual TLS (mTLS) authentication where the client must also present a certificate, provide both trust store and key store configuration: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-registrar.type=consul +stork.my-service.service-registrar.consul-host=localhost +stork.my-service.service-registrar.consul-port=8501 +stork.my-service.service-registrar.ssl=true +stork.my-service.service-registrar.trust-store-path=/path/to/truststore.jks +stork.my-service.service-registrar.trust-store-password=changeit +stork.my-service.service-registrar.key-store-path=/path/to/keystore.jks +stork.my-service.service-registrar.key-store-password=changeit +stork.my-service.service-registrar.verify-host=true +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-registrar.type=consul +quarkus.stork.my-service.service-registrar.consul-host=localhost +quarkus.stork.my-service.service-registrar.consul-port=8501 +quarkus.stork.my-service.service-registrar.ssl=true +quarkus.stork.my-service.service-registrar.trust-store-path=/path/to/truststore.jks +quarkus.stork.my-service.service-registrar.trust-store-password=changeit +quarkus.stork.my-service.service-registrar.key-store-path=/path/to/keystore.jks +quarkus.stork.my-service.service-registrar.key-store-password=changeit +quarkus.stork.my-service.service-registrar.verify-host=true +---- + +-- + +==== + +== ACL Token Authentication + +When your Consul cluster has ACL (Access Control List) enabled, you need to provide an ACL token for authentication: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-registrar.type=consul +stork.my-service.service-registrar.consul-host=localhost +stork.my-service.service-registrar.consul-port=8500 +stork.my-service.service-registrar.acl-token=your-acl-token +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-registrar.type=consul +quarkus.stork.my-service.service-registrar.consul-host=localhost +quarkus.stork.my-service.service-registrar.consul-port=8500 +quarkus.stork.my-service.service-registrar.acl-token=your-acl-token +---- + +-- + +==== + +The ACL token can be combined with SSL/TLS configuration for secure, authenticated access to Consul. diff --git a/content/docs/smallrye-stork/main/service-registration/custom-service-registration.adoc b/content/docs/smallrye-stork/main/service-registration/custom-service-registration.adoc new file mode 100644 index 000000000..9e7d8e4cd --- /dev/null +++ b/content/docs/smallrye-stork/main/service-registration/custom-service-registration.adoc @@ -0,0 +1,165 @@ += Implement your own service registration mechanism +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Stork is extensible, and you can implement your own service registrar mechanism. + +== Dependencies + +To implement your _Service Registration Provider_, make sure your project depends on Core and Configuration Generator. +The former brings classes necessary to implement custom registrar, the latter contains an https://docs.oracle.com/en/java/javase/11/docs/api/java.compiler/javax/annotation/processing/Processor.html[annotation processor] that generates classes needed by Stork. + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-core + {project-version} + + + io.smallrye.stork + stork-configuration-generator + provided + + {project-version} + +---- + +== Implementing a service registrar provider + +Service registration implementation consists of three elements: + +* `ServiceRegistrar` which is responsible for registering service instances for a single Stork service. +* `ServiceRegistrarProvider` which creates instances of `ServiceRegistrar` for a given service registrar _type_. +* `$typeConfiguration` which is a configuration for the registrar. This class is automatically generated during the compilation (using an annotation processor). + +A _type_, for example, `acme`, identifies each provider. +This _type_ is used in the configuration to reference the provider: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-registrar.type=acme +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-registrar.type=acme +---- + +-- + +==== + +A `ServiceRegistrarProvider` implementation needs to be annotated with `@ServiceRegistrarType` that defines the _type_. +Any configuration properties that the provider expects should be defined with `@ServiceRegistrarAttribute` annotations placed on the provider. +Optionally, you can also add `@ApplicationScoped` annotation in order to provide the service registrar implementation as CDI bean. + +A service registrar provider class should look as follows: + +[source,java] +---- +include::{examples-dir}/examples/AcmeServiceRegistrarProvider.java[indent=0] +---- + +Note, that the `ServiceRegistrarProvider` interface takes a configuration class as a parameter. This configuration class +is generated automatically by the _Configuration Generator_. +Its name is created by appending `RegistrarConfiguration` to the service registrar type, such as `AcmeRegistrarConfiguration`. + +The next step is to implement the `ServiceRegistrar` interface: + +[source,java] +---- +include::{examples-dir}/examples/AcmeServiceRegistrar.java[indent=0] +---- + +This implementation is simplistic. +Typically, instead of creating a service instance with values from the configuration, you would connect to a service registration backend, look for the service and build the list of service instance accordingly. +That's why the method returns a `Uni`. +Most of the time, the lookup is a remote operation. + +As you can see, the `AcmeConfiguration` class gives access to the configuration attribute. + +=== Implement your own service deregistration mechanism + +The `registerServiceInstance` method receives an optional `instanceName` parameter that acts as an explicit identifier for the registered instance. +When provided, it should be used as the registration ID so that the same instance can be precisely targeted during deregistration. +If `instanceName` is `null` or empty, implementations should fall back to a deterministic ID derived from the service name, IP address, and port. + +Finally, you can implement your own service deregistration mechanism in `deregisterServiceInstance` method. + +[WARNING] +.Override the specific-instance deregistration methods +==== + +The `ServiceRegistrar` interface provides several deregistration methods: + +* `deregisterServiceInstance(String serviceName)` — deregisters *all* instances of a service. +* `deregisterServiceInstance(String serviceName, String instanceName)` — deregisters the instance registered under the given name. +* `deregisterServiceInstance(String serviceName, String ipAddress, int port)` — deregisters the instance identified by IP and port. + +The default implementations of the second and third methods fall back to the first, which may deregister *all* instances. +*Custom registrar implementations should override at least one of the specific-instance methods* to provide precise, instance-level deregistration and avoid unintended side effects. + +Note that the `Service` class wraps these with simpler methods that do not require passing the service name: +`deregisterNamedInstance(instanceName)`, `deregisterServiceInstance(ipAddress, port)`, and the deprecated no-arg `deregisterServiceInstance()`. + +==== + +== Using your service registrar using the programmatic API + +In the project using it, don't forget to add the dependency on the module providing your implementation. +Then, in the configuration, just add: + +[.tabs] +==== + +stork standalone:: ++ +-- + +[source,properties] +---- +stork.my-service.service-registrar.type=acme +stork.my-service.service-registrar.host=localhost +stork.my-service.service-registrar.port=1234 +---- + +-- + +stork in quarkus:: ++ +-- + +[source,properties] +---- +quarkus.stork.my-service.service-registrar.type=acme +quarkus.stork.my-service.service-registrar.host=localhost +quarkus.stork.my-service.service-registrar.port=1234 +---- + +-- + +==== + +When building your service registrar project project, the configuration generator creates a configuration class. +Then, Stork will use your implementation to register/deregister the service instances using the `acme` backend. + +This class can be used to configure your service registrar using the Stork programmatic API. + +[source,java] +---- +include::{examples-dir}/examples/AcmeRegistrarApiUsage.java[indent=0] +---- diff --git a/content/docs/smallrye-stork/main/service-registration/eureka.adoc b/content/docs/smallrye-stork/main/service-registration/eureka.adoc new file mode 100644 index 000000000..8e327934d --- /dev/null +++ b/content/docs/smallrye-stork/main/service-registration/eureka.adoc @@ -0,0 +1,105 @@ += Eureka Service Registration +:version-root: ../ +include::{version-root}_attributes.adoc[] + +https://github.com/Netflix/eureka[Eureka] is a RESTful service that is primarily used in the AWS cloud for the purpose of discovery, load balancing, and failover of middle-tier servers. + +This page explains how Stork can use Eureka to handle the service registration. + +== Dependency + +First, you need to add the Stork Eureka Service Registration provider: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-service-registration-eureka + {project-version} + +---- + +== Service registration configuration + +For each service that should register the service instances in Eureka, configure the service registrar `type`: + +[.tabs] +==== + +stork standalone:: ++ +-- + +-- + +==== + +[source,properties] +---- +stork.my-service.service-registrar.type=eureka +stork.my-service.service-registrar.eureka-host=localhost +stork.my-service.service-registrar.eureka-port=8761 +---- + +[.tabs] +==== + +stork in quarkus:: ++ +-- + +-- + +==== + +[source,properties] +---- +quarkus.stork.my-service.service-registrar.type=eureka +quarkus.stork.my-service.service-registrar.eureka-host=localhost +quarkus.stork.my-service.service-registrar.eureka-port=8761 +---- + +Eureka service registrar is configured with the following parameters: + +include::{generated-dir}/eureka-sr-attributes.adoc[] + +The `ipAddress` passed to `registerServiceInstance()` is used as both the IP address (`ipAddr`) and the hostname (`hostName`) registered in Eureka. +If the instance has a distinct DNS hostname, set `host-name` explicitly to override this default. + +== Service deregistration configuration + +There is no specific configuration required to enable deregistration; however, you must ensure that a consul service registrar is configured for the service: + +[.tabs] +==== + +stork standalone:: ++ +-- + +-- + +==== + +[source,properties] +---- +stork.my-service.service-registrar.type=eureka +---- + +[.tabs] +==== + +stork in quarkus:: ++ +-- + +-- + +==== + +[source,properties] +---- +quarkus.stork.my-service.service-registrar.type=eureka +---- + +As with registration, deregistration relies on the service name. diff --git a/content/docs/smallrye-stork/main/service-registration/overview.adoc b/content/docs/smallrye-stork/main/service-registration/overview.adoc new file mode 100644 index 000000000..db10b9a5f --- /dev/null +++ b/content/docs/smallrye-stork/main/service-registration/overview.adoc @@ -0,0 +1,82 @@ += Overview +:version-root: ../ +include::{version-root}_attributes.adoc[] + +== Service Registration in SmallRye Stork + +Service registration is the process by which services announce their availability to a central registry, allowing other services to discover and communicate with them. +In SmallRye Stork, service registration is automated and integrated with supported registries like *Consul*. +This ensures that services can dynamically join and leave the network. + +=== Key Features: + +* *Automatic Registration*: For *Quarkus applications*, SmallRye Stork automatically registers the application with the configured service registry — but *only for built-in registrars such as Consul, Eureka, and Static*. +If you're using a custom registrar, registration must still be handled programmatically, as before this feature. + +Registration is enabled by default. If you want to disable registration, you can do it per service using the following property: + +[.tabs] +==== + +stork standalone:: ++ +-- + +-- + +==== + +[source,properties] +---- +stork.my-service.service-registrar.enable=false +---- + +[.tabs] +==== + +stork in quarkus:: ++ +-- + +-- + +==== + +[source,properties] +---- +quarkus.stork.my-service.service-registrar.enable=false +---- + +*IMPORTANT* Public IP address needs to be provided. Smallrye Stork will fail if the service IP address is not provided during registration. + +==== Supported Registries: + +Currently, Smallrye Stork provides seamless integration with Consul, Eureka and a Static registry. +This integration simplifies the management of dynamic environments where services are frequently added or removed. + +==== Custom Registration: + +In addition to the default mechanisms, SmallRye Stork allows you to implement custom service registration strategies, +providing flexibility for different infrastructures or custom service registration needs. + +In the following sections you can have more details about each specific implementation. + +=== Deregister Services + +SmallRye Stork now supports deregistration of service instances from a central registry when explicitly requested. +This feature complements the registration mechanism. + +This operation is typically should be triggered when a service instance is shut down or no longer available, helping maintain a consistent and accurate service registry. +To enable service deregistration, you can invoke the deregistration methods on the `Service` instance programmatically. +To target a specific instance, prefer one of these methods: + +* `deregisterNamedInstance(instanceName)` — deregisters the instance registered under the given name. +* `deregisterServiceInstance(ipAddress, port)` — deregisters the instance identified by IP and port. + +The no-argument overload `deregisterServiceInstance()` deregisters *all* instances of the service and is deprecated, prefer one of the specific methods above. + +*Note*: When used in standalone mode, Stork does *not* automatically handle service instance registration or deregistration. +However, when using the quarkus-stork-registration extension within a Quarkus application, +service instances are automatically registered at application startup and deregistered upon shutdown, unless this behavior is explicitly disabled. + +In the following sections you can have more details about each specific implementation. diff --git a/content/docs/smallrye-stork/main/service-registration/static-list.adoc b/content/docs/smallrye-stork/main/service-registration/static-list.adoc new file mode 100644 index 000000000..87e3eb570 --- /dev/null +++ b/content/docs/smallrye-stork/main/service-registration/static-list.adoc @@ -0,0 +1,96 @@ += Static List Service Registration +:version-root: ../ +include::{version-root}_attributes.adoc[] + +Stork provides the ability to register services using Static list as backend. + +== Dependency + +To use the `static` service registrar, first add the appropriate Service Registration provider dependency to your project: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-service-registration-static-list + {project-version} + +---- + +== Service registration configuration + +For each service that should register the service instances in a static list, configure the service registrar `type`: + +[.tabs] +==== + +stork standalone:: ++ +-- + +-- + +==== + +[source,properties] +---- +stork.my-service.service-registrar.type=static +---- + +[.tabs] +==== + +stork in quarkus:: ++ +-- + +-- + +==== + +[source,properties] +---- +quarkus.stork.my-service.service-registrar.type=static +---- + +Static service registrar is configured with the following parameters: + +include::{generated-dir}/static-sr-attributes.adoc[] + +== Service deregistration configuration + +There is no specific configuration required to enable deregistration; however, you must ensure that a consul service registrar is configured for the service: + +[.tabs] +==== + +stork standalone:: ++ +-- + +-- + +==== + +[source,properties] +---- +stork.my-service.service-registrar.type=static +---- + +[.tabs] +==== + +stork in quarkus:: ++ +-- + +-- + +==== + +[source,properties] +---- +quarkus.stork.my-service.service-registrar.type=static +---- + +As with registration, deregistration relies on the service name. diff --git a/content/docs/smallrye-stork/main/springboot-config.adoc b/content/docs/smallrye-stork/main/springboot-config.adoc new file mode 100644 index 000000000..05d2f1a3b --- /dev/null +++ b/content/docs/smallrye-stork/main/springboot-config.adoc @@ -0,0 +1,40 @@ += Stork Spring Boot Config +include::_attributes.adoc[] + +Stork seamlessly supports Spring Boot configuration, facilitating access to configuration properties. +This documentation elaborates on how Spring Boot developers can use Stork in their Spring Boot applications and configure it using the application.properties file. + +== Dependency setup + +To enable Spring Boot configuration integration in Stork, you need to include the following dependency: + +[source,xml,subs="attributes+"] +---- + + io.smallrye.stork + stork-spring-boot-config + {project-version} + +---- + +== Initializing Stork + +Since Spring Boot lacks a pre-configured Stork instance, you'll need create one. It can be done by providing a Spring bean performing Stork initialization: + +[source,java] +---- +include::{examples-dir}/examples/SpringBootInitializationExample.java[indent=0] +---- + +Upon initialization, Stork scans for the `io.smallrye.stork.springboot.SpringBootConfigProvider` SPI provider and CDI beans (from version 2.x onwards). +It then builds a comprehensive list of managed services by parsing the properties configuration files. + +Please note the importance of the `io.smallrye.stork.springboot.SpringBootApplicationContextProvider` bean in our setup. +This bean has a critical role by granting Stork access to the current `org.springframework.context.ApplicationContext`. +It enables it to retrieve configuration details effectively. Consequently, it's imperative that this bean is instantiated prior to initiating the Stork initialization process. +In this case, we utilize the `@DependsOn` annotation for that. +It allows us controlling the bean creation order. + +== Comprehensive Example + +You can check our https://github.com/aureamunoz/spring-stork-guitar-hero/[Guitar Hero Application] showcasing the seamless integration of Stork with Spring Boot Configuration. diff --git a/content/index.html b/content/index.html index 4c75d18e1..9b65750aa 100644 --- a/content/index.html +++ b/content/index.html @@ -13,7 +13,7 @@

Helping Java Developers deliver for tomorrow

APIs and implementations tailored for Cloud development, including, but not limited to, Eclipse MicroProfile.

Browse the projects - Documentation + Documentation

diff --git a/content/posts/2020-05-22-smallrye-config-1-8.adoc b/content/posts/2020-05-22-smallrye-config-1-8.adoc index 3956d7263..84a2b09b2 100644 --- a/content/posts/2020-05-22-smallrye-config-1-8.adoc +++ b/content/posts/2020-05-22-smallrye-config-1-8.adoc @@ -83,7 +83,7 @@ Not every interceptor is registered by default. Only the `ProfileConfigSourceInt Other interceptors require manual registration via the `ServiceLoader` mechanism. To learn more about all the built-in interceptors, please refer to the -https://smallrye.io/docs/smallrye-config/interceptors/interceptors.html#_built_in_interceptors[documentation page]. +https://smallrye.io/smallrye-config/Latest/extensions/interceptors/[documentation page]. === Expression Expansion @@ -152,4 +152,4 @@ Please, feel free to drop us any feedback in the mailto:smallrye@googlegroups.co == Additional Resources * https://github.com/smallrye/smallrye-config/releases/tag/1.8.0[Release Notes] -* https://smallrye.io/docs/smallrye-config/index.html[Documentation] +* https://smallrye.io/smallrye-config/Latest/[Documentation] diff --git a/content/posts/2020-09-24-smallrye-config-1-9.adoc b/content/posts/2020-09-24-smallrye-config-1-9.adoc index a33ba100f..451d1c164 100644 --- a/content/posts/2020-09-24-smallrye-config-1-9.adoc +++ b/content/posts/2020-09-24-smallrye-config-1-9.adoc @@ -122,7 +122,7 @@ public interface Server { ---- For additional information, check out our documentation about -https://smallrye.io/docs/smallrye-config/mapping/mapping.html[Config Mappings] and the example in the +https://smallrye.io/smallrye-config/Latest/config/mappings/[Config Mappings] and the example in the https://github.com/smallrye/smallrye-config/tree/main/examples/mapping SmallRye Config Github repo. In MicroProfile Config 2.0, an initial version for this feature will be available. It doesn't support all the features @@ -166,7 +166,7 @@ configure other sources produced by lower priority `ConfigSourceFactory`. To reg standard Java `ServiceLoader`. For additional information, check out our documentation about -https://smallrye.io/docs/smallrye-config/config-sources/config-sources.html#_config_source_factory[Config Mappings] and +https://smallrye.io/smallrye-config/Latest/config-sources/factories/[Config Mappings] and the example in the https://github.com/smallrye/smallrye-config/tree/main/examples/configmap SmallRye Config Github repo. @@ -208,4 +208,4 @@ Please, feel free to drop us any feedback to the mailto:smallrye@googlegroups.co * https://github.com/smallrye/smallrye-config/[GitHub Repository] * https://github.com/smallrye/smallrye-config/releases/tag/1.9.0[Release Notes] -* https://smallrye.io/docs/smallrye-config/index.html[Documentation] +* https://smallrye.io/smallrye-config/Latest/[Documentation] diff --git a/content/posts/2021-01-15-fault-tolerance-5-0.adoc b/content/posts/2021-01-15-fault-tolerance-5-0.adoc index b7aa38048..e82a42310 100644 --- a/content/posts/2021-01-15-fault-tolerance-5-0.adoc +++ b/content/posts/2021-01-15-fault-tolerance-5-0.adoc @@ -6,14 +6,13 @@ :page-author: lthon :smallrye-ft: SmallRye Fault Tolerance :microprofile-ft: MicroProfile Fault Tolerance -:uri-smallrye-ft-docs: https://smallrye.io/docs/smallrye-fault-tolerance/5.0.0 = Fault Tolerance 5.0 Roughly a year ago, we link:/blog/fault-tolerance-4-0/[announced] the release of {smallrye-ft} 4.0, the new implementation of Eclipse {microprofile-ft}. Today, we bring you version 5.0, an evolution of the 4.x stream that fully implements {microprofile-ft} 3.0 and brings a lot of improvements and even new features. These new features are not present in the {microprofile-ft} specification, but were requested by users, especially in the Quarkus community. -In addition, the API for integrators has been simplified significantly, and the first version of {uri-smallrye-ft-docs}/index.html[documentation] is also ready. +In addition, the API for integrators has been simplified significantly, and the first version of https://smallrye.io/docs/smallrye-fault-tolerance/5/[documentation] is also ready. Let's take a look. == {microprofile-ft} 3.0 @@ -84,7 +83,7 @@ Afterwards, you can `@Inject` a `CircuitBreakerMaintenance` and use its `current In addition, `CircuitBreakerMaintenance` also allows you to reset any given named circuit breaker (`reset`), or all circuit breakers (`resetAll`), to the initial state. -Read the {uri-smallrye-ft-docs}/usage/extra.html#_circuit_breaker_maintenance[documentation] for more information and examples. +Read the https://smallrye.io/docs/smallrye-fault-tolerance/5/usage/extra/#circuit-breaker-maintenance[documentation] for more information and examples. === `@Blocking` and `@NonBlocking` @@ -98,7 +97,7 @@ On the other hand, `@NonBlocking` means that the annotated method doesn't block Note that these annotations are only taken into account if the annotated method also applies some other fault tolerance strategy, such as `@Fallback` or `@Retry`, _and_ if the method returns `CompletionStage` (or some of the additional asynchronous types as described below). If there's no fault tolerance annotation, or if the method doesn't return `CompletionStage`, {smallrye-ft} will ignore the `@Blocking` and `@NonBlocking` annotations completely. -Read the {uri-smallrye-ft-docs}/usage/extra.html#blocking-nonblocking[documentation] for more information and examples. +Read the https://smallrye.io/docs/smallrye-fault-tolerance/5/usage/extra/#async-nonblocking[documentation] for more information and examples. === Additional asynchronous types @@ -108,7 +107,7 @@ Note that only single-valued types, such as `Single` or `Uni`, are supported; st This support is based on the https://github.com/smallrye/smallrye-reactive-utils/tree/main/reactive-converters[SmallRye Reactive Converters] project, so you have to make sure that the corresponding converter library is present. Integrators may include some converters by default if they choose so. -Read the {uri-smallrye-ft-docs}/usage/extra.html#_additional_asynchronous_types[documentation] for more information and examples. +Read the https://smallrye.io/docs/smallrye-fault-tolerance/5/usage/extra/#async-types[documentation] for more information and examples. === Other improvements @@ -156,7 +155,7 @@ As described above, the SmallRye Reactive Converters project is used to add supp This means that {smallrye-ft} requires the `io.smallrye.reactive:smallrye-reactive-converter-api` artifact to be present. Presence of Reactive Converter API implementations is optional, but the API itself is mandatory. -Read the {uri-smallrye-ft-docs}/integration/intro.html[integration documentation] for more information. +Read the https://smallrye.io/docs/smallrye-fault-tolerance/5/integration/intro/[integration documentation] for more information. == Summary diff --git a/content/posts/2021-05-17-fault-tolerance-5-1.adoc b/content/posts/2021-05-17-fault-tolerance-5-1.adoc index 42b2aa1f6..0afe0a634 100644 --- a/content/posts/2021-05-17-fault-tolerance-5-1.adoc +++ b/content/posts/2021-05-17-fault-tolerance-5-1.adoc @@ -41,7 +41,7 @@ In this release, we provide an integration API for event loops, as well as an im With this integration enabled, invocations of `@NonBlocking` methods made on an event loop thread are always executed on the same event loop thread. -For more information, see link:https://smallrye.io/docs/smallrye-fault-tolerance/5.1.0/usage/extra.html#blocking-nonblocking[user documentation] and link:https://smallrye.io/docs/smallrye-fault-tolerance/5.1.0/integration/event-loop.html[integration documentation]. +For more information, see link:https://smallrye.io/docs/smallrye-fault-tolerance/5/usage/extra/#async-nonblocking[user documentation] and link:https://smallrye.io/docs/smallrye-fault-tolerance/5/integration/event-loop/[integration documentation]. == Others diff --git a/content/posts/2021-07-02-fault-tolerance-5-2.adoc b/content/posts/2021-07-02-fault-tolerance-5-2.adoc index 2eae9921a..354972ef6 100644 --- a/content/posts/2021-07-02-fault-tolerance-5-2.adoc +++ b/content/posts/2021-07-02-fault-tolerance-5-2.adoc @@ -45,7 +45,7 @@ With `@ExponentialBackoff`, the delays between retry attempts will grow exponent With `@FibonacciBackoff`, the delays will grow per the Fibonacci sequence. With `@CustomBackoff`, it is possible to implement completely custom backoff strategy. -See https://smallrye.io/docs/smallrye-fault-tolerance/5.2.0/usage/extra.html#_backoff_strategies_for_retry[the documentation] for more information. +See https://smallrye.io/docs/smallrye-fault-tolerance/5/usage/extra/#backoff-strategies-for-retry[the documentation] for more information. == `@Blocking` and `@NonBlocking` on classes @@ -76,7 +76,7 @@ public class MyService { Here, the `hello` method is treated as `@Blocking`, but other methods would inherit the `@NonBlocking` annotation from the class. -See https://smallrye.io/docs/smallrye-fault-tolerance/5.2.0/usage/extra.html#blocking-nonblocking[the documentation] for more information. +See https://smallrye.io/docs/smallrye-fault-tolerance/5/usage/extra/#async-nonblocking[the documentation] for more information. == Non-compatible mode for determining asynchrony @@ -108,7 +108,7 @@ The compatible mode conforms to the specification, but we find that behavior les Quarkus will enable this mode by default in the 2.1.0.Final release. -See https://smallrye.io/docs/smallrye-fault-tolerance/5.2.0/usage/extra.html#_non_compatible_mode[the documentation] for more information. +See https://smallrye.io/docs/smallrye-fault-tolerance/5/usage/extra/#noncompatible-mode[the documentation] for more information. == Others @@ -116,7 +116,7 @@ Together with the new features described above, we have also performed a major i The configuration system, the single last remnant of {smallrye-ft} 2.x, has been rewritten from scratch. Instead of storing configuration values into a `Map`, the new system is based on configuration interfaces. Implementations of these interfaces are automatically generated during {smallrye-ft} build, using an annotation processor. -See https://smallrye.io/docs/smallrye-fault-tolerance/5.2.0/internals/config.html[the documentation] for more details if you're interested. +See https://smallrye.io/docs/smallrye-fault-tolerance/5/internals/config/[the documentation] for more details if you're interested. Furthermore, we have fixed a small bug in `@Fallback` validation. If you had a method guarded with `@Fallback` that returned a raw type (such as `List`), and its fallback method retured a parameterized type (such as `List`), the validation code would enter an infinite regress. diff --git a/content/posts/2022-02-08-fault-tolerance-5-3.adoc b/content/posts/2022-02-08-fault-tolerance-5-3.adoc index d6b670764..c097b8fe2 100644 --- a/content/posts/2022-02-08-fault-tolerance-5-3.adoc +++ b/content/posts/2022-02-08-fault-tolerance-5-3.adoc @@ -19,7 +19,7 @@ Since the very beginning, {smallrye-ft} has implemented the {microprofile-ft} de It also has several SmallRye-specific additions to this API, such as retry backoff strategies or circuit breaker maintenance. With this release, {smallrye-ft} adds an alternative, programmatic API. -It has https://smallrye.io/docs/smallrye-fault-tolerance/5.3.0/usage/programmatic-api.html[extensive documentation], so we'll just provide a short overview here. +It has https://smallrye.io/docs/smallrye-fault-tolerance/5/usage/programmatic-api/[extensive documentation], so we'll just provide a short overview here. Let's start with an example: [source,java] @@ -101,7 +101,7 @@ In certain contexts, the true exceptions are often (or always) wrapped into anot In such situation, configuring when a fault tolerance strategy should apply or be ignored becomes nearly impossible. In this release, {smallrye-ft} adds a non-standard feature to solve this problem. -In the https://smallrye.io/docs/smallrye-fault-tolerance/5.3.0/usage/extra.html#_non_compatible_mode[non-compatible mode], if the class of the thrown exception isn't present in either of the two sets, the cause chain of the exception is inspected automatically. +In the https://smallrye.io/docs/smallrye-fault-tolerance/5/usage/extra/#noncompatible-mode[non-compatible mode], if the class of the thrown exception isn't present in either of the two sets, the cause chain of the exception is inspected automatically. == Revamped async types support diff --git a/content/posts/2022-03-31-fault-tolerance-5-4.adoc b/content/posts/2022-03-31-fault-tolerance-5-4.adoc index ef1f1c0a7..81832cf55 100644 --- a/content/posts/2022-03-31-fault-tolerance-5-4.adoc +++ b/content/posts/2022-03-31-fault-tolerance-5-4.adoc @@ -50,7 +50,7 @@ public class MyService { } ---- -See the https://smallrye.io/docs/smallrye-fault-tolerance/5.4.0/usage/extra.html#_reusable_preconfigured_fault_tolerance[documentation] for more information. +See the https://smallrye.io/docs/smallrye-fault-tolerance/5/usage/extra/#reusable-preconfigured-fault-tolerance[documentation] for more information. == Exception decisions in programmatic API diff --git a/content/posts/2022-07-11-fault-tolerance-5-5.adoc b/content/posts/2022-07-11-fault-tolerance-5-5.adoc index 45b05736e..efe31fb7b 100644 --- a/content/posts/2022-07-11-fault-tolerance-5-5.adoc +++ b/content/posts/2022-07-11-fault-tolerance-5-5.adoc @@ -23,14 +23,14 @@ In this version, we add support for {mmeter}. The set of metrics emitted to Micrometer is the same as the set of metrics emitted to MicroProfile Metrics, using the same metric names and tags. Metric types are also mapped as closely as possible. -More information about that can be found in the https://smallrye.io/docs/smallrye-fault-tolerance/5.5.0/usage/basic.html#_metrics[documentation]. +More information about that can be found in the https://smallrye.io/docs/smallrye-fault-tolerance/5/usage/basic/#metrics[documentation]. Support for {mmeter} is added mainly for Quarkus, which strives to replace {mp-metrics} with {mmeter}. Note that Quarkus does its own integration of {smallrye-ft}. The CDI Portable Extension present in {smallrye-ft}, which other integrators use, still assumes usage of {mp-metrics}, and does the integration accordingly. That is, if you integrate {smallrye-ft} and are fine with using {mp-metrics}, you don't have to do anything (except managing dependencies correctly, see below). -More information about metrics integration is available in the https://smallrye.io/docs/smallrye-fault-tolerance/5.5.0/integration/metrics.html[documentation]. +More information about metrics integration is available in the https://smallrye.io/docs/smallrye-fault-tolerance/5/integration/metrics/[documentation]. === Transitive dependencies diff --git a/content/posts/2022-10-25-fault-tolerance-5-6-0-and-6-1-0.adoc b/content/posts/2022-10-25-fault-tolerance-5-6-0-and-6-1-0.adoc index 245930540..849460bac 100644 --- a/content/posts/2022-10-25-fault-tolerance-5-6-0-and-6-1-0.adoc +++ b/content/posts/2022-10-25-fault-tolerance-5-6-0-and-6-1-0.adoc @@ -76,7 +76,7 @@ Fallback( ) ---- -A lot more information about rate limiting can be found https://smallrye.io/docs/smallrye-fault-tolerance/5.6.0/usage/extra.html#_rate_limit[in the documentation]. +A lot more information about rate limiting can be found https://smallrye.io/docs/smallrye-fault-tolerance/5/usage/extra/#rate-limit[in the documentation]. == New annotation for asynchronous non-blocking methods @@ -94,7 +94,7 @@ For asynchronous methods that do not require thread offload and return `Completi - in fully compatible mode, use `@AsynchronousNonBlocking`. `@AsynchronousNonBlocking` is a new annotation introduced by {smallrye-ft} in this version. -The https://smallrye.io/docs/smallrye-fault-tolerance/5.6.0/usage/extra.html#async-nonblocking[relevant section in the documentation] was thoroughly revised and contains all the details. +The https://smallrye.io/docs/smallrye-fault-tolerance/5/usage/extra/#async-nonblocking[relevant section in the documentation] was thoroughly revised and contains all the details. === Compatibility @@ -132,7 +132,7 @@ public class MyService { It is possible to declare multiple overloads of the fallback method, each having different type of the exception parameter. It is also possible to declare fallback method(s) with an exception parameter together with a fallback method without an exception parameter. -As usual, see https://smallrye.io/docs/smallrye-fault-tolerance/5.6.0/usage/extra.html#_fallback_method_with_exception_parameter[the documentation] for more information. +As usual, see https://smallrye.io/docs/smallrye-fault-tolerance/5/usage/extra/#fallback-method-with-exception-parameter[the documentation] for more information. == Circuit breaker state transitions diff --git a/content/posts/2023-02-10-fault-tolerance-6-2-0.adoc b/content/posts/2023-02-10-fault-tolerance-6-2-0.adoc index b47c3c196..6b1401a33 100644 --- a/content/posts/2023-02-10-fault-tolerance-6-2-0.adoc +++ b/content/posts/2023-02-10-fault-tolerance-6-2-0.adoc @@ -21,7 +21,7 @@ This release also upgrades to the Jakarta EE 10 dependencies. Previously, the documentation contained numerous references to the {microprofile-ft} specification, and all {smallrye-ft} improvements on top of the specification were described in a single, long, hard to read document. -In {smallrye-ft} 6.2.0, the https://smallrye.io/docs/smallrye-fault-tolerance/6.2.0/[documentation] has been thoroughly restructured and revised. +In {smallrye-ft} 6.2.0, the https://smallrye.io/docs/smallrye-fault-tolerance/6/[documentation] has been thoroughly restructured and revised. It should contain all the necessary information, so that you no longer have to refer to the specification for pretty basic stuff. The improvements on top of the specification are now placed in the same guide that describes the basic feature. diff --git a/content/posts/2024-07-22-fault-tolerance-6-4-0.adoc b/content/posts/2024-07-22-fault-tolerance-6-4-0.adoc index 602aad98f..adca1b7bf 100644 --- a/content/posts/2024-07-22-fault-tolerance-6-4-0.adoc +++ b/content/posts/2024-07-22-fault-tolerance-6-4-0.adoc @@ -14,7 +14,7 @@ This release contains one new feature and one bugfix. The `@BeforeRetry` annotation was added, together with the corresponding methods in the programmatic API. The annotation allows defining an action that should happen before each retry, but not before the initial attempt. -See the https://smallrye.io/docs/smallrye-fault-tolerance/6.4.0/reference/retry.html#_before_retry_actions[documentation] for more information. +See the https://smallrye.io/docs/smallrye-fault-tolerance/6/reference/retry/#before-retry-actions[documentation] for more information. Further, the `synchronized` keyword was replaced on one place with a `ReentrantLock`, making the programmatic API more friendly with virtual threads. diff --git a/content/posts/2024-11-28-fault-tolerance-6-7-0.adoc b/content/posts/2024-11-28-fault-tolerance-6-7-0.adoc index 2fb77fc6e..3af1b92bb 100644 --- a/content/posts/2024-11-28-fault-tolerance-6-7-0.adoc +++ b/content/posts/2024-11-28-fault-tolerance-6-7-0.adoc @@ -72,8 +72,8 @@ Comparison of `@ApplyFaultTolerance` and `@ApplyGuard`: For more information about migrating to the new programmatic API, see the migration guides: -* link:https://smallrye.io/docs/smallrye-fault-tolerance/6.7.0/reference/programmatic-api.html#migration_from_faulttolerance[Migration from `FaultTolerance`] -* link:https://smallrye.io/docs/smallrye-fault-tolerance/6.7.0/reference/reusable.html#migration_from_applyfaulttolerance[Migration from `@ApplyFaultTolerance`] +* link:https://smallrye.io/docs/smallrye-fault-tolerance/6/reference/programmatic-api/#migration_from_faulttolerance[Migration from `FaultTolerance`] +* link:https://smallrye.io/docs/smallrye-fault-tolerance/6/reference/reusable/#migration_from_applyfaulttolerance[Migration from `@ApplyFaultTolerance`] Further, the `MutinyFaultTolerance` API has no replacement. The `Guard` and `TypedGuard` APIs support `Uni` out of the box, if the Mutiny support library is present. diff --git a/data/docs.yml b/data/docs.yml new file mode 100644 index 000000000..d347e8609 --- /dev/null +++ b/data/docs.yml @@ -0,0 +1,259 @@ +# Every SmallRye project with documentation, listed on /docs/. +# +# A project with `repo` and `path` has its documentation on this site: the sync (tools/docs/Docs.java) copies +# the AsciiDoc in / into content/docs///, published at +# /docs///. Versions it follows: +# main the branch head, synced every night +# latest the newest tag matching `latest-tag` +# streams older release lines kept online, as - id: "2" tag: "^2\.\d+\.\d+$" +# (ids are lowercase letters, digits and hyphens: they are part of the address) +# An archived project follows no branch (`archived: true`): its streams are all it has. +# Until a release carries AsciiDoc docs, the sync converts that release's MkDocs pages +# (`mkdocs`) on the fly, so the site never waits for a project's next release. +# +# A project without `path` is not migrated yet: `url` is where its documentation lives today. +# `maven-attributes` sets attributes from the Maven properties of the version published +# (attribute: property, from the repository's pom.xml), so versions the pages mention follow the +# build instead of being copied by hand. +# A stream can carry its own `mkdocs` settings, for a release line that laid its MkDocs sources out +# differently. +# `redirects` maps single old addresses on this site to new ones. +# `archive` points to the documentation of releases published before the move, on the project's +# own pages. `legacy` lists the addresses this site published before (Antora's +# /docs///.html): `Docs.java redirects` points each of them to the same page +# in the version that replaces it. + +projects: + - key: smallrye-graphql + name: SmallRye GraphQL + description: Implementation of MicroProfile GraphQL, with type-safe and dynamic Java clients. + group: microprofile + repo: smallrye/smallrye-graphql + path: docs/src/main/asciidoc + branch: main + latest-tag: '^\d+\.\d+\.\d+(\.(Final|Beta\d+|CR\d+|Alpha\d+))?$' + streams: + - id: '2' + tag: '^2\.\d+\.\d+$' + - id: '1' + tag: '^1\.\d+\.\d+$' + mkdocs: + config: mkdocs.yml + snippet-prefix: 'docs/snippets/=' + examples-dir: ../java + examples-root: docs/snippets + + - key: smallrye-config + name: SmallRye Config + description: A Java configuration library, and the implementation of MicroProfile Config. + group: microprofile + repo: smallrye/smallrye-config + path: documentation/src/main/asciidoc + branch: main + latest-tag: '^\d+\.\d+\.\d+$' + streams: + - id: '3' + tag: '^3\.\d+\.\d+$' + - id: '2' + tag: '^2\.\d+\.\d+$' + mkdocs: + config: documentation/mkdocs.yaml + examples-root: documentation/src/main/java + redirects: + # the Antora documentation of Config 1.x, gone since 2022 + /docs/smallrye-config/index.html: /docs/smallrye-config/latest/ + + - key: smallrye-fault-tolerance + name: SmallRye Fault Tolerance + description: Bulkheads, circuit breakers, fallbacks, rate limits, retries and timeouts; implements MicroProfile Fault Tolerance. + group: microprofile + repo: smallrye/smallrye-fault-tolerance + path: docs/src/main/asciidoc + branch: main + latest-tag: '^\d+\.\d+\.\d+$' + streams: + - id: '6' + tag: '^6\.\d+\.\d+$' + - id: '5' + tag: '^5\.\d+\.\d+$' + antora: doc + legacy: + url: '/docs/smallrye-fault-tolerance/{release}/{page}.html' + # sections renamed between the releases and the version that replaces them + anchors: + blocking-nonblocking: async-nonblocking + _additional_asynchronous_types: async-types + releases: + '6': ['6.0.0', '6.1.0', '6.2.0', '6.2.1', '6.2.2', '6.2.3', '6.2.4', '6.2.6', '6.3.0', '6.4.0', '6.4.1', '6.4.2', '6.4.3', '6.5.0', '6.6.0', '6.6.1', '6.6.2', '6.6.3', '6.7.0', '6.7.1', '6.7.2', '6.7.3', '6.8.0', '6.9.0', '6.9.1', '6.9.2', '6.9.3', '6.9.4', '6.10.0', '6.10.1', '6.10.2', '6.11.0', '6.11.1', '6.11.2', '6.11.3', '6.11.4'] + '5': ['5.0.0', '5.1.0', '5.2.0', '5.2.1', '5.3.0', '5.3.1', '5.3.2', '5.4.0', '5.4.1', '5.5.0', '5.6.0'] + + - key: smallrye-health + name: SmallRye Health + description: Implementation of MicroProfile Health. + group: microprofile + repo: smallrye/smallrye-health + path: docs/src/main/asciidoc + branch: main + latest-tag: '^\d+\.\d+\.\d+$' + antora: doc + legacy: + url: '/docs/smallrye-health/{release}/{page}.html' + releases: + latest: ['3.0.0', '3.0.1'] + + - key: smallrye-jwt + name: SmallRye JWT + description: Implementation of MicroProfile JWT RBAC, plus an API to build and sign tokens. + group: microprofile + repo: smallrye/smallrye-jwt + path: docs/src/main/asciidoc + branch: main + latest-tag: '^\d+\.\d+\.\d+$' + antora: doc + legacy: + url: '/docs/smallrye-jwt/{page}.html' + releases: + latest: [''] + + - key: smallrye-metrics + name: SmallRye Metrics + description: Implementation of MicroProfile Metrics. The project is archived; this is its last documentation. + group: microprofile + repo: smallrye/smallrye-metrics + path: docs/src/main/asciidoc + archived: true + streams: + - id: '3' + tag: '^3\.0\.\d+$' + - id: '2' + tag: '^2\.4\.\d+$' + antora: doc + legacy: + url: '/docs/smallrye-metrics/{release}/{page}.html' + releases: + '3': ['3.0.0'] + '2': ['2.4.0'] + + - key: smallrye-reactive-messaging + name: SmallRye Reactive Messaging + description: Event-driven, message-based applications on Kafka, AMQP, MQTT, RabbitMQ, Pulsar and more. + group: reactive + repo: smallrye/smallrye-reactive-messaging + path: documentation/src/main/asciidoc + branch: main + latest-tag: '^\d+\.\d+\.\d+(-RC\d+)?$' + streams: + - id: '4' + tag: '^4\.\d+\.\d+$' + - id: '3' + tag: '^3\.\d+\.\d+$' + mkdocs: + config: documentation/mkdocs.yml + examples-root: documentation/src/main/java + snippet-prefix: '../../../target/connectors/={generated-dir}/' + # The connector attribute tables are generated into each connector jar (META-INF/connector/). + generated: + maven-dependencies-of: documentation/pom.xml + group: io.smallrye.reactive + extract: META-INF/connector/ + # Dependency versions the pages mention, from the pom.xml of the version published. + maven-attributes: + attributes: + weld-version: weld.version + smallrye-config-version: smallrye-config.version + mutiny-version: mutiny.version + camel-version: camel.version + spec-version: microprofile-reactive-messaging.version + + - key: smallrye-mutiny + name: SmallRye Mutiny + description: An intuitive, event-driven reactive programming library for Java. + group: reactive + repo: smallrye/smallrye-mutiny + path: documentation/src/main/asciidoc + branch: main + latest-tag: '^\d+\.\d+\.\d+$' + streams: + - id: '2' + tag: '^2\.\d+\.\d+$' + mkdocs: + config: documentation/mkdocs.yml + examples-root: documentation/src/test + + - key: smallrye-mutiny-vertx-bindings + name: Mutiny Vert.x bindings + description: Mutiny APIs for Eclipse Vert.x. + group: reactive + repo: smallrye/smallrye-mutiny-vertx-bindings + path: docs/src/main/asciidoc + branch: main + latest-tag: '^\d+\.\d+\.\d+$' + streams: + - id: '3' + tag: '^3\.\d+\.\d+$' + - id: '2' + tag: '^2\.\d+\.\d+$' + mkdocs: + config: mkdocs.yml + + - key: smallrye-mutiny-zero + name: Mutiny Zero + description: A zero-dependency Reactive Streams publishers library. + group: reactive + repo: smallrye/smallrye-mutiny-zero + path: docs/src/main/asciidoc + branch: main + latest-tag: '^\d+\.\d+\.\d+$' + streams: + - id: '0' + tag: '^0\.\d+\.\d+$' + mkdocs: + config: mkdocs.yml + examples-root: . + + - key: smallrye-stork + name: SmallRye Stork + description: Service discovery and client-side load balancing. + group: standalone + repo: smallrye/smallrye-stork + path: docs/src/main/asciidoc + branch: main + latest-tag: '^\d+\.\d+\.\d+$' + streams: + - id: '2' + tag: '^2\.\d+\.\d+$' + - id: '1' + tag: '^1\.\d+\.\d+$' + # 1.x includes its examples as snippets/examples/... (relative to docs/) + mkdocs: + snippet-prefix: + - 'snippets/=' + - 're:^.*META-INF/stork-docs/={generated-dir}/' + mkdocs: + config: docs/mkdocs.yml + examples-root: docs/snippets + # the tables, wherever a page took them from (target/attributes/ or a module's target/classes/) + snippet-prefix: 're:^.*META-INF/stork-docs/={generated-dir}/' + # The configuration attribute tables are generated into each Stork jar (META-INF/stork-docs/). + generated: + maven-dependencies-of: docs/pom.xml + group: io.smallrye.stork + extract: META-INF/stork-docs/ + + - key: jandex + name: Jandex + description: A space-efficient Java class file indexer and offline reflection library. + group: standalone + repo: smallrye/jandex + path: docs/src/main/asciidoc + branch: main + latest-tag: '^\d+\.\d+\.\d+$' + antora: doc + +groups: + - key: microprofile + name: MicroProfile implementations + - key: reactive + name: Reactive + - key: standalone + name: Libraries diff --git a/data/docversions.yml b/data/docversions.yml new file mode 100644 index 000000000..69b77b53a --- /dev/null +++ b/data/docversions.yml @@ -0,0 +1,2508 @@ +# Generated by tools/docs/Docs.java sync: the documentation versions on the site, the ref each +# came from, and its navigation. Do not edit; run the sync instead. +jandex: + versions: + - converted: true + id: latest + nav: + - page: index + title: Introduction + - items: + - page: core/getting + title: Getting Jandex + - page: core/browsing + title: Browsing Declarations and Types + - page: core/indexing + title: Indexing Classes + title: Jandex + - items: + - page: maven/basic + title: Basic Usage + - page: maven/advanced + title: Advanced Usage + - page: maven/shading + title: Usage with Shading + title: Jandex Maven Plugin + ref: 3.6.0 + sha: 566fc2e71ce3fdec300a1d0575567e087ead23b2 + synced: '2026-10-15' + - converted: true + id: main + nav: + - page: index + title: Introduction + - items: + - page: core/getting + title: Getting Jandex + - page: core/browsing + title: Browsing Declarations and Types + - page: core/indexing + title: Indexing Classes + title: Jandex + - items: + - page: maven/basic + title: Basic Usage + - page: maven/advanced + title: Advanced Usage + - page: maven/shading + title: Usage with Shading + title: Jandex Maven Plugin + ref: main + sha: b2a6a19123f4c7369ff5ec24cff67089f71ce093 + synced: '2026-10-15' +smallrye-config: + versions: + - converted: true + id: latest + nav: + - page: index + title: Home + - items: + - page: config/getting-started + title: Getting Started + - page: config/expressions + title: Expressions + - page: config/profiles + title: Profiles + - page: config/environment-variables + title: Environment Variables + - page: config/indexed-properties + title: Indexed Properties + - page: config/config-value + title: ConfigValue + - page: config/mappings + title: Mappings + - page: config/instance-builder + title: Instance Builder + - page: config/map-support + title: Map Support + - page: config/secret-keys + title: Secret Keys + - page: config/customizer + title: Customizer + - page: config/configuration + title: Configuration Reference + title: Config + - items: + - page: config-sources/custom + title: Custom + - page: config-sources/factories + title: Factories + - page: config-sources/locations + title: Locations + - page: config-sources/yaml + title: YAML + - page: config-sources/toml + title: TOML + - page: config-sources/json + title: JSON + - page: config-sources/filesystem + title: FileSystem + - page: config-sources/zookeeper + title: ZooKeeper + - page: config-sources/hocon + title: HOCON + - page: config-sources/keystore + title: KeyStore + title: Config Sources + - items: + - page: converters/custom + title: Custom + title: Converters + - items: + - page: extensions/interceptors + title: Interceptors + - page: extensions/fallback + title: Fallback + - page: extensions/relocate + title: Relocate + - page: extensions/logging + title: Logging + title: Extensions + ref: 4.0.0 + sha: fa21e03fe5d0fadf288ac9bc75dcd109e678ec1a + synced: '2026-10-15' + - converted: true + id: '3' + nav: + - page: index + title: Home + - items: + - page: config/getting-started + title: Getting Started + - page: config/expressions + title: Expressions + - page: config/profiles + title: Profiles + - page: config/environment-variables + title: Environment Variables + - page: config/indexed-properties + title: Indexed Properties + - page: config/config-value + title: ConfigValue + - page: config/mappings + title: Mappings + - page: config/map-support + title: Map Support + - page: config/secret-keys + title: Secret Keys + - page: config/customizer + title: Customizer + - page: config/configuration + title: Configuration Reference + title: Config + - items: + - page: config-sources/custom + title: Custom + - page: config-sources/factories + title: Factories + - page: config-sources/locations + title: Locations + - page: config-sources/yaml + title: YAML + - page: config-sources/json + title: JSON + - page: config-sources/filesystem + title: FileSystem + - page: config-sources/zookeeper + title: ZooKeeper + - page: config-sources/hocon + title: HOCON + - page: config-sources/keystore + title: KeyStore + title: Config Sources + - items: + - page: converters/custom + title: Custom + title: Converters + - items: + - page: extensions/interceptors + title: Interceptors + - page: extensions/fallback + title: Fallback + - page: extensions/relocate + title: Relocate + - page: extensions/logging + title: Logging + - page: extensions/config-events + title: Events + - page: extensions/config-source-injection + title: Config Source Injection + title: Extensions + ref: 3.18.3 + sha: null + synced: '2026-10-15' + - converted: true + id: '2' + nav: + - page: index + title: Home + - items: + - page: config/getting-started + title: Getting Started + - page: config/expressions + title: Expressions + - page: config/profiles + title: Profiles + - page: config/environment-variables + title: Environment Variables + - page: config/indexed-properties + title: Indexed Properties + - page: config/config-value + title: ConfigValue + - page: config/mappings + title: Mappings + - page: config/map-support + title: Map Support + - page: config/secret-keys + title: Secret Keys + - page: config/configuration + title: Configuration Reference + title: Config + - items: + - page: config-sources/custom + title: Custom + - page: config-sources/factories + title: Factories + - page: config-sources/locations + title: Locations + - page: config-sources/yaml + title: YAML + - page: config-sources/filesystem + title: FileSystem + - page: config-sources/zookeeper + title: ZooKeeper + - page: config-sources/hocon + title: HOCON + title: Config Sources + - items: + - page: converters/custom + title: Custom + - page: converters/json + title: JSON + title: Converters + - items: + - page: extensions/interceptors + title: Interceptors + - page: extensions/fallback + title: Fallback + - page: extensions/relocate + title: Relocate + - page: extensions/logging + title: Logging + title: Extensions + ref: 2.13.3 + sha: null + synced: '2026-10-15' + - converted: true + id: main + nav: + - page: index + title: Home + - items: + - page: config/getting-started + title: Getting Started + - page: config/expressions + title: Expressions + - page: config/profiles + title: Profiles + - page: config/environment-variables + title: Environment Variables + - page: config/indexed-properties + title: Indexed Properties + - page: config/config-value + title: ConfigValue + - page: config/mappings + title: Mappings + - page: config/instance-builder + title: Instance Builder + - page: config/map-support + title: Map Support + - page: config/secret-keys + title: Secret Keys + - page: config/customizer + title: Customizer + - page: config/configuration + title: Configuration Reference + title: Config + - items: + - page: config-sources/custom + title: Custom + - page: config-sources/factories + title: Factories + - page: config-sources/locations + title: Locations + - page: config-sources/yaml + title: YAML + - page: config-sources/toml + title: TOML + - page: config-sources/json + title: JSON + - page: config-sources/filesystem + title: FileSystem + - page: config-sources/zookeeper + title: ZooKeeper + - page: config-sources/hocon + title: HOCON + - page: config-sources/keystore + title: KeyStore + title: Config Sources + - items: + - page: converters/custom + title: Custom + title: Converters + - items: + - page: extensions/interceptors + title: Interceptors + - page: extensions/fallback + title: Fallback + - page: extensions/relocate + title: Relocate + - page: extensions/logging + title: Logging + title: Extensions + ref: main + sha: b496eece429e850170b5ca009a80d063aba21474 + synced: '2026-10-15' +smallrye-fault-tolerance: + versions: + - converted: true + id: latest + nav: + - page: index + title: Overview + - items: + - page: howto/begin + title: Begin + - page: howto/fallback + title: Supply Fallback Values + - page: howto/bulkhead + title: Limit Concurrency + - page: howto/circuit-breaker + title: Fail Fast on Recurring Failures + - page: howto/timeout + title: Watch for Timeouts + - page: howto/retry + title: Retry on Failures + - page: howto/asynchronous + title: Guard Asynchronous Methods + - page: howto/rate-limit + title: Limit Execution Rate + - page: howto/multiple + title: Use Multiple Strategies + title: How-to + - items: + - page: reference/fallback + title: Fallback + - page: reference/bulkhead + title: Bulkhead + - page: reference/circuit-breaker + title: Circuit Breaker + - page: reference/timeout + title: Timeout + - page: reference/retry + title: Retry + - page: reference/asynchronous + title: Asynchronous Execution + - page: reference/rate-limit + title: Rate Limit + - page: reference/config + title: Configuration + - page: reference/metrics + title: Metrics + - page: reference/non-compat + title: Non-compatible Mode + - page: reference/programmatic-api + title: Programmatic API + - page: reference/reusable + title: Reusable Fault Tolerance + title: Reference + - items: + - page: integration/intro + title: Introduction + - page: integration/metrics + title: Metrics + - page: integration/thread-pool + title: Thread Pool + - page: integration/context-propagation + title: Context Propagation + - page: integration/event-loop + title: Event Loop + - page: integration/async-types + title: Additional Asynchronous Types Integration Concerns + - page: integration/kotlin + title: Kotlin Integration Concerns + - page: integration/programmatic-api + title: Programmatic API Integration Concerns + title: Integration + - items: + - page: internals/project-structure + title: Project Structure + - page: internals/instructions + title: Instructions + - page: internals/core + title: Fault Tolerance Core + - page: internals/config + title: Configuration System + - page: internals/logging + title: Logging + title: Internals + ref: 7.0.0 + sha: 2b50e5515fd6461e4db046220f16f4ca49bed947 + synced: '2026-10-15' + - converted: true + id: '6' + nav: + - page: index + title: Overview + - items: + - page: howto/begin + title: Begin + - page: howto/fallback + title: Supply Fallback Values + - page: howto/bulkhead + title: Limit Concurrency + - page: howto/circuit-breaker + title: Fail Fast on Recurring Failures + - page: howto/timeout + title: Watch for Timeouts + - page: howto/retry + title: Retry on Failures + - page: howto/asynchronous + title: Guard Asynchronous Methods + - page: howto/rate-limit + title: Limit Execution Rate + - page: howto/multiple + title: Use Multiple Strategies + title: How-to + - items: + - page: reference/fallback + title: Fallback + - page: reference/bulkhead + title: Bulkhead + - page: reference/circuit-breaker + title: Circuit Breaker + - page: reference/timeout + title: Timeout + - page: reference/retry + title: Retry + - page: reference/asynchronous + title: Asynchronous Execution + - page: reference/rate-limit + title: Rate Limit + - page: reference/config + title: Configuration + - page: reference/metrics + title: Metrics + - page: reference/non-compat + title: Non-compatible Mode + - page: reference/programmatic-api + title: Programmatic API + - page: reference/reusable + title: Reusable Fault Tolerance + title: Reference + - items: + - page: integration/intro + title: Introduction + - page: integration/metrics + title: Metrics + - page: integration/thread-pool + title: Thread Pool + - page: integration/context-propagation + title: Context Propagation + - page: integration/opentracing + title: OpenTracing + - page: integration/event-loop + title: Event Loop + - page: integration/async-types + title: Additional Asynchronous Types Integration Concerns + - page: integration/kotlin + title: Kotlin Integration Concerns + - page: integration/programmatic-api + title: Programmatic API Integration Concerns + title: Integration + - items: + - page: internals/project-structure + title: Project Structure + - page: internals/instructions + title: Instructions + - page: internals/core + title: Fault Tolerance Core + - page: internals/config + title: Configuration System + - page: internals/logging + title: Logging + title: Internals + ref: 6.11.4 + sha: 8cc24faf4870f5dd1c399ef9e82a23559e090933 + synced: '2026-10-15' + - converted: true + id: '5' + nav: + - page: index + title: Overview + - items: + - page: usage/basic + title: Basic Usage + - page: usage/extra + title: Extra Features + - page: usage/programmatic-api + title: Programmatic API + title: Usage + - items: + - page: integration/intro + title: Introduction + - page: integration/metrics + title: Metrics + - page: integration/thread-pool + title: Thread Pool + - page: integration/context-propagation + title: Context Propagation + - page: integration/opentracing + title: OpenTracing + - page: integration/event-loop + title: Event Loop + - page: integration/async-types + title: Additional Asynchronous Types Integration Concerns + - page: integration/kotlin + title: Kotlin Integration Concerns + - page: integration/programmatic-api + title: Programmatic API Integration Concerns + title: Integration + - items: + - page: internals/project-structure + title: Project Structure + - page: internals/instructions + title: Instructions + - page: internals/core + title: Fault Tolerance Core + - page: internals/config + title: Configuration System + - page: internals/logging + title: Logging + title: Internals + ref: 5.6.0 + sha: 1efeb5eab3642e85e7b1fec85f551ca2d14c164c + synced: '2026-10-15' + - converted: true + id: main + nav: + - page: index + title: Overview + - items: + - page: howto/begin + title: Begin + - page: howto/fallback + title: Supply Fallback Values + - page: howto/bulkhead + title: Limit Concurrency + - page: howto/circuit-breaker + title: Fail Fast on Recurring Failures + - page: howto/timeout + title: Watch for Timeouts + - page: howto/retry + title: Retry on Failures + - page: howto/asynchronous + title: Guard Asynchronous Methods + - page: howto/rate-limit + title: Limit Execution Rate + - page: howto/multiple + title: Use Multiple Strategies + title: How-to + - items: + - page: reference/fallback + title: Fallback + - page: reference/bulkhead + title: Bulkhead + - page: reference/circuit-breaker + title: Circuit Breaker + - page: reference/timeout + title: Timeout + - page: reference/retry + title: Retry + - page: reference/asynchronous + title: Asynchronous Execution + - page: reference/rate-limit + title: Rate Limit + - page: reference/config + title: Configuration + - page: reference/metrics + title: Metrics + - page: reference/non-compat + title: Non-compatible Mode + - page: reference/programmatic-api + title: Programmatic API + - page: reference/reusable + title: Reusable Fault Tolerance + title: Reference + - items: + - page: integration/intro + title: Introduction + - page: integration/metrics + title: Metrics + - page: integration/thread-pool + title: Thread Pool + - page: integration/context-propagation + title: Context Propagation + - page: integration/event-loop + title: Event Loop + - page: integration/async-types + title: Additional Asynchronous Types Integration Concerns + - page: integration/kotlin + title: Kotlin Integration Concerns + - page: integration/programmatic-api + title: Programmatic API Integration Concerns + title: Integration + - items: + - page: internals/project-structure + title: Project Structure + - page: internals/instructions + title: Instructions + - page: internals/core + title: Fault Tolerance Core + - page: internals/config + title: Configuration System + - page: internals/logging + title: Logging + title: Internals + ref: main + sha: b474eedebc0e681f2c7431b86e6fcbd37c64e71e + synced: '2026-10-15' +smallrye-graphql: + versions: + - converted: true + id: latest + nav: + - page: index + title: Overview + - items: + - page: custom-json-serializers-deserializers + title: Customizing JSON (de)serializers + - page: directives + title: Directives + - page: federation + title: Federation + - page: custom-error-extensions + title: Custom error extensions + - page: extensions + title: Response extensions + - page: mutation-void + title: Returning Void from Mutations + - page: handling-init-payload-from-the-websocket + title: Handling of the WebSocket's init-payload + - page: custom-scalar + title: Custom scalars + - page: inspecting-executable-directives-on-server-side + title: Inspecting executable directives + - page: namespaces-on-server-side + title: Namespaces + - page: target-for-input-types + title: Target input fields + - page: kotlin + title: Kotlin support + title: Server side features + - items: + - page: typesafe-client-usage + title: Basic usage + - page: typesafe-client-unions-and-interfaces + title: Unions and Interfaces + - page: typesafe-client-reactive-types + title: Reactive + - page: typesafe-client-custom-scalars + title: Custom scalar types + - page: typesafe-client-headers + title: Adding headers + - page: typesafe-client-logging + title: Logging + - page: typesafe-client-multiple-queries + title: Running multiple queries at once + - page: typesafe-client-error-handling + title: Error handling + - page: typesafe-client-accessing-metadata-of-responses + title: Accessing metadata of responses + - page: typesafe-client-directives + title: Static Directives + title: Typesafe client + - items: + - page: dynamic-client-usage + title: Basic usage + - page: dynamic-client-error-handling + title: Error handling + title: Dynamic client + - items: + - page: client-standalone + title: Bootstrapping the client + - page: using-smallrye-stork + title: Using SmallRye Stork + title: Clients in general + - items: + - page: maven-plugin + title: Maven plugin + - page: gradle-plugin + title: Gradle plugin + title: Developer tools + - items: + - page: server-configuration + title: Server-side configuration + - page: client-configuration + title: Client-side configuration + title: Reference material + ref: 3.0.0.Beta8 + sha: 91e4f32a837f8b80fe7b867d59ec59f69104470b + synced: '2026-10-15' + - converted: true + id: '2' + nav: + - page: index + title: Overview + - items: + - page: custom-json-serializers-deserializers + title: Customizing JSON (de)serializers + - page: directives + title: Directives + - page: federation + title: Federation + - page: custom-error-extensions + title: Custom error extensions + - page: extensions + title: Response extensions + - page: mutation-void + title: Returning Void from Mutations + - page: handling-init-payload-from-the-websocket + title: Handling of the WebSocket's init-payload + - page: custom-scalar + title: Custom scalars + - page: inspecting-executable-directives-on-server-side + title: Inspecting executable directives + - page: namespaces-on-server-side + title: Namespaces + - page: kotlin + title: Kotlin support + title: Server side features + - items: + - page: typesafe-client-usage + title: Basic usage + - page: typesafe-client-unions-and-interfaces + title: Unions and Interfaces + - page: typesafe-client-reactive-types + title: Reactive + - page: typesafe-client-custom-scalars + title: Custom scalar types + - page: typesafe-client-headers + title: Adding headers + - page: typesafe-client-logging + title: Logging + - page: typesafe-client-multiple-queries + title: Running multiple queries at once + - page: typesafe-client-error-handling + title: Error handling + - page: typesafe-client-accessing-metadata-of-responses + title: Accessing metadata of responses + - page: typesafe-client-directives + title: Static Directives + title: Typesafe client + - items: + - page: dynamic-client-usage + title: Basic usage + - page: dynamic-client-error-handling + title: Error handling + title: Dynamic client + - items: + - page: client-standalone + title: Bootstrapping the client + - page: using-smallrye-stork + title: Using SmallRye Stork + title: Clients in general + - items: + - page: maven-plugin + title: Maven plugin + - page: gradle-plugin + title: Gradle plugin + title: Developer tools + - items: + - page: server-configuration + title: Server-side configuration + - page: client-configuration + title: Client-side configuration + title: Reference material + ref: 2.18.5 + sha: null + synced: '2026-10-15' + - converted: true + id: '1' + nav: + - page: index + title: Overview + - items: + - page: custom-json-deserializers + title: Customizing JSON deserializers + - page: directives + title: Directives + - page: federation + title: Federation + - page: custom-error-extensions + title: Custom error extensions + - page: extensions + title: Response extensions + title: Server side features + - items: + - page: typesafe-client-usage + title: Basic usage + - page: typesafe-client-reactive-types + title: Reactive + - page: typesafe-client-custom-scalars + title: Custom scalar types + - page: typesafe-client-headers + title: Adding headers + - page: typesafe-client-logging + title: Logging + - page: typesafe-client-multiple-queries + title: Running multiple queries at once + - page: typesafe-client-error-handling + title: Error handling + title: Typesafe client + - items: + - page: dynamic-client-usage + title: Basic usage + - page: dynamic-client-error-handling + title: Error handling + title: Dynamic client + - items: + - page: using-smallrye-stork + title: Using SmallRye Stork + title: Clients in general + - items: + - page: maven-plugin + title: Maven plugin + - page: gradle-plugin + title: Gradle plugin + title: Developer tools + - items: + - page: server-configuration + title: Server-side configuration + - page: client-configuration + title: Client-side configuration + title: Reference material + ref: 1.9.5 + sha: null + synced: '2026-10-15' + - converted: true + id: main + nav: + - page: index + title: Overview + - items: + - page: custom-json-serializers-deserializers + title: Customizing JSON (de)serializers + - page: directives + title: Directives + - page: federation + title: Federation + - page: custom-error-extensions + title: Custom error extensions + - page: extensions + title: Response extensions + - page: mutation-void + title: Returning Void from Mutations + - page: handling-init-payload-from-the-websocket + title: Handling of the WebSocket's init-payload + - page: custom-scalar + title: Custom scalars + - page: inspecting-executable-directives-on-server-side + title: Inspecting executable directives + - page: namespaces-on-server-side + title: Namespaces + - page: target-for-input-types + title: Target input fields + - page: kotlin + title: Kotlin support + title: Server side features + - items: + - page: typesafe-client-usage + title: Basic usage + - page: typesafe-client-unions-and-interfaces + title: Unions and Interfaces + - page: typesafe-client-reactive-types + title: Reactive + - page: typesafe-client-custom-scalars + title: Custom scalar types + - page: typesafe-client-headers + title: Adding headers + - page: typesafe-client-logging + title: Logging + - page: typesafe-client-multiple-queries + title: Running multiple queries at once + - page: typesafe-client-error-handling + title: Error handling + - page: typesafe-client-accessing-metadata-of-responses + title: Accessing metadata of responses + - page: typesafe-client-directives + title: Static Directives + title: Typesafe client + - items: + - page: dynamic-client-usage + title: Basic usage + - page: dynamic-client-error-handling + title: Error handling + title: Dynamic client + - items: + - page: client-standalone + title: Bootstrapping the client + - page: using-smallrye-stork + title: Using SmallRye Stork + title: Clients in general + - items: + - page: maven-plugin + title: Maven plugin + - page: gradle-plugin + title: Gradle plugin + title: Developer tools + - items: + - page: server-configuration + title: Server-side configuration + - page: client-configuration + title: Client-side configuration + title: Reference material + ref: main + sha: 8a13b1627cbdba22b0a33b6a917c4fa987fe1d3f + synced: '2026-10-15' +smallrye-health: + versions: + - converted: true + id: latest + nav: + - page: index + title: Overview + - items: + - page: async-checks + title: Asynchronous health checks + - page: health-groups + title: Health Groups + - page: health-registry + title: Health Registry + - page: wellness + title: Wellness + - page: static-access + title: Static access + - page: custom-config-properties + title: Custom Config properties + title: Features on top of the specification + ref: 4.3.0 + sha: 82491635611c1415527df52972f0ffa6faaa0a78 + synced: '2026-10-15' + - converted: true + id: main + nav: + - page: index + title: Overview + - items: + - page: async-checks + title: Asynchronous health checks + - page: health-groups + title: Health Groups + - page: health-registry + title: Health Registry + - page: wellness + title: Wellness + - page: static-access + title: Static access + - page: custom-config-properties + title: Custom Config properties + title: Features on top of the specification + ref: main + sha: 30079851263182c051e7475ded0bd764b526d28d + synced: '2026-10-15' +smallrye-jwt: + versions: + - converted: true + id: latest + nav: + - page: index + title: Overview + - page: configuration + title: Configuration + - page: generate-jwt + title: Generate JWT Tokens + ref: 4.6.4 + sha: f6a6e1270661f80a3b51ff4cbde366909eaa7306 + synced: '2026-10-15' + - converted: true + id: main + nav: + - page: index + title: Overview + - page: configuration + title: Configuration + - page: generate-jwt + title: Generate JWT Tokens + ref: main + sha: 3ce74581854a4870fd3acad3f86a28499a49506e + synced: '2026-10-15' +smallrye-metrics: + versions: + - converted: true + id: '3' + nav: + - page: index + title: Overview + - items: + - page: extended-metadata + title: Extended metadata + title: Features on top of the specification + - items: + - page: cdi-extension + title: CDI Extension + - page: base-and-vendor-metrics + title: Base and vendor metrics + - page: metric-registry-lifecycle + title: Metric registry lifecycle + - page: jaxrs-metrics + title: JAX-RS metrics + title: Integrating SmallRye Metrics into runtimes + ref: 3.0.5 + sha: adaa9e2c68653b64ae813e0c00beff5593d6719f + synced: '2026-10-15' + - converted: true + id: '2' + nav: + - page: index + title: Overview + - items: + - page: extended-metadata + title: Extended metadata + title: Features on top of the specification + - items: + - page: cdi-extension + title: CDI Extension + - page: base-and-vendor-metrics + title: Base and vendor metrics + - page: metric-registry-lifecycle + title: Metric registry lifecycle + - page: jaxrs-metrics + title: JAX-RS metrics + title: Integrating SmallRye Metrics into runtimes + ref: 2.4.7 + sha: 7d5b8308c9bcf5e3bf1b87ca0a49f84138191c24 + synced: '2026-10-15' +smallrye-mutiny: + versions: + - converted: true + id: latest + nav: + - page: index + title: Mutiny -- Intuitive Event-Driven Reactive Programming Library for Java + - items: + - page: tutorials/getting-mutiny + title: Getting started with Mutiny + - page: tutorials/hello-mutiny + title: Hello Mutiny! + - page: tutorials/creating-uni-pipelines + title: Creating `Uni` pipelines + - page: tutorials/creating-multi-pipelines + title: Creating `Multi` pipelines + - page: tutorials/observing-events + title: Observing events + - page: tutorials/transforming-items + title: Transforming items + - page: tutorials/transforming-items-asynchronously + title: Transforming items asynchronously + - page: tutorials/handling-failures + title: Handling failures + - page: tutorials/retrying + title: Retrying on failures + - page: tutorials/mutiny-workshop + title: Go further with the Mutiny workshop! + title: Tutorials + - items: + - page: guides/imperative-to-reactive + title: From imperative to reactive + - page: guides/reactive-to-imperative + title: From reactive to imperative + - page: guides/unchecked-exceptions + title: Dealing with checked exceptions + - page: guides/filtering-items + title: Filtering items from Multi + - page: guides/collecting-items + title: Collecting items from Multi + - page: guides/grouping-items + title: Grouping items from Multi + - page: guides/take-skip-items + title: Take/Skip the first or last items + - page: guides/eliminate-duplicates-and-repetitions + title: Eliminate duplicates and repetitions + - page: guides/handling-null + title: How to handle null? + - page: guides/handling-timeouts + title: How to handle timeouts? + - page: guides/delaying-events + title: How to delay events? + - page: guides/pagination + title: How to use paginated APIs? + - page: guides/polling + title: How to use polling? + - page: guides/emission-threads + title: How to change the emission thread? + - page: guides/emit-on-vs-run-subscription-on + title: What is the difference between emitOn and runSubscriptionOn? + - page: guides/completion-stage + title: How to deal with CompletionStage? + - page: guides/rx + title: Using map, flatMap and concatMap + - page: guides/shortcut-methods + title: Shortcut methods + - page: guides/merging-and-concatenating-streams + title: Merging and Concatenating Streams + - page: guides/combining-items + title: Combining items from streams + - page: guides/joining-unis + title: Joining several unis + - page: guides/converters + title: Using other reactive programming libraries + - page: guides/testing + title: How can I write unit / integration tests? + - page: guides/spies + title: Spying on events + - page: guides/custom-operators + title: Can I have custom operators? + - page: guides/dropped-exceptions + title: How to deal with dropped exceptions? + - page: guides/framework-integration + title: How can I integrate Mutiny with my framework? + - page: guides/hot-streams + title: Hot streams + - page: guides/integrate-a-non-reactive-source + title: How can I create a Multi from a non-reactive source? + - page: guides/kotlin + title: Kotlin integration + - page: guides/logging + title: Logging events + - page: guides/context-passing + title: Context passing + - page: guides/replaying-multis + title: Replaying Multis + - page: guides/controlling-demand + title: Controlling the demand + - page: guides/multi-split + title: Splitting a Multi into several Multi + - page: guides/branching + title: How to do branching in a reactive pipeline? + - page: guides/broadcasting-to-multiple-subscribers + title: Broadcasting to multiple subscribers (like server-sent events, websockets, etc) + title: Guides + - items: + - page: reference/migrating-to-mutiny-2 + title: Migrating to Mutiny 2 + - page: reference/why-is-asynchronous-important + title: Why is asynchronous important? + - page: reference/what-is-reactive-programming + title: What is Reactive Programming? + - page: reference/what-makes-mutiny-different + title: What makes Mutiny different? + - page: reference/uni-and-multi + title: Uni and Multi + - page: reference/going-reactive-a-few-pitfalls + title: 'Going reactive: a few pitfalls' + - page: reference/publications + title: Publications + - page: tags-index + title: Tags index + title: Reference + - title: API + url: https://javadoc.io/doc/io.smallrye.reactive/mutiny/latest/index.html + - title: Discussions/Questions + url: https://github.com/smallrye/smallrye-mutiny/discussions + ref: 3.3.0 + sha: 2a6b4f1923304ef93340166ecca65d69704cbd40 + synced: '2026-10-15' + - converted: true + id: '2' + nav: + - page: index + title: Mutiny -- Intuitive Event-Driven Reactive Programming Library for Java + - items: + - page: tutorials/getting-mutiny + title: Getting started with Mutiny + - page: tutorials/hello-mutiny + title: Hello Mutiny! + - page: tutorials/creating-uni-pipelines + title: Creating `Uni` pipelines + - page: tutorials/creating-multi-pipelines + title: Creating `Multi` pipelines + - page: tutorials/observing-events + title: Observing events + - page: tutorials/transforming-items + title: Transforming items + - page: tutorials/transforming-items-asynchronously + title: Transforming items asynchronously + - page: tutorials/handling-failures + title: Handling failures + - page: tutorials/retrying + title: Retrying on failures + - page: tutorials/mutiny-workshop + title: Go further with the Mutiny workshop! + title: Tutorials + - items: + - page: guides/imperative-to-reactive + title: From imperative to reactive + - page: guides/reactive-to-imperative + title: From reactive to imperative + - page: guides/unchecked-exceptions + title: Dealing with checked exceptions + - page: guides/filtering-items + title: Filtering items from Multi + - page: guides/collecting-items + title: Collecting items from Multi + - page: guides/take-skip-items + title: Take/Skip the first or last items + - page: guides/eliminate-duplicates-and-repetitions + title: Eliminate duplicates and repetitions + - page: guides/handling-null + title: How to handle null? + - page: guides/handling-timeouts + title: How to handle timeouts? + - page: guides/delaying-events + title: How to delay events? + - page: guides/pagination + title: How to use paginated APIs? + - page: guides/polling + title: How to use polling? + - page: guides/emission-threads + title: How to change the emission thread? + - page: guides/emit-on-vs-run-subscription-on + title: What is the difference between emitOn and runSubscriptionOn? + - page: guides/completion-stage + title: How to deal with CompletionStage? + - page: guides/rx + title: Using map, flatMap and concatMap + - page: guides/shortcut-methods + title: Shortcut methods + - page: guides/merging-and-concatenating-streams + title: Merging and Concatenating Streams + - page: guides/combining-items + title: Combining items from streams + - page: guides/joining-unis + title: Joining several unis + - page: guides/converters + title: Using other reactive programming libraries + - page: guides/testing + title: How can I write unit / integration tests? + - page: guides/spies + title: Spying on events + - page: guides/custom-operators + title: Can I have custom operators? + - page: guides/dropped-exceptions + title: How to deal with dropped exceptions? + - page: guides/framework-integration + title: How can I integrate Mutiny with my framework? + - page: guides/hot-streams + title: Hot streams + - page: guides/integrate-a-non-reactive-source + title: How can I create a Multi from a non-reactive source? + - page: guides/kotlin + title: Kotlin integration + - page: guides/logging + title: Logging events + - page: guides/context-passing + title: Context passing + - page: guides/replaying-multis + title: Replaying Multis + - page: guides/controlling-demand + title: Controlling the demand + - page: guides/multi-split + title: Splitting a Multi into several Multi + - page: guides/branching + title: How to do branching in a reactive pipeline? + - page: guides/broadcasting-to-multiple-subscribers + title: Broadcasting to multiple subscribers (like server-sent events, websockets, etc) + title: Guides + - items: + - page: reference/migrating-to-mutiny-2 + title: Migrating to Mutiny 2 + - page: reference/why-is-asynchronous-important + title: Why is asynchronous important? + - page: reference/what-is-reactive-programming + title: What is Reactive Programming? + - page: reference/what-makes-mutiny-different + title: What makes Mutiny different? + - page: reference/uni-and-multi + title: Uni and Multi + - page: reference/going-reactive-a-few-pitfalls + title: 'Going reactive: a few pitfalls' + - page: reference/publications + title: Publications + - page: tags-index + title: Tags index + title: Reference + - title: API + url: https://javadoc.io/doc/io.smallrye.reactive/mutiny/latest/index.html + - title: Discussions/Questions + url: https://github.com/smallrye/smallrye-mutiny/discussions + ref: 2.9.5 + sha: null + synced: '2026-10-15' + - converted: true + id: main + nav: + - page: index + title: Mutiny -- Intuitive Event-Driven Reactive Programming Library for Java + - items: + - page: tutorials/getting-mutiny + title: Getting started with Mutiny + - page: tutorials/hello-mutiny + title: Hello Mutiny! + - page: tutorials/creating-uni-pipelines + title: Creating `Uni` pipelines + - page: tutorials/creating-multi-pipelines + title: Creating `Multi` pipelines + - page: tutorials/observing-events + title: Observing events + - page: tutorials/transforming-items + title: Transforming items + - page: tutorials/transforming-items-asynchronously + title: Transforming items asynchronously + - page: tutorials/handling-failures + title: Handling failures + - page: tutorials/retrying + title: Retrying on failures + - page: tutorials/mutiny-workshop + title: Go further with the Mutiny workshop! + title: Tutorials + - items: + - page: guides/imperative-to-reactive + title: From imperative to reactive + - page: guides/reactive-to-imperative + title: From reactive to imperative + - page: guides/unchecked-exceptions + title: Dealing with checked exceptions + - page: guides/filtering-items + title: Filtering items from Multi + - page: guides/collecting-items + title: Collecting items from Multi + - page: guides/grouping-items + title: Grouping items from Multi + - page: guides/take-skip-items + title: Take/Skip the first or last items + - page: guides/eliminate-duplicates-and-repetitions + title: Eliminate duplicates and repetitions + - page: guides/handling-null + title: How to handle null? + - page: guides/handling-timeouts + title: How to handle timeouts? + - page: guides/delaying-events + title: How to delay events? + - page: guides/pagination + title: How to use paginated APIs? + - page: guides/polling + title: How to use polling? + - page: guides/emission-threads + title: How to change the emission thread? + - page: guides/emit-on-vs-run-subscription-on + title: What is the difference between emitOn and runSubscriptionOn? + - page: guides/completion-stage + title: How to deal with CompletionStage? + - page: guides/rx + title: Using map, flatMap and concatMap + - page: guides/shortcut-methods + title: Shortcut methods + - page: guides/merging-and-concatenating-streams + title: Merging and Concatenating Streams + - page: guides/combining-items + title: Combining items from streams + - page: guides/joining-unis + title: Joining several unis + - page: guides/converters + title: Using other reactive programming libraries + - page: guides/testing + title: How can I write unit / integration tests? + - page: guides/spies + title: Spying on events + - page: guides/custom-operators + title: Can I have custom operators? + - page: guides/dropped-exceptions + title: How to deal with dropped exceptions? + - page: guides/framework-integration + title: How can I integrate Mutiny with my framework? + - page: guides/hot-streams + title: Hot streams + - page: guides/integrate-a-non-reactive-source + title: How can I create a Multi from a non-reactive source? + - page: guides/kotlin + title: Kotlin integration + - page: guides/logging + title: Logging events + - page: guides/context-passing + title: Context passing + - page: guides/replaying-multis + title: Replaying Multis + - page: guides/controlling-demand + title: Controlling the demand + - page: guides/multi-split + title: Splitting a Multi into several Multi + - page: guides/branching + title: How to do branching in a reactive pipeline? + - page: guides/broadcasting-to-multiple-subscribers + title: Broadcasting to multiple subscribers (like server-sent events, websockets, etc) + title: Guides + - items: + - page: reference/migrating-to-mutiny-2 + title: Migrating to Mutiny 2 + - page: reference/why-is-asynchronous-important + title: Why is asynchronous important? + - page: reference/what-is-reactive-programming + title: What is Reactive Programming? + - page: reference/what-makes-mutiny-different + title: What makes Mutiny different? + - page: reference/uni-and-multi + title: Uni and Multi + - page: reference/going-reactive-a-few-pitfalls + title: 'Going reactive: a few pitfalls' + - page: reference/publications + title: Publications + - page: tags-index + title: Tags index + title: Reference + - title: API + url: https://javadoc.io/doc/io.smallrye.reactive/mutiny/latest/index.html + - title: Discussions/Questions + url: https://github.com/smallrye/smallrye-mutiny/discussions + ref: main + sha: 1c2005decd7445b83775cd7bc85d527960ee31ad + synced: '2026-10-15' +smallrye-mutiny-vertx-bindings: + versions: + - converted: true + id: latest + nav: + - page: index + title: Overview + - page: getting-started + title: Getting Started + - page: uni-and-multi + title: Uni and Multi + - page: type-mapping + title: Type Mapping + - page: error-handling + title: Error Handling + - page: blocking-and-testing + title: Blocking and Testing + - page: using-the-generator + title: Using the Generator + - page: available-modules + title: Available Modules + - page: migration-guide-3x-4x + title: Upgrading from version 3 to 4 + ref: 4.0.2 + sha: cfbfb52da7562a7fd6775df8e4200544e9a9e10c + synced: '2026-10-15' + - converted: true + id: '3' + nav: + - page: index + title: Overview + - page: api-translation + title: API Translation + ref: 3.23.1 + sha: a6a2e5bf1a5569a918c9a4d9bccf8eadb5ad45ea + synced: '2026-10-15' + - converted: true + id: '2' + nav: + - page: index + title: Overview + - page: api-translation + title: API Translation + ref: 2.30.1 + sha: null + synced: '2026-10-15' + - converted: true + id: main + nav: + - page: index + title: Overview + - page: getting-started + title: Getting Started + - page: uni-and-multi + title: Uni and Multi + - page: type-mapping + title: Type Mapping + - page: error-handling + title: Error Handling + - page: blocking-and-testing + title: Blocking and Testing + - page: using-the-generator + title: Using the Generator + - page: available-modules + title: Available Modules + - page: migration-guide-3x-4x + title: Upgrading from version 3 to 4 + ref: main + sha: a7b3ca6b9b084bfd937d12ba41ec904f8a71a705 + synced: '2026-10-15' +smallrye-mutiny-zero: + versions: + - converted: true + id: latest + nav: + - page: index + title: Overview + - page: quick-start + title: Quick start + - page: flow-adapters + title: JDK Flow from/to Reactive Streams Adapters + - page: vertx-based-reactive-streams-publishers + title: Vert.x-based Reactive Streams Publishers + ref: 1.3.1 + sha: 148562e4fecea2b9ef926bf10093d301af9056e8 + synced: '2026-10-15' + - converted: true + id: '0' + nav: + - page: index + title: Overview + - page: quick-start + title: Quick start + - page: flow-adapters + title: JDK Flow from/to Reactive Streams Adapters + - page: vertx-based-reactive-streams-publishers + title: Vert.x-based Reactive Streams Publishers + ref: 0.5.0 + sha: null + synced: '2026-10-15' + - converted: true + id: main + nav: + - page: index + title: Overview + - page: quick-start + title: Quick start + - page: flow-adapters + title: JDK Flow from/to Reactive Streams Adapters + - page: vertx-based-reactive-streams-publishers + title: Vert.x-based Reactive Streams Publishers + ref: main + sha: 3860259a0e316a76e040a03874412423d638ebef + synced: '2026-10-15' +smallrye-reactive-messaging: + versions: + - converted: true + id: latest + nav: + - page: index + title: Home + - items: + - page: getting-started + title: Getting Started + - items: + - page: concepts/concepts + title: Introduction + - page: concepts/model + title: Development Model + - page: concepts/emitter + title: Emitters and Channel + - page: concepts/connectors + title: Connectors + - page: concepts/contributing-connectors + title: Contributing Connectors + - page: concepts/acknowledgement + title: Acknowledgement + - page: concepts/blocking + title: Blocking Processing + - page: concepts/signatures + title: Method Signatures + - page: concepts/skipping + title: Skipping Messages + - page: concepts/converters + title: Message Converters + - page: concepts/keyed-multi + title: Keyed Streams + - page: concepts/decorators + title: Channel Decorators and Interceptors + - page: concepts/broadcast + title: Broadcast + - page: concepts/merge + title: Merge channels + - page: concepts/incoming-concurrency + title: Incoming Channel Concurrency + - page: concepts/incomings + title: '@Incomings' + - page: concepts/outgoings + title: '@Outgoings' + - page: concepts/testing + title: Testing + - page: concepts/logging + title: Logging + - page: concepts/observability + title: Observability API + - page: concepts/advanced-config + title: Advanced Configuration + - page: concepts/message-context + title: Message Context + - page: concepts/incoming-metadata-injection + title: Metadata Injection + - page: concepts/generic-payloads + title: Generic Payloads + - page: concepts/pausable-channels + title: Pausable Channels + title: Concepts + title: Core + - items: + - page: kafka/kafka + title: Apache Kafka Connector + - page: kafka/receiving-kafka-records + title: Receiving records + - page: kafka/writing-kafka-records + title: Writing records + - page: kafka/health + title: Health Checks + - page: kafka/avro-configuration + title: Using Avro + - page: kafka/protobuf-configuration + title: Using Protobuf + - page: kafka/consumer-rebalance-listener + title: Rebalance Listeners + - page: kafka/kerberos + title: Kerberos authentication + - page: kafka/client-service + title: Accessing the client + - page: kafka/default-configuration + title: Customizing Default Kafka Configuration + - page: kafka/test-companion + title: Test Companion for Kafka + - page: kafka/transactions + title: Kafka Transactions and Exactly-Once Processing + - page: kafka/request-reply + title: Kafka Request/Reply + title: Kafka + - items: + - page: amqp/amqp + title: AMQP 1.0 Connector + - page: amqp/receiving-amqp-messages + title: Receiving messages + - page: amqp/sending-amqp-messages + title: Sending messages + - page: amqp/request-reply + title: AMQP Request/Reply + - page: amqp/cbs-authentication + title: CBS Authentication + - page: amqp/health + title: Health Checks + - page: amqp/client-customization + title: Client Customization + - page: amqp/rabbitmq + title: Using RabbitMQ + title: AMQP 1.0 + - items: + - page: rabbitmq/receiving-messages-from-rabbitmq + title: Receiving messages + - page: rabbitmq/sending-messages-to-rabbitmq + title: Sending messages + - page: rabbitmq/request-reply + title: RabbitMQ Request/Reply + title: RabbitMQ + - items: + - page: pulsar/pulsar + title: Apache Pulsar Connector + - page: pulsar/receiving-pulsar-messages + title: Receiving messages + - page: pulsar/sending-messages-to-pulsar + title: Sending messages + - page: pulsar/schema-configuration + title: Configuring the Schema + - page: pulsar/client-configuration + title: Configuring the Pulsar client + - page: pulsar/health + title: Health Checks + - page: pulsar/client-service + title: Accessing the client + - page: pulsar/transactions + title: Pulsar Transactions and Exactly-Once Processing + title: Pulsar + - items: + - page: camel/camel + title: Apache Camel Connector + - page: camel/receiving-messages-from-camel + title: Receiving messages from Camel + - page: camel/sending-messages-to-camel + title: Sending messages to Camel + - page: camel/camel-processor + title: Implementing Camel processor + - page: camel/using-existing-routes + title: Using existing Camel routes + title: Apache Camel + - items: + - page: jms/jms + title: The JMS connector + - page: jms/receiving-jms-messages + title: Receiving JMS messages + - page: jms/sending-jms-messages + title: Sending JMS messages + - page: jms/advanced-jms + title: Advanced configuration + title: JMS + - items: + - page: mqtt/mqtt + title: MQTT Connector + - page: mqtt/receiving-mqtt-messages + title: Receiving MQTT messages + - page: mqtt/sending-messages-to-mqtt + title: Sending MQTT messages + - page: mqtt/client-customization + title: Customizing the MQTT client + title: MQTT + - items: + - page: sqs/sqs + title: AWS SQS Connector + - page: sqs/receiving-aws-sqs-messages + title: Receiving AWS SQS messages + - page: sqs/sending-aws-sqs-messages + title: Sending AWS SQS messages + title: AWS SQS + - items: + - page: sns/sns + title: AWS SNS Connector + - page: sns/sending-aws-sns-messages + title: Sending AWS SNS messages + title: AWS SNS + ref: 5.0.0-RC1 + sha: null + synced: '2026-10-15' + - converted: true + id: '4' + nav: + - page: index + title: Home + - items: + - page: getting-started + title: Getting Started + - items: + - page: concepts/concepts + title: Introduction + - page: concepts/model + title: Development Model + - page: concepts/emitter + title: Emitters and Channel + - page: concepts/connectors + title: Connectors + - page: concepts/contributing-connectors + title: Contributing Connectors + - page: concepts/acknowledgement + title: Acknowledgement + - page: concepts/blocking + title: Blocking Processing + - page: concepts/signatures + title: Method Signatures + - page: concepts/skipping + title: Skipping Messages + - page: concepts/converters + title: Message Converters + - page: concepts/keyed-multi + title: Keyed Streams + - page: concepts/decorators + title: Channel Decorators and Interceptors + - page: concepts/broadcast + title: Broadcast + - page: concepts/merge + title: Merge channels + - page: concepts/incoming-concurrency + title: Incoming Channel Concurrency + - page: concepts/incomings + title: '@Incomings' + - page: concepts/outgoings + title: '@Outgoings' + - page: concepts/testing + title: Testing + - page: concepts/logging + title: Logging + - page: concepts/observability + title: Observability API + - page: concepts/advanced-config + title: Advanced Configuration + - page: concepts/message-context + title: Message Context + - page: concepts/incoming-metadata-injection + title: Metadata Injection + - page: concepts/generic-payloads + title: Generic Payloads + - page: concepts/pausable-channels + title: Pausable Channels + title: Concepts + title: Core + - items: + - page: kafka/kafka + title: Apache Kafka Connector + - page: kafka/receiving-kafka-records + title: Receiving records + - page: kafka/writing-kafka-records + title: Writing records + - page: kafka/health + title: Health Checks + - page: kafka/avro-configuration + title: Using Avro + - page: kafka/protobuf-configuration + title: Using Protobuf + - page: kafka/consumer-rebalance-listener + title: Rebalance Listeners + - page: kafka/kerberos + title: Kerberos authentication + - page: kafka/client-service + title: Accessing the client + - page: kafka/default-configuration + title: Customizing Default Kafka Configuration + - page: kafka/test-companion + title: Test Companion for Kafka + - page: kafka/transactions + title: Kafka Transactions and Exactly-Once Processing + - page: kafka/request-reply + title: Kafka Request/Reply + title: Kafka + - items: + - page: amqp/amqp + title: AMQP 1.0 Connector + - page: amqp/receiving-amqp-messages + title: Receiving messages + - page: amqp/sending-amqp-messages + title: Sending messages + - page: amqp/request-reply + title: AMQP Request/Reply + - page: amqp/cbs-authentication + title: CBS Authentication + - page: amqp/health + title: Health Checks + - page: amqp/client-customization + title: Client Customization + - page: amqp/rabbitmq + title: Using RabbitMQ + title: AMQP 1.0 + - items: + - page: rabbitmq/rabbitmq + title: RabbitMQ Connector + - page: rabbitmq/receiving-messages-from-rabbitmq + title: Receiving messages + - page: rabbitmq/sending-messages-to-rabbitmq + title: Sending messages + - page: rabbitmq/rabbitmq-health + title: Health Checks + - page: rabbitmq/rabbitmq-client-customization + title: Client Customization + - page: rabbitmq/rabbitmq-cloud + title: Connecting to managed instances + - page: rabbitmq/request-reply + title: RabbitMQ Request/Reply + title: RabbitMQ + - items: + - page: pulsar/pulsar + title: Apache Pulsar Connector + - page: pulsar/receiving-pulsar-messages + title: Receiving messages + - page: pulsar/sending-messages-to-pulsar + title: Sending messages + - page: pulsar/schema-configuration + title: Configuring the Schema + - page: pulsar/client-configuration + title: Configuring the Pulsar client + - page: pulsar/health + title: Health Checks + - page: pulsar/client-service + title: Accessing the client + - page: pulsar/transactions + title: Pulsar Transactions and Exactly-Once Processing + title: Pulsar + - items: + - page: camel/camel + title: Apache Camel Connector + - page: camel/receiving-messages-from-camel + title: Receiving messages from Camel + - page: camel/sending-messages-to-camel + title: Sending messages to Camel + - page: camel/camel-processor + title: Implementing Camel processor + - page: camel/using-existing-routes + title: Using existing Camel routes + title: Apache Camel + - items: + - page: jms/jms + title: The JMS connector + - page: jms/receiving-jms-messages + title: Receiving JMS messages + - page: jms/sending-jms-messages + title: Sending JMS messages + - page: jms/advanced-jms + title: Advanced configuration + title: JMS + - items: + - page: mqtt/mqtt + title: MQTT Connector + - page: mqtt/receiving-mqtt-messages + title: Receiving MQTT messages + - page: mqtt/sending-messages-to-mqtt + title: Sending MQTT messages + - page: mqtt/client-customization + title: Customizing the MQTT client + title: MQTT + - items: + - page: sqs/sqs + title: AWS SQS Connector + - page: sqs/receiving-aws-sqs-messages + title: Receiving AWS SQS messages + - page: sqs/sending-aws-sqs-messages + title: Sending AWS SQS messages + title: AWS SQS + - items: + - page: sns/sns + title: AWS SNS Connector + - page: sns/sending-aws-sns-messages + title: Sending AWS SNS messages + title: AWS SNS + ref: 4.37.1 + sha: 6c698fa841a0dd01ffa85f2a6bc362b66087f709 + synced: '2026-10-15' + - converted: true + id: '3' + nav: + - page: index + title: Home + - items: + - page: getting-started + title: Getting Started + - items: + - page: concepts/concepts + title: Introduction + - page: concepts/model + title: Development Model + - page: concepts/emitter + title: Emitters and Channel + - page: concepts/connectors + title: Connectors + - page: concepts/acknowledgement + title: Acknowledgement + - page: concepts/blocking + title: Blocking Processing + - page: concepts/signatures + title: Method Signatures + - page: concepts/skipping + title: Skipping Messages + - page: concepts/converters + title: Message Converters + - page: concepts/decorators + title: Channel Decorators + - page: concepts/broadcast + title: Broadcast + - page: concepts/merge + title: Merge channels + - page: concepts/incomings + title: '@Incomings' + - page: concepts/testing + title: Testing + - page: concepts/logging + title: Logging + - page: concepts/advanced-config + title: Advanced Configuration + - page: concepts/message-context + title: Message Context + title: Concepts + title: Core + - items: + - page: kafka/kafka + title: Apache Kafka Connector + - page: kafka/receiving-kafka-records + title: Receiving records + - page: kafka/writing-kafka-records + title: Writing records + - page: kafka/health + title: Health Checks + - page: kafka/avro-configuration + title: Using Avro + - page: kafka/protobuf-configuration + title: Using Protobuf + - page: kafka/consumer-rebalance-listener + title: Rebalance Listeners + - page: kafka/kerberos + title: Kerberos authentication + - page: kafka/client-service + title: Accessing the client + - page: kafka/default-configuration + title: Customizing Default Kafka Configuration + - page: kafka/test-companion + title: Test Companion for Kafka + - page: kafka/transactions + title: Kafka Transactions and Exactly-Once Processing + title: Kafka + - items: + - page: amqp/amqp + title: AMQP 1.0 Connector + - page: amqp/receiving-amqp-messages + title: Receiving messages + - page: amqp/sending-amqp-messages + title: Sending messages + - page: amqp/health + title: Health Checks + - page: amqp/client-customization + title: Client Customization + - page: amqp/rabbitmq + title: Using RabbitMQ + title: AMQP 1.0 + - items: + - page: rabbitmq/rabbitmq + title: RabbitMQ Connector + - page: rabbitmq/receiving-messages-from-rabbitmq + title: Receiving messages + - page: rabbitmq/sending-messages-to-rabbitmq + title: Sending messages + - page: rabbitmq/rabbitmq-health + title: Health Checks + - page: rabbitmq/rabbitmq-client-customization + title: Client Customization + - page: rabbitmq/rabbitmq-cloud + title: Connecting to managed instances + title: RabbitMQ + - items: + - page: camel/camel + title: Apache Camel Connector + - page: camel/receiving-messages-from-camel + title: Receiving messages from Camel + - page: camel/sending-messages-to-camel + title: Sending messages to Camel + - page: camel/camel-processor + title: Implementing Camel processor + - page: camel/using-existing-routes + title: Using existing Camel routes + title: Apache Camel + - items: + - page: jms/jms + title: The JMS connector + - page: jms/receiving-jms-messages + title: Receiving JMS messages + - page: jms/sending-jms-messages + title: Sending JMS messages + - page: jms/advanced-jms + title: Advanced configuration + title: JMS + - items: + - page: mqtt/mqtt + title: MQTT Connector + - page: mqtt/receiving-mqtt-messages + title: Receiving MQTT messages + - page: mqtt/sending-messages-to-mqtt + title: Sending MQTT messages + - page: mqtt/client-customization + title: Customizing the MQTT client + title: MQTT + ref: 3.25.0 + sha: null + synced: '2026-10-15' + - converted: true + id: main + nav: + - page: index + title: Home + - items: + - page: getting-started + title: Getting Started + - items: + - page: concepts/concepts + title: Introduction + - page: concepts/model + title: Development Model + - page: concepts/emitter + title: Emitters and Channel + - page: concepts/connectors + title: Connectors + - page: concepts/contributing-connectors + title: Contributing Connectors + - page: concepts/acknowledgement + title: Acknowledgement + - page: concepts/blocking + title: Blocking Processing + - page: concepts/signatures + title: Method Signatures + - page: concepts/skipping + title: Skipping Messages + - page: concepts/converters + title: Message Converters + - page: concepts/keyed-multi + title: Keyed Streams + - page: concepts/decorators + title: Channel Decorators and Interceptors + - page: concepts/broadcast + title: Broadcast + - page: concepts/merge + title: Merge channels + - page: concepts/incoming-concurrency + title: Incoming Channel Concurrency + - page: concepts/incomings + title: '@Incomings' + - page: concepts/outgoings + title: '@Outgoings' + - page: concepts/testing + title: Testing + - page: concepts/logging + title: Logging + - page: concepts/observability + title: Observability API + - page: concepts/advanced-config + title: Advanced Configuration + - page: concepts/message-context + title: Message Context + - page: concepts/incoming-metadata-injection + title: Metadata Injection + - page: concepts/generic-payloads + title: Generic Payloads + - page: concepts/pausable-channels + title: Pausable Channels + title: Concepts + title: Core + - items: + - page: kafka/kafka + title: Apache Kafka Connector + - page: kafka/receiving-kafka-records + title: Receiving records + - page: kafka/writing-kafka-records + title: Writing records + - page: kafka/health + title: Health Checks + - page: kafka/avro-configuration + title: Using Avro + - page: kafka/protobuf-configuration + title: Using Protobuf + - page: kafka/consumer-rebalance-listener + title: Rebalance Listeners + - page: kafka/kerberos + title: Kerberos authentication + - page: kafka/client-service + title: Accessing the client + - page: kafka/default-configuration + title: Customizing Default Kafka Configuration + - page: kafka/test-companion + title: Test Companion for Kafka + - page: kafka/transactions + title: Kafka Transactions and Exactly-Once Processing + - page: kafka/request-reply + title: Kafka Request/Reply + title: Kafka + - items: + - page: amqp/amqp + title: AMQP 1.0 Connector + - page: amqp/receiving-amqp-messages + title: Receiving messages + - page: amqp/sending-amqp-messages + title: Sending messages + - page: amqp/request-reply + title: AMQP Request/Reply + - page: amqp/cbs-authentication + title: CBS Authentication + - page: amqp/health + title: Health Checks + - page: amqp/client-customization + title: Client Customization + - page: amqp/rabbitmq + title: Using RabbitMQ + title: AMQP 1.0 + - items: + - page: rabbitmq/receiving-messages-from-rabbitmq + title: Receiving messages + - page: rabbitmq/sending-messages-to-rabbitmq + title: Sending messages + - page: rabbitmq/request-reply + title: RabbitMQ Request/Reply + title: RabbitMQ + - items: + - page: pulsar/pulsar + title: Apache Pulsar Connector + - page: pulsar/receiving-pulsar-messages + title: Receiving messages + - page: pulsar/sending-messages-to-pulsar + title: Sending messages + - page: pulsar/schema-configuration + title: Configuring the Schema + - page: pulsar/client-configuration + title: Configuring the Pulsar client + - page: pulsar/health + title: Health Checks + - page: pulsar/client-service + title: Accessing the client + - page: pulsar/transactions + title: Pulsar Transactions and Exactly-Once Processing + title: Pulsar + - items: + - page: camel/camel + title: Apache Camel Connector + - page: camel/receiving-messages-from-camel + title: Receiving messages from Camel + - page: camel/sending-messages-to-camel + title: Sending messages to Camel + - page: camel/camel-processor + title: Implementing Camel processor + - page: camel/using-existing-routes + title: Using existing Camel routes + title: Apache Camel + - items: + - page: jms/jms + title: The JMS connector + - page: jms/receiving-jms-messages + title: Receiving JMS messages + - page: jms/sending-jms-messages + title: Sending JMS messages + - page: jms/jms-transactions + title: JMS Transactions + - page: jms/advanced-jms + title: Advanced configuration + title: JMS + - items: + - page: mqtt/mqtt + title: MQTT Connector + - page: mqtt/receiving-mqtt-messages + title: Receiving MQTT messages + - page: mqtt/sending-messages-to-mqtt + title: Sending MQTT messages + - page: mqtt/client-customization + title: Customizing the MQTT client + title: MQTT + - items: + - page: sqs/sqs + title: AWS SQS Connector + - page: sqs/receiving-aws-sqs-messages + title: Receiving AWS SQS messages + - page: sqs/sending-aws-sqs-messages + title: Sending AWS SQS messages + title: AWS SQS + - items: + - page: sns/sns + title: AWS SNS Connector + - page: sns/sending-aws-sns-messages + title: Sending AWS SNS messages + title: AWS SNS + ref: main + sha: null + synced: '2026-10-15' +smallrye-stork: + versions: + - converted: true + id: latest + nav: + - page: index + title: Overview + - page: concepts + title: Concepts + - page: programmatic-api + title: Programmatic API + - page: microprofile-config + title: Microprofile Configuration + - page: springboot-config + title: Spring Boot Configuration + - page: observability + title: Observability + - page: logging + title: Logging + - title: Javadoc + url: https://javadoc.io/doc/io.smallrye.stork/smallrye-stork-api/latest/index.html + - page: quarkus + title: Using Stork with Quarkus + - items: + - page: service-discovery/overview + title: Overview + - page: service-discovery/consul + title: Consul + - page: service-discovery/dns + title: DNS + - page: service-discovery/kubernetes + title: Kubernetes + - page: service-discovery/knative + title: KNative + - page: service-discovery/eureka + title: Eureka + - page: service-discovery/composite + title: Composite + - page: service-discovery/static-list + title: Static List + - page: service-discovery/custom-service-discovery + title: Custom Service Discovery + title: Service Discovery + - items: + - page: load-balancer/overview + title: Overview + - page: load-balancer/round-robin + title: Round Robin + - page: load-balancer/random + title: Random + - page: load-balancer/least-requests + title: Least Requests + - page: load-balancer/least-response-time + title: Least Response Time + - page: load-balancer/power-of-two-choices + title: Power Of Two Choices + - page: load-balancer/sticky + title: Sticky + - page: load-balancer/custom-load-balancer + title: Custom Load Balancer + title: Load-Balancing + - items: + - page: service-registration/overview + title: Overview + - page: service-registration/consul + title: Consul + - page: service-registration/eureka + title: Eureka + - page: service-registration/static-list + title: Static List + - page: service-registration/custom-service-registration + title: Custom Service Registration + title: Service Registration + ref: 3.0.3 + sha: 0def793a36e25c4d2d79c2a08db6f5b50fa28ef1 + synced: '2026-10-15' + - converted: true + id: '2' + nav: + - page: index + title: Overview + - page: concepts + title: Concepts + - page: programmatic-api + title: Programmatic API + - page: microprofile-config + title: Microprofile Configuration + - page: springboot-config + title: Spring Boot Configuration + - page: observability + title: Observability + - title: Javadoc + url: https://javadoc.io/doc/io.smallrye.stork/smallrye-stork-api/latest/index.html + - page: quarkus + title: Using Stork with Quarkus + - items: + - page: service-discovery/overview + title: Overview + - page: service-discovery/consul + title: Consul + - page: service-discovery/dns + title: DNS + - page: service-discovery/kubernetes + title: Kubernetes + - page: service-discovery/knative + title: KNative + - page: service-discovery/eureka + title: Eureka + - page: service-discovery/composite + title: Composite + - page: service-discovery/static-list + title: Static List + - page: service-discovery/custom-service-discovery + title: Custom Service Discovery + title: Service Discovery + - items: + - page: load-balancer/overview + title: Overview + - page: load-balancer/round-robin + title: Round Robin + - page: load-balancer/random + title: Random + - page: load-balancer/least-requests + title: Least Requests + - page: load-balancer/least-response-time + title: Least Response Time + - page: load-balancer/power-of-two-choices + title: Power Of Two Choices + - page: load-balancer/sticky + title: Sticky + - page: load-balancer/custom-load-balancer + title: Custom Load Balancer + title: Load-Balancing + - items: + - page: service-registration/overview + title: Overview + - page: service-registration/consul + title: Consul + - page: service-registration/eureka + title: Eureka + - page: service-registration/static-list + title: Static List + - page: service-registration/custom-service-registration + title: Custom Service Registration + title: Service Registration + ref: 2.7.11 + sha: null + synced: '2026-10-15' + - converted: true + id: '1' + nav: + - page: index + title: Overview + - page: concepts + title: Concepts + - page: programmatic-api + title: Programmatic API + - title: Javadoc + url: https://javadoc.io/doc/io.smallrye.stork/smallrye-stork-api/latest/index.html + - page: quarkus + title: Using Stork with Quarkus + - items: + - page: service-discovery/consul + title: Consul + - page: service-discovery/dns + title: DNS + - page: service-discovery/kubernetes + title: Kubernetes + - page: service-discovery/knative + title: KNative + - page: service-discovery/eureka + title: Eureka + - page: service-discovery/composite + title: Composite + - page: service-discovery/static-list + title: Static List + - page: service-discovery/custom-service-discovery + title: Custom Service Discovery + title: Service Discovery + - items: + - page: load-balancer/round-robin + title: Round Robin + - page: load-balancer/random + title: Random + - page: load-balancer/least-requests + title: Least Requests + - page: load-balancer/response-time + title: Response Time + - page: load-balancer/power-of-two-choices + title: Power Of Two Choices + - page: load-balancer/sticky + title: Sticky + - page: load-balancer/custom-load-balancer + title: Custom Load Balancer + title: Load-Balancing + ref: 1.4.2 + sha: null + synced: '2026-10-15' + - converted: true + id: main + nav: + - page: index + title: Overview + - page: concepts + title: Concepts + - page: programmatic-api + title: Programmatic API + - page: microprofile-config + title: Microprofile Configuration + - page: springboot-config + title: Spring Boot Configuration + - page: observability + title: Observability + - page: logging + title: Logging + - title: Javadoc + url: https://javadoc.io/doc/io.smallrye.stork/smallrye-stork-api/latest/index.html + - page: quarkus + title: Using Stork with Quarkus + - items: + - page: service-discovery/overview + title: Overview + - page: service-discovery/consul + title: Consul + - page: service-discovery/dns + title: DNS + - page: service-discovery/kubernetes + title: Kubernetes + - page: service-discovery/knative + title: KNative + - page: service-discovery/eureka + title: Eureka + - page: service-discovery/composite + title: Composite + - page: service-discovery/static-list + title: Static List + - page: service-discovery/custom-service-discovery + title: Custom Service Discovery + title: Service Discovery + - items: + - page: load-balancer/overview + title: Overview + - page: load-balancer/round-robin + title: Round Robin + - page: load-balancer/random + title: Random + - page: load-balancer/least-requests + title: Least Requests + - page: load-balancer/least-response-time + title: Least Response Time + - page: load-balancer/power-of-two-choices + title: Power Of Two Choices + - page: load-balancer/sticky + title: Sticky + - page: load-balancer/custom-load-balancer + title: Custom Load Balancer + title: Load-Balancing + - items: + - page: service-registration/overview + title: Overview + - page: service-registration/consul + title: Consul + - page: service-registration/eureka + title: Eureka + - page: service-registration/static-list + title: Static List + - page: service-registration/custom-service-registration + title: Custom Service Registration + title: Service Registration + ref: main + sha: 813825bd5db457651b21f001027fe3c2b9315461 + synced: '2026-10-15' diff --git a/docs/antora-playbook.yml b/docs/antora-playbook.yml deleted file mode 100644 index 5b6b2e067..000000000 --- a/docs/antora-playbook.yml +++ /dev/null @@ -1,35 +0,0 @@ -site: - title: SmallRye documentation - start_page: index::index.adoc -content: - sources: - - url: .. # common module containing just the main index - start_path: docs - branches: HEAD - - url: https://github.com/smallrye/smallrye-jwt.git - branches: main - start_path: doc - - url: https://github.com/smallrye/smallrye-metrics.git - branches: [2.4.x, main] - start_path: doc - - url: https://github.com/smallrye/smallrye-fault-tolerance.git - branches: ~ - tags: - - '5*' - - '6*' - - '!*-RC*' - start_path: doc - - url: https://github.com/smallrye/smallrye-health.git - branches: 3.0.x - start_path: doc -ui: - bundle: - url: https://raw.githubusercontent.com/smallrye/smallrye-antora-ui/refs/heads/main/build/ui-bundle.zip - snapshot: true -asciidoc: - extensions: - - asciidoctor-kroki - attributes: - page-pagination: '' - kroki-fetch-diagram: true - kroki-server-url: http://localhost:8000 diff --git a/docs/antora.yml b/docs/antora.yml deleted file mode 100644 index f87c098b1..000000000 --- a/docs/antora.yml +++ /dev/null @@ -1,4 +0,0 @@ -# Descriptor of the common Antora module that contains just a landing page for the whole documentation site -name: index -title: Index -version: index diff --git a/docs/modules/ROOT/pages/index.adoc b/docs/modules/ROOT/pages/index.adoc deleted file mode 100644 index e21a8e03d..000000000 --- a/docs/modules/ROOT/pages/index.adoc +++ /dev/null @@ -1,8 +0,0 @@ -= SmallRye documentation - -Welcome to the SmallRye documentation site. Pick the project you're interested in from the list below. - -* xref:smallrye-jwt:ROOT:index.adoc[SmallRye JWT] -* xref:smallrye-metrics:ROOT:index.adoc[SmallRye Metrics] -* xref:smallrye-fault-tolerance:ROOT:index.adoc[SmallRye Fault Tolerance] -* xref:smallrye-health:ROOT:index.adoc[SmallRye Health] diff --git a/jbang-catalog.json b/jbang-catalog.json new file mode 100644 index 000000000..d534ced7f --- /dev/null +++ b/jbang-catalog.json @@ -0,0 +1,13 @@ +{ + "aliases": { + "docs-preview": { + "script-ref": "tools/docs/Docs.java", + "arguments": ["preview"], + "description": "Preview a SmallRye project's documentation the way smallrye.io shows it: run it in the project's checkout" + }, + "smallrye-docs": { + "script-ref": "tools/docs/Docs.java", + "description": "The tools behind smallrye.io/docs (sync, preview, redirects, ...)" + } + } +} diff --git a/public/css/main.css b/public/css/main.css index 8832025dd..bb6d486d9 100644 --- a/public/css/main.css +++ b/public/css/main.css @@ -37,6 +37,8 @@ --btn-fg: #FFFFFF; --shadow: none; --code-bg: #F1EFE6; + --img-paper: transparent; + --img-paper-pad: 0; --code-fg: #1C1A30; --hl-comment: #6A6880; --hl-keyword: #7A2E8E; @@ -65,6 +67,8 @@ --btn-bg: #6FCBE8; --btn-fg: #14122A; --code-bg: #0E0C20; + --img-paper: #FBFAF6; + --img-paper-pad: 0.75rem; --code-fg: #E6E5F2; --hl-comment: #9895B3; --hl-keyword: #D2A8FF; @@ -87,6 +91,8 @@ --btn-bg: #6FCBE8; --btn-fg: #14122A; --code-bg: #0E0C20; + --img-paper: #FBFAF6; + --img-paper-pad: 0.75rem; --code-fg: #E6E5F2; --hl-comment: #9895B3; --hl-keyword: #D2A8FF; @@ -237,6 +243,8 @@ code, kbd, pre, samp { font-family: var(--font-mono); font-size: 0.9em; } .prose th, .prose td { border: 1px solid var(--line); padding: 0.45rem 0.7rem; text-align: left; vertical-align: top; } .prose th { background: var(--bg-alt); } .prose img { border-radius: var(--radius); } +/* Diagrams are mostly dark lines on a transparent background: in dark mode they sit on light paper */ +.prose .imageblock img { background: var(--img-paper); padding: var(--img-paper-pad); } .prose hr { border: 0; border-top: 1px solid var(--line); margin: 2.5em 0; } /* AsciiDoc blocks */ .prose .title { font-weight: 600; color: var(--fg-muted); font-size: 0.9rem; margin: 1.25em 0 0.4em; } @@ -279,6 +287,116 @@ code, kbd, pre, samp { font-family: var(--font-mono); font-size: 0.9em; } .pagination a { display: inline-flex; align-items: center; min-height: 2.9rem; font-weight: 600; } .pagination .page-number { color: var(--fg-muted); font-size: 0.9rem; font-family: var(--font-mono); } +/* ---- Project documentation (/docs///) ------------------ */ +.visually-hidden { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; } +/* The project and its version picker sit in the header, next to the logo */ +.docs-brand { display: flex; align-items: center; gap: 0.75rem; margin-right: auto; padding-left: 1rem; border-left: 1px solid var(--line); } +.docs-brand-project { font-weight: 700; font-size: 1.1rem; color: var(--fg); text-decoration: none; white-space: nowrap; } +.docs-brand-project:hover { color: var(--accent-ink); } +.docs-version select { font: inherit; font-size: 0.9rem; color: var(--fg); background: var(--bg-card); border: 1px solid var(--line); border-radius: var(--radius); padding: 0.4rem 0.6rem; min-height: 2.6rem; max-width: 14rem; } +@media (max-width: 63.99rem) { + .bar:has(.docs-brand) { flex-wrap: wrap; } + .docs-brand { order: 3; width: 100%; justify-content: space-between; padding: 0 0 0.6rem; border-left: 0; } +} +.docs-layout { display: grid; grid-template-columns: minmax(0, 1fr); gap: 0 2.5rem; padding-top: 1.5rem; padding-bottom: 4rem; max-width: 90rem; } +/* Sidebar: closed by default behind the Contents button; its sections fold */ +.docs-side { margin-bottom: 1rem; } +.docs-nav-button { display: inline-flex; align-items: center; gap: 0.55rem; min-height: 2.75rem; min-width: 2.75rem; padding: 0 0.8rem; font: inherit; font-weight: 600; color: var(--fg); background: var(--bg-card); border: 1px solid var(--line); border-radius: var(--radius); cursor: pointer; } +.docs-nav-button:hover { color: var(--accent-ink); border-color: var(--accent); } +.docs-nav-button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; } +.docs-nav-icon, .docs-nav-icon::before, .docs-nav-icon::after { display: block; width: 1.05rem; height: 2px; background: currentColor; position: relative; content: ""; } +.docs-nav-icon::before { position: absolute; top: -5px; } +.docs-nav-icon::after { position: absolute; top: 5px; } +.docs-nav { display: none; font-size: 0.95rem; margin-top: 0.75rem; } +.docs-nav-open .docs-nav { display: block; } +.docs-nav ul { list-style: none; margin: 0; padding: 0; } +.docs-nav ul ul { margin: 0.1rem 0 0.6rem; } +.docs-nav li a { display: block; padding: 0.3rem 0.6rem; border-radius: var(--radius); color: var(--fg); text-decoration: none; line-height: 1.35; } +.docs-nav li a:hover { background: var(--bg-alt); color: var(--accent-ink); } +.docs-nav li a[aria-current="page"] { background: var(--bg-alt); color: var(--accent-ink); font-weight: 600; box-shadow: inset 3px 0 0 var(--accent); } +.docs-nav-heading { display: flex; align-items: center; gap: 0.4rem; margin: 0.35rem 0 0; padding: 0.45rem 0.6rem; font-family: var(--font-mono); font-size: 0.75rem; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: var(--fg-muted); cursor: pointer; list-style: none; border-radius: var(--radius); } +.docs-nav-heading::-webkit-details-marker { display: none; } +.docs-nav-heading::before { content: ""; width: 0.4rem; height: 0.4rem; border-right: 2px solid currentColor; border-bottom: 2px solid currentColor; transform: rotate(-45deg); transition: transform 0.15s; flex: none; } +.docs-nav details[open] > .docs-nav-heading::before { transform: rotate(45deg); } +.docs-nav-heading:hover { color: var(--accent-ink); background: var(--bg-alt); } +.docs-nav-heading:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; } +.docs-main { min-width: 0; } +.docs-main { max-width: 68rem; } +/* Text keeps a readable line length; code, tables, diagrams and cards use the whole column */ +.docs-main :is(.paragraph, .ulist, .olist, .dlist, .colist, .admonitionblock, .quoteblock, .verseblock, .sidebarblock, .docs-source, h1, h2, h3, h4, h5, h6) { max-width: 50rem; } +.docs-main :is(.cards, .tableblock, .exampleblock) .paragraph { max-width: none; } +.docs-main .lead p { font-size: 1.2rem; color: var(--fg-muted); margin-top: 0.25rem; } +.docs-main h1 { margin-top: 0.5rem; } +.docs-notice { border: 1px solid var(--line); border-left: 3px solid var(--accent); background: var(--bg-alt); border-radius: 0 var(--radius) var(--radius) 0; padding: 0.7rem 1rem; font-size: 0.95rem; } +.docs-source { margin-top: 3rem; padding-top: 1.25rem; border-top: 1px solid var(--line); font-size: 0.9rem; } +.docs-toc { display: none; font-size: 0.875rem; } +.docs-toc .toc-title { display: flex; align-items: center; gap: 0.4rem; cursor: pointer; list-style: none; } +.docs-toc .toc-title::-webkit-details-marker { display: none; } +.docs-toc .toc-title::before { content: ""; width: 0.4rem; height: 0.4rem; border-right: 2px solid currentColor; border-bottom: 2px solid currentColor; transform: rotate(-45deg); transition: transform 0.15s; flex: none; } +.docs-toc details[open] > .toc-title::before { transform: rotate(45deg); } +.docs-toc .toc-title:hover { color: var(--accent-ink); } +.docs-toc-closed .docs-toc details > :not(summary) { display: none; } +.docs-toc ul { list-style: none; margin: 0; padding: 0; } +.docs-toc ul ul { padding-left: 0.9rem; } +.docs-toc li { margin: 0.25rem 0; } +.docs-toc a { color: var(--fg-muted); text-decoration: none; } +.docs-toc a:hover { color: var(--accent-ink); text-decoration: underline; } +@media (min-width: 60rem) { + .docs-layout { grid-template-columns: auto minmax(0, 68rem); justify-content: start; } + .docs-side { grid-column: 1; grid-row: 1 / span 2; position: sticky; top: 1rem; align-self: start; max-height: calc(100vh - 2rem); overflow-y: auto; margin: 0; padding: 2px 0.5rem 2px 2px; } + .docs-nav-open .docs-side { width: 15rem; } + :root:not(.docs-nav-open) .docs-nav-label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } + :root:not(.docs-nav-open) .docs-nav-button { padding: 0; justify-content: center; } + .docs-main { grid-column: 2; } +} +@media (min-width: 78rem) { + .docs-layout { grid-template-columns: auto minmax(0, 68rem) 13rem; } + .docs-toc { display: block; grid-column: 3; grid-row: 1; position: sticky; top: 1rem; align-self: start; max-height: calc(100vh - 2rem); overflow-y: auto; } +} +.prose .sect1 h2 > a.anchor, .prose h3 > a.anchor, .prose h4 > a.anchor { float: left; margin-left: -1.2em; padding-right: 0.3em; text-decoration: none; visibility: hidden; color: var(--fg-muted); } +.prose h2:hover > a.anchor, .prose h3:hover > a.anchor, .prose h4:hover > a.anchor { visibility: visible; } +.prose h2 > a.anchor::before, .prose h3 > a.anchor::before, .prose h4 > a.anchor::before { content: "#"; } +/* Tabs: an example block with the "tabs" role holds a labelled list; docs code makes it tabs. */ +.prose .exampleblock { margin: 1.5em 0; } +.prose .tabs .tab-list { display: flex; flex-wrap: wrap; gap: 0.25rem; border-bottom: 1px solid var(--line); margin-bottom: 0.75rem; } +.prose .tabs .tab-list button { font: inherit; font-size: 0.9rem; font-weight: 600; background: none; border: 0; border-bottom: 2px solid transparent; color: var(--fg-muted); padding: 0.5rem 0.8rem; min-height: 2.75rem; cursor: pointer; } +.prose .tabs .tab-list button[aria-selected="true"] { color: var(--accent-ink); border-bottom-color: var(--accent); } +.prose .tabs [role="tabpanel"][hidden] { display: none; } +/* Cards: an open block with the "cards" role lays out the sidebar blocks it holds as a grid; + a link with the "button" role is a call to action. */ +.prose .cards > .content { display: grid; grid-template-columns: minmax(0, 1fr); gap: 1rem; margin: 1.75em 0; } +@media (min-width: 40rem) { + .prose .cards > .content { grid-template-columns: repeat(2, minmax(0, 1fr)); } +} +.prose .cards .sidebarblock { margin: 0; background: var(--bg-card); border: 1px solid var(--line); border-radius: var(--radius); padding: 1rem 1.15rem; } +.prose .cards .sidebarblock > .content > :first-child { margin-top: 0; } +.prose .cards .sidebarblock > .content > :last-child { margin-bottom: 0; } +.prose .cards .sidebarblock > .content > .title { font-weight: 700; margin-bottom: 0.4em; } +.prose .cards h2.discrete, .prose .cards h3.discrete { font-size: 1.1rem; margin: 0 0 0.5em; padding-top: 0; } +.prose .cards p { margin: 0.5em 0; } +.prose a.button { display: inline-flex; align-items: center; min-height: 2.75rem; padding: 0.55rem 1.2rem; border-radius: var(--radius); font-weight: 600; text-decoration: none; background: var(--btn-bg); color: var(--btn-fg); } +.prose a.button:hover { filter: brightness(1.15); } +.prose details { margin: 1.5em 0; border: 1px solid var(--line); border-radius: var(--radius); padding: 0.6rem 1rem; } +.prose details > summary { cursor: pointer; font-weight: 600; } +/* A version's start page lists its documentation: a card per section */ +.docs-contents { margin-top: 3rem; } +.docs-contents-grid { columns: 2 20rem; column-gap: 1rem; } +.docs-contents-card { break-inside: avoid; margin: 0 0 1rem; padding: 1rem 1.15rem; background: var(--bg-card); border: 1px solid var(--line); border-radius: var(--radius); } +.docs-contents-card:not(:has(li)) { display: none; } +.docs-contents-card h3 { margin: 0 0 0.5rem; font-size: 1.05rem; } +.docs-contents-card ul { list-style: none; margin: 0; padding: 0; } +.docs-contents-card li { margin: 0.2rem 0; } +.docs-contents-card a { text-decoration: none; } +.docs-contents-card a:hover { text-decoration: underline; } +.docs-contents-section > span { display: block; margin: 0.6rem 0 0.2rem; font-family: var(--font-mono); font-size: 0.75rem; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: var(--fg-muted); } +.docs-contents-section > ul { padding-left: 0.2rem; } +/* Docs hub */ +.docs-hub { list-style: none; margin: 0 0 2.5rem; padding: 0; border-top: 1px solid var(--line); } +.docs-hub li { padding: 1rem 0; border-bottom: 1px solid var(--line); } +.docs-hub h3 { margin: 0 0 0.3rem; font-size: 1.15rem; } +.docs-hub p { margin: 0; color: var(--fg-muted); } +.docs-hub .docs-hub-versions { margin-top: 0.4rem; font-size: 0.9rem; font-family: var(--font-mono); } + /* ---- Footer -------------------------------------------------------------- */ .site-footer { background: var(--primary-deep); color: #CFCDE3; font-size: 0.95rem; margin-top: 0; } .site-footer a { color: #ECEBF6; } diff --git a/templates/layouts/doc.html b/templates/layouts/doc.html new file mode 100644 index 000000000..e34ab1281 --- /dev/null +++ b/templates/layouts/doc.html @@ -0,0 +1,71 @@ +--- +layout: default +--- +{@io.quarkiverse.roq.frontmatter.runtime.model.Page page} +{@io.quarkiverse.roq.frontmatter.runtime.model.Site site} +{! A page of a project's documentation: content/docs///.adoc, synced from + the project's repository by tools/docs (sync). The project comes from data/docs.yml, its + versions and navigation from data/docversions.yml. !} +{#head} +{#bundle /} +{! The sidebar starts closed and the page's contents open; each stays the way the reader left it. !} + + +{/head} +{#let key=page.url.path.split('/').get(2) version=page.url.path.split('/').get(3)} +{#let base=site.url('docs/' + key + '/' + version + '/') current=page.url.path.replace('/docs/' + key + '/' + version + '/', '').replaceAll('/$', '')} +{#for p in cdi:docs.getJsonArray('projects')}{#if p.getString('key') == key} +{#let versions=cdi:docversions.getJsonObject(key).getJsonArray('versions')} +
+ +
+ {#if version == 'main'} +

This is the documentation of the main branch: it may describe changes that are not released yet. {#if versions.size > 1}Read the latest release’s documentation.{/if}

+ {#else if p.containsKey('archived')} +

{=p.getString('name')} is archived: this documentation is no longer updated.

+ {#else if version != 'latest'} +

This is the documentation of an older release line. Read the latest release’s documentation.

+ {/if} + {! The start page's title only repeats the project named in the header: kept for screen readers and search engines !} + {=page.title} + {#insert /} + {#if !current}{#for v in versions}{#if v.getString('id') == version} + {#include partials/docs-contents items=v.getJsonArray('nav') base=base /} + {/if}{/for}{/if} + {#for v in versions}{#if v.getString('id') == version} +

+ {#if version == 'main' && !v.getBoolean('converted')} + Edit this page on GitHub + {#else if !v.getBoolean('converted')} + View the source of this page + {/if} +

+ {/if}{/for} +
+ {#if page.toc.size >= 2} + + {/if} +
+{/let} +{/if}{/for} +{/let} +{/let} diff --git a/templates/partials/docs-brand.html b/templates/partials/docs-brand.html new file mode 100644 index 000000000..789be969d --- /dev/null +++ b/templates/partials/docs-brand.html @@ -0,0 +1,22 @@ +{@io.quarkiverse.roq.frontmatter.runtime.model.Site site} +{@io.quarkiverse.roq.frontmatter.runtime.model.Page page} +{! On a project's documentation page (/docs///...): the project and the version + picker, in the header next to the logo. !} +{#let key=page.url.path.split('/').get(2) version=page.url.path.split('/').get(3)} +{#if cdi:docversions.containsKey(key)} +{#for p in cdi:docs.getJsonArray('projects')}{#if p.getString('key') == key} +
+ {=p.getString('name').replace('SmallRye ', '')} + +
+{/if}{/for} +{/if} +{/let} diff --git a/templates/partials/docs-contents-list.html b/templates/partials/docs-contents-list.html new file mode 100644 index 000000000..88863d96d --- /dev/null +++ b/templates/partials/docs-contents-list.html @@ -0,0 +1,14 @@ +{! The pages of a section on a start page; sections inside it recurse under their titles. !} +
    + {#for item in items} + {#if item.containsKey('items')} +
  • {=item.getString('title')} + {#include partials/docs-contents-list items=item.getJsonArray('items') base=base /} +
  • + {#else if item.containsKey('url')} + {#if item.getString('url').startsWith('http')}
  • {=item.getString('title')}
  • {/if} + {#else if item.containsKey('page')} +
  • {=item.getString('title')}
  • + {/if} + {/for} +
diff --git a/templates/partials/docs-contents.html b/templates/partials/docs-contents.html new file mode 100644 index 000000000..48060dd94 --- /dev/null +++ b/templates/partials/docs-contents.html @@ -0,0 +1,21 @@ +{! A version's start page lists the documentation, from its navigation: the pages outside a section, + then a card per section (sections inside it listed under their titles). items: the nav. !} +
+

Contents

+
+
+
    + {#for item in items}{#if !item.containsKey('items') && item.getString('page') != 'index'} + {#if item.containsKey('url')}{#if item.getString('url').startsWith('http')}
  • {=item.getString('title')}
  • {/if} + {#else if item.containsKey('page')}
  • {=item.getString('title')}
  • {/if} + {/if}{/for} +
+
+ {#for item in items}{#if item.containsKey('items')} +
+

{=item.getString('title')}

+ {#include partials/docs-contents-list items=item.getJsonArray('items') base=base /} +
+ {/if}{/for} +
+
diff --git a/templates/partials/docs-nav.html b/templates/partials/docs-nav.html new file mode 100644 index 000000000..5bdcc89cc --- /dev/null +++ b/templates/partials/docs-nav.html @@ -0,0 +1,21 @@ +{! One level of a documentation sidebar; sections recurse, folded (the page's own section is + unfolded by docs code). items: [{title, page} | {title, items} | {title, url}]. + current: the page's path inside its version, '' for the start page. !} +
    + {#for item in items} + {#if item.containsKey('items')} +
  • +
    + {=item.getString('title')} + {#include partials/docs-nav items=item.getJsonArray('items') base=base current=current /} +
    +
  • + {#else if item.containsKey('url')} + {#if item.getString('url').startsWith('http')}
  • {=item.getString('title')}
  • {/if} + {#else if item.containsKey('page')} + {#let target=(item.getString('page') == 'index' ? '' : item.getString('page'))} +
  • {=item.getString('title')}
  • + {/let} + {/if} + {/for} +
diff --git a/templates/partials/footer.html b/templates/partials/footer.html index 8dcd612c7..b198b562c 100644 --- a/templates/partials/footer.html +++ b/templates/partials/footer.html @@ -13,7 +13,7 @@
  • Projects
  • Community
  • Blog
  • -
  • Documentation
  • +
  • Documentation