Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LRA testing feature #8469

Merged
merged 2 commits into from
Oct 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions bom/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -781,6 +781,16 @@
<artifactId>helidon-lra-coordinator-narayana-client</artifactId>
<version>${helidon.version}</version>
</dependency>
<dependency>
<groupId>io.helidon.lra</groupId>
<artifactId>helidon-lra-coordinator-server</artifactId>
<version>${helidon.version}</version>
</dependency>
<dependency>
<groupId>io.helidon.microprofile.lra</groupId>
<artifactId>helidon-microprofile-lra-testing</artifactId>
<version>${helidon.version}</version>
</dependency>

<!-- integrations -->
<dependency>
Expand Down
15 changes: 15 additions & 0 deletions docs/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,21 @@
<artifactId>postgresql</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.helidon.microprofile.lra</groupId>
<artifactId>helidon-microprofile-lra-testing</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-all</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.helidon.microprofile.testing</groupId>
<artifactId>helidon-microprofile-testing-junit5</artifactId>
<optional>true</optional>
</dependency>
</dependencies>

<build>
Expand Down
126 changes: 125 additions & 1 deletion docs/src/main/asciidoc/mp/lra.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
:spec-version: 1.0-RC3
:spec-name: MicroProfile {feature-name} specification
:javadoc-link: https://download.eclipse.org/microprofile/microprofile-lra-{spec-version}/apidocs/org/eclipse/microprofile/lra/annotation/
:microtx-link: https://docs.oracle.com/en/database/oracle/transaction-manager-for-microservices/index.html
:rootdir: {docdir}/..

include::{rootdir}/includes/mp.adoc[]
Expand All @@ -36,8 +37,10 @@ include::{rootdir}/includes/mp.adoc[]
* <<API, API>>
* <<Configuration, Configuration>>
* <<Examples, Examples>>
* <<Testing, Testing>>
* <<Additional Information, Additional Information>>
** <<Coordinator, Coordinator>>
** <<MicroTx LRA Coordinator, MicroTx LRA Coordinator>>
** <<Helidon LRA Coordinator, Helidon LRA Coordinator>>
** <<Narayana, Narayana>>
* <<Reference, Reference>>
Expand Down Expand Up @@ -351,6 +354,73 @@ include::{sourcedir}/mp/LraSnippets.java[tag=snippet_13, indent=0]
<5> Method which will be called by coordinator when LRA is completed
<6> Method which will be called by coordinator when LRA is canceled

== Testing
Testing of JAX-RS resources with LRA can be challenging as LRA participant running in parallel with the test is needed.

Helidon provides test coordinator which can be started automatically with additional socket on a random port within your
own Helidon application. You only need one extra test dependency to enable test coordinator in your xref:testing/testing.adoc[@HelidonTest].

[source, xml]
.Dependency
----
<dependency>
<groupId>io.helidon.microprofile.lra</groupId>
<artifactId>helidon-microprofile-lra-testing</artifactId>
<scope>test</scope>
</dependency>
----

Considering that you have LRA enabled JAX-RS resource you want to test.

[source, java]
.Example JAX-RS resource with LRA.
----
include::{sourcedir}/mp/LraSnippets.java[tag=snippet_14, indent=0]
----

Helidon test with enabled CDI discovery can look like this.

[source, java]
.HelidonTest with LRA test support.
----
include::{sourcedir}/mp/LraSnippets.java[tag=snippet_15, indent=0]
----
<1> Resource is discovered automatically
<2> Test coordinator needs to be added manually
<3> Injecting test coordinator to access state of LRA managed by coordinator mid-test
<4> Retrieving LRA managed by coordinator by LraId
<5> Asserting LRA state in coordinator

LRA testing feature has the following default configuration:

* port: `0` - coordinator is started on random port(Helidon LRA participant is capable to discover test coordinator automatically)
* bind-address: `localhost` - bind address of the coordinator
* helidon.lra.coordinator.persistence: `false` - LRAs managed by test coordinator are not persisted
* helidon.lra.participant.use-build-time-index: `false` - Participant annotation inspection ignores Jandex index files created in build time, it helps to avoid issues with additional test resources

Testing LRA coordinator is started on additional named socket `test-lra-coordinator` configured with default index `500`.
Default index can be changed with system property `helidon.lra.coordinator.test-socket.index`.

Example: `-Dhelidon.lra.coordinator.test-socket.index=20`.

[source, java]
.HelidonTest override LRA test feature default settings.
----
include::{sourcedir}/mp/LraSnippets.java[tag=snippet_16, indent=0]
----
<1> Start test LRA coordinator always on the same port 8070(default is random port)
<2> Test LRA coordinator socket bind address (default is localhost)
<3> Persist LRA managed by coordinator(default is false)
<4> Use build time Jandex index(default is false)

When CDI bean auto-discovery is not desired, LRA and Config CDI extensions needs to be added manually.

[source, java]
.HelidonTest setup with disabled discovery.
----
include::{sourcedir}/mp/LraSnippets.java[tag=snippet_17, indent=0]
----

== Additional Information

=== Coordinator
Expand All @@ -359,12 +429,65 @@ the participants when the LRA transaction gets cancelled or completes (in case i
In addition, participant also keeps track of timeouts, retries participant calls, and assigns LRA ids.

.Helidon LRA supports following coordinators
* {microtx-link}[MicroTx LRA coordinator]
* Helidon LRA coordinator
* https://narayana.io/lra[Narayana coordinator].

=== MicroTx LRA Coordinator
Oracle Transaction Manager for Microservices - {microtx-link}[MicroTx] is an enterprise grade transaction manager for microservices,
among other it manages LRA transactions and is compatible with Narayana LRA clients.

MicroTx LRA coordinator is compatible with Narayana clients when `narayanaLraCompatibilityMode` is on,
you need to add another dependency to enable Narayana client:
[source,xml]
.Dependency needed for using Helidon LRA with Narayana compatible coordinator
----
<dependency>
<groupId>io.helidon.lra</groupId>
<artifactId>helidon-lra-coordinator-narayana-client</artifactId>
</dependency>
----

[source, bash]
.Run MicroTx in Docker
----
docker container run --name otmm -v "$(pwd)":/app/config \
-w /app/config -p 8080:8080/tcp --env CONFIG_FILE=tcs.yaml \
--add-host host.docker.internal:host-gateway -d tmm:<version>
----

To use MicroTx with Helidon LRA participant, `narayanaLraCompatibilityMode` needs to be enabled.

[source, yaml]
.Configure MicroTx for development
----
tmmAppName: tcs
tmmConfiguration:
listenAddr: 0.0.0.0:8080
internalAddr: http://host.docker.internal:8080
externalUrl: http://lra-coordinator.acme.com:8080
xaCoordinator:
enabled: false
lraCoordinator:
enabled: true
tccCoordinator:
enabled: false
storage:
type: memory
authentication:
enabled: false
authorization:
enabled: false
serveTLS:
enabled: false
narayanaLraCompatibilityMode:
enabled: true #<1>
----
<1> Enable Narayana compatibility mode

=== Helidon LRA Coordinator

CAUTION: Experimental tool, usage in production is not advised.
CAUTION: Test tool, usage in production is not advised.

[source,bash]
.Build and run Helidon LRA coordinator
Expand Down Expand Up @@ -417,3 +540,4 @@ with port `8070` defined in the snippet above you need to configure your Helidon
* {microprofile-lra-spec-url}[{spec-name}]
* https://download.eclipse.org/microprofile/microprofile-lra-{spec-version}/apidocs/org/eclipse/microprofile/lra/[Microprofile LRA JavaDoc]
* https://helidon.io/docs/v4/apidocs/io.helidon.lra.coordinator.client/module-summary.html[Helidon LRA Client JavaDoc]
* {microtx-link}[MicroTx - Oracle Transaction Manager for Microservices]
111 changes: 111 additions & 0 deletions docs/src/main/java/io/helidon/docs/mp/LraSnippets.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,33 @@

import java.net.URI;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;

import io.helidon.lra.coordinator.Lra;
import io.helidon.microprofile.config.ConfigCdiExtension;
import io.helidon.microprofile.lra.LraCdiExtension;
import io.helidon.microprofile.testing.junit5.AddBean;
import io.helidon.microprofile.testing.junit5.AddConfig;
import io.helidon.microprofile.testing.junit5.AddExtension;
import io.helidon.microprofile.testing.junit5.AddJaxRs;
import io.helidon.microprofile.testing.junit5.DisableDiscovery;
import io.helidon.microprofile.testing.junit5.HelidonTest;
import io.helidon.microprofile.testing.lra.TestLraCoordinator;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.HeaderParam;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.client.Entity;
import jakarta.ws.rs.client.WebTarget;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.lra.LRAResponse;
import org.eclipse.microprofile.lra.annotation.AfterLRA;
Expand All @@ -36,10 +55,15 @@
import org.eclipse.microprofile.lra.annotation.Status;
import org.eclipse.microprofile.lra.annotation.ws.rs.LRA;
import org.eclipse.microprofile.lra.annotation.ws.rs.Leave;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test;

import static org.eclipse.microprofile.lra.annotation.ws.rs.LRA.LRA_HTTP_CONTEXT_HEADER;
import static org.eclipse.microprofile.lra.annotation.ws.rs.LRA.LRA_HTTP_ENDED_CONTEXT_HEADER;
import static org.eclipse.microprofile.lra.annotation.ws.rs.LRA.LRA_HTTP_PARENT_CONTEXT_HEADER;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.is;

@SuppressWarnings("ALL")
class LraSnippets {
Expand Down Expand Up @@ -218,4 +242,91 @@ public Response compensateExample(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) URI lraI
// end::snippet_13[]
}

// tag::snippet_14[]
@ApplicationScoped
@Path("/test")
public class WithdrawResource {

private final List<String> completedLras = new CopyOnWriteArrayList<>();
private final List<String> cancelledLras = new CopyOnWriteArrayList<>();

@PUT
@Path("/withdraw")
@LRA(LRA.Type.REQUIRES_NEW)
public Response withdraw(@HeaderParam(LRA.LRA_HTTP_CONTEXT_HEADER) Optional<URI> lraId, String content) {
if ("BOOM".equals(content)) {
throw new IllegalArgumentException("BOOM");
}
return Response.ok().build();
}

@Complete
public void complete(URI lraId) {
completedLras.add(lraId.toString());
}

@Compensate
public void rollback(URI lraId) {
cancelledLras.add(lraId.toString());
}

public List<String> getCompletedLras() {
return completedLras;
}
}
// end::snippet_14[]

// tag::snippet_15[]
@HelidonTest
//@AddBean(WithdrawResource.class) //<1>
@AddBean(TestLraCoordinator.class) //<2>
public class LraTest {

@Inject
private WithdrawResource withdrawTestResource;

@Inject
private TestLraCoordinator coordinator; //<3>

@Inject
private WebTarget target;

@Test
public void testLraComplete() {
try (Response res = target
.path("/test/withdraw")
.request()
.put(Entity.entity("test", MediaType.TEXT_PLAIN_TYPE))) {
assertThat(res.getStatus(), is(200));
String lraId = res.getHeaderString(LRA.LRA_HTTP_CONTEXT_HEADER);
Lra lra = coordinator.lra(lraId); //<4>
assertThat(lra.status(), is(LRAStatus.Closed)); //<5>
assertThat(withdrawTestResource.getCompletedLras(), contains(lraId));
}
}
}
// end::snippet_15[]

// tag::snippet_16[]
@HelidonTest
@AddBean(TestLraCoordinator.class)
@AddConfig(key = "server.sockets.500.port", value = "8070") //<1>
@AddConfig(key = "server.sockets.500.bind-address", value = "custom.bind.name") //<2>
@AddConfig(key = "helidon.lra.coordinator.persistence", value = "true") //<3>
@AddConfig(key = "helidon.lra.participant.use-build-time-index", value = "true") //<4>
public class LraCustomConfigTest {
}
// end::snippet_16[]

// tag::snippet_17[]
@HelidonTest
@DisableDiscovery
@AddJaxRs
@AddBean(TestLraCoordinator.class)
@AddExtension(LraCdiExtension.class)
@AddExtension(ConfigCdiExtension.class)
@AddBean(WithdrawResource.class)
public class LraNoDiscoveryTest {
}
// end::snippet_17[]
}
Loading