Skip to content

Commit

Permalink
docs: documenting ComponentClient
Browse files Browse the repository at this point in the history
  • Loading branch information
aludwiko committed Aug 1, 2023
1 parent c34fd59 commit 35d767f
Show file tree
Hide file tree
Showing 10 changed files with 177 additions and 50 deletions.
2 changes: 1 addition & 1 deletion docs/src/modules/java/nav.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
*** xref:java:actions.adoc[Actions]
*** xref:java:actions-publishing-subscribing.adoc[Publishing and Subscribing]
*** xref:java:timers.adoc[Timers]
*** xref:java:call-another-service.adoc[Calling other services]
*** xref:java:component-and-service-calls.adoc[Component and Service Calls]
*** xref:java:failures-and-errors.adoc[Failures and Errors]
*** xref:java:access-control.adoc[Using ACLs]
*** xref:java:using-jwts.adoc[Using JWTs]
36 changes: 33 additions & 3 deletions docs/src/modules/java/pages/actions.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ include::example$java-spring-valueentity-shopping-cart/src/main/java/com/example
<5> For allowed requests, use `componentClient` to get a deferred call to the entity.
<6> The `deferredCall` is then used with `effects().forward()` to forward the request to the entity.

NOTE: NOTE: You might be wondering what the `componentClient` is about. For now, think of it as a lightweight, type safe, HTTP client allowing you to reach out to other Kalix services. All details can be found at xref:call-another-service.adoc[] chapter.
NOTE: NOTE: You might be wondering what the `componentClient` is about. For now, think of it as a lightweight, type safe, HTTP client allowing you to reach out to other Kalix services. All details can be found at xref:component-and-service-calls.adoc[] chapter.

=== Forwarding Headers

Expand Down Expand Up @@ -247,9 +247,39 @@ Calling an action method through the test kit gives us back an link:{attachments
* `getError()` returns the error description when `effects().error()` was returned to signal an error.
* `getForward()` returns details about what message was forwarded and where the call was forwarded (since it is a unit test the forward is not actually executed).



ifdef::todo[TODO: add links to before and after]

NOTE: The side effects of an Action can NOT be tested in isolation at the moment.

=== Integration tests

Actions (like any other Kalix component) can be verified with integration tests. The https://docs.spring.io/spring-framework/reference/web/webflux-webclient.html[Spring WebClient] utility can be used to run any HTTP call to test Kalix components.

[.tabset]
Java::
+
.src/it/java/com/example/fibonacci/FibonacciActionIntegrationTest.java
[source,java]
----
include::example$java-spring-fibonacci-action/src/it/java/com/example/fibonacci/FibonacciActionIntegrationTest.java[tag=testing-action]
----
<1> Mark the test as a Spring integration tests.
<2> Set up the Kalix infrastructure by extending `KalixIntegrationTestKitSupport`.
<3> Use `WebClient` to call the Action component endpoint.

In cases where detailed assertions on the HTTP response are not required, the Kalix xref:component-and-service-calls.adoc#_component_client[ComponentClient] can be used in integration tests.


[.tabset]
Java::
+
.src/it/java/com/example/fibonacci/FibonacciActionComponentClientIntegrationTest.java
[source,java]
----
include::example$java-spring-fibonacci-action/src/it/java/com/example/fibonacci/FibonacciActionComponentClientIntegrationTest.java[tag=testing-action]
----
<1> Use the `ComponentClient` to call the Action component endpoint.
<2> Transform the `DeferredCall` to a `CompletionStage` and wait for the response.


NOTE: The integration tests in samples are under in a specific project profile `it` and can be run using `mvn verify -Pit`.
Original file line number Diff line number Diff line change
@@ -1,6 +1,28 @@
= Calling other services
:page-aliases: spring:call-another-service.adoc
= Component and Service Calls
:page-aliases: spring:component-and-service-calls.adoc

== Kalix components

Since Kalix is an auto-scaling solution, components can be distributed across many nodes within the same service. Kalix doesn’t hide this fact and makes it explicit. That's why calling Kalix components is done via HTTP/gRPC `DeferredCall` calls. A `DeferredCall` is just an instruction on how to call a given component, which can be optimized by the Kalix engine (see xref:java:actions.adoc#_forwarding_commands[forwarding]). Sometimes it's necessary to transform the `DeferredCall` into the `CompletionStage` to combine many different component calls and build a single xref:java:actions.adoc#_composing_calls[asynchronous reply].

=== Component Client

The Kalix `ComponentClient` is a utility for creating deferred calls in a type-safe way. You don't have to remember what the endpoint path is or which HTTP method should be selected for the call. Constructing the call is a matter of:

* selecting the component type,
* choosing the endpoint, with a Java method reference,
* passing in parameters, if expected.

[source,java,indent=0]
.src/main/java/com/example/fibonacci/LimitedFibonacciAction.java
----
include::java:example$java-spring-fibonacci-action/src/main/java/com/example/fibonacci/LimitedFibonacciAction.java[tag=component-client]
----
<1> Select Action component.
<2> Select endpoint method reference.
<3> Pass in endpoint method arguments.

NOTE: NOTE: Calling endpoints that return a stream response like `Flux` is not supported currently by the `ComponentClient`.

== Kalix services

Expand All @@ -17,8 +39,6 @@ In our delegating service implementation:
----
include::java:example$java-spring-doc-snippets/src/main/java/com/example/callanotherservice/DelegatingServiceAction.java[tag=delegating-action]
----


<1> Let the `WebClientProvider` be injected into the Action with constructor injection.
<2> Use the `WebClientProvider` to build a `WebClient` for the *counter* service.
<3> Use the `WebClient` to make a REST call to the *counter* service.
Expand Down
4 changes: 2 additions & 2 deletions docs/src/modules/java/pages/event-sourced-entities.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ For the above example, this class provides access to all the command handlers of

=== Integration tests

The skeleton of an Integration Test is generated for you if you use the archetype to start your Kalix app. Let's see what it could look like to test our Counter Entity:
The skeleton of an Integration Test is generated for you if you use the archetype to start your Kalix app. Let's see what it could look like to test our `ShoppingCartEntity`:

./src/it/java/com/example/IntegrationTest.java
[source,java,indent=0]
Expand All @@ -205,4 +205,4 @@ include::example$java-spring-eventsourced-shopping-cart/src/it/java/com/example/
<4> Request to add an item to the cart.
<5> `GET` request to retrieve current status of the shopping cart and assert there should only be one item.

NOTE: The integration tests are under in a specific project profile `it` and can be run using `mvn verify -Pit`.
NOTE: The integration tests in samples are under in a specific project profile `it` and can be run using `mvn verify -Pit`.
2 changes: 1 addition & 1 deletion docs/src/modules/java/pages/running-locally.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ Use `Ctrl+C` to shut down the service. When stopping your service, it will also

== Running multiple services locally

A typical Kalix application is composed of one or more services deployed to the same Kalix project. When deployed under the same Kalix project, two different services can make xref:call-another-service.adoc[calls to each other] or xref:service-to-service.adoc[subscribe to each other's event streams] by simply using their logical names.
A typical Kalix application is composed of one or more services deployed to the same Kalix project. When deployed under the same Kalix project, two different services can make xref:component-and-service-calls.adoc[calls to each other] or xref:service-to-service.adoc[subscribe to each other's event streams] by simply using their logical names.

The same can be done on your local machine by configuring the services to run on different ports and by configuring them to "_discover_" each other using some extra configurations.

Expand Down
2 changes: 1 addition & 1 deletion docs/src/modules/java/pages/value-entity.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -141,4 +141,4 @@ include::example$java-spring-valueentity-counter/src/it/java/com/example/Counter
<4> Request to increase the value of counter `bar`. Response should have value `1`.
<5> Explicit `GET` request to retrieve value of `bar` that should be `1`.

NOTE: The integration tests are under in a specific project profile `it` and can be run using `mvn verify -Pit`.
NOTE: The integration tests in samples are under in a specific project profile `it` and can be run using `mvn verify -Pit`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.example.fibonacci;

import com.example.Main;
import com.google.protobuf.any.Any;
import kalix.javasdk.DeferredCall;
import kalix.spring.testkit.KalixIntegrationTestKitSupport;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

import java.time.Duration;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import static java.time.temporal.ChronoUnit.SECONDS;
import static java.util.concurrent.TimeUnit.MILLISECONDS;

@DirtiesContext
// tag::testing-action[]
@SpringBootTest(classes = Main.class)
public class FibonacciActionComponentClientIntegrationTest extends KalixIntegrationTestKitSupport {

private Duration timeout = Duration.of(5, SECONDS);

@Test
public void calculateNextNumber() throws ExecutionException, InterruptedException, TimeoutException {

Number response = componentClient.forAction() // <1>
.call(FibonacciAction::nextNumber)
.params(new Number(5))
.execute() // <2>
.toCompletableFuture()
.get(timeout.toMillis(), MILLISECONDS);

Assertions.assertEquals(8, response.value());
}
// end::testing-action[]

@Test
public void calculateNextNumberWithLimitedFibo() throws ExecutionException, InterruptedException, TimeoutException {

Number response = componentClient.forAction()
.call(LimitedFibonacciAction::nextNumber)
.params(new Number(5))
.execute()
.toCompletableFuture()
.get(timeout.toMillis(), MILLISECONDS);

Assertions.assertEquals(8, response.value());
}

// tag::testing-action[]
}
// end::testing-action[]
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.web.ErrorResponse;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import reactor.core.publisher.Mono;
Expand All @@ -15,55 +19,58 @@

import static java.time.temporal.ChronoUnit.SECONDS;

@SpringBootTest(classes = Main.class)
public class FibonacciActionIntegrationTest extends KalixIntegrationTestKitSupport {

@DirtiesContext
// tag::testing-action[]
@SpringBootTest(classes = Main.class) // <1>
public class FibonacciActionIntegrationTest extends KalixIntegrationTestKitSupport { // <2>

@Autowired
private WebClient webClient;

private Duration timeout = Duration.of(5, SECONDS);

@Test
public void calculateNextNumber() {

Mono<Number> response =
webClient.get()
.uri("/fibonacci/5/next")
.retrieve().bodyToMono(Number.class);

long next = response.block(Duration.of(5, SECONDS)).value();
Assertions.assertEquals(8, next);
ResponseEntity<Number> response = webClient.get()
.uri("/fibonacci/5/next")
.retrieve()
.toEntity(Number.class)
.block(timeout); // <3>

Assertions.assertEquals(HttpStatus.OK, response.getStatusCode());
Assertions.assertEquals(8, response.getBody().value());
}

// end::testing-action[]
@Test
public void calculateNextNumberWithLimitedFibo() {

Mono<Number> response =
webClient.get()
.uri("/limitedfibonacci/5/next")
.retrieve().bodyToMono(Number.class);

long next = response.block(Duration.of(5, SECONDS)).value();
Assertions.assertEquals(8, next);
ResponseEntity<Number> response = webClient.get()
.uri("/limitedfibonacci/5/next")
.retrieve()
.toEntity(Number.class)
.block(timeout);

Assertions.assertEquals(HttpStatus.OK, response.getStatusCode());
Assertions.assertEquals(8, response.getBody().value());
}

@Test
public void wrongNumberReturnsError() {
try {


ResponseEntity<Number> response =
webClient.get()
.uri("/fibonacci/7/next")
.retrieve().toEntity(Number.class)
.block(Duration.of(5, SECONDS));

Assertions.fail("Should have failed");

} catch (WebClientResponseException.InternalServerError ex) {
String bodyErrorMessage = ex.getResponseBodyAsString();
Assertions.assertTrue(bodyErrorMessage.contains("Input number is not a Fibonacci number"));
}
Mono<ResponseEntity<Void>> response = webClient.get()
.uri("/fibonacci/7/next")
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError, clientResponse ->
clientResponse.bodyToMono(String.class)
.flatMap(error -> Mono.error(new RuntimeException(error)))
)
.toBodilessEntity();

RuntimeException exception = Assertions.assertThrows(RuntimeException.class, () -> response.block(timeout));
Assertions.assertEquals("Input number is not a Fibonacci number, received '7'", exception.getMessage());
}
// tag::testing-action[]
}
// end::testing-action[]
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,19 @@
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.PathVariable;

// end::implementing-action[]

import static io.grpc.Status.Code.INVALID_ARGUMENT;


// tag::implementing-action[]
@RequestMapping("/fibonacci")
public class FibonacciAction extends Action {
public class FibonacciAction extends Action {

private boolean isFibonacci(long num) { // <1>
Predicate<Long> isPerfectSquare = (n) -> {
long square = (long) Math.sqrt(n);
return square*square == n;
return square * square == n;
};
return isPerfectSquare.test(5*num*num + 4) || isPerfectSquare.test(5*num*num - 4);
}
Expand All @@ -42,7 +44,7 @@ public Effect<Number> nextNumber(@RequestBody Number number) {
return effects().reply(new Number(nextFib(num)));
} else {
return effects() // <5>
.error("Input number is not a Fibonacci number, received '" + num + "'");
.error("Input number is not a Fibonacci number, received '" + num + "'", INVALID_ARGUMENT);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.example.fibonacci;

import com.google.protobuf.any.Any;
import io.grpc.Status;
import kalix.javasdk.DeferredCall;
import kalix.javasdk.action.Action;
import kalix.javasdk.action.ActionCreationContext;
import kalix.javasdk.client.ComponentClient;
Expand All @@ -24,14 +26,18 @@ public LimitedFibonacciAction(ActionCreationContext ctx, ComponentClient compone
}

@GetMapping("/{number}/next")
public Effect<Number> nextNumber(@PathVariable Long number) {
public Effect<Number> nextNumberPath(@PathVariable Long number) {
if (number < 0 || number > 10000) {
return effects().error("Only numbers between 0 and 10k are allowed", Status.Code.INVALID_ARGUMENT);
} else {
logger.info("Executing GET call to real /fibonacci = " + number);
var serviceCall = componentClient.forAction().call(FibonacciAction::getNumber).params(number);
// tag::component-client[]
DeferredCall<Any, Number> deferredCall = componentClient.forAction() // <1>
.call(FibonacciAction::getNumber) // <2>
.params(number);// <3>

return effects().forward(serviceCall);
return effects().forward(deferredCall);
// end::component-client[]
}
}

Expand Down

0 comments on commit 35d767f

Please sign in to comment.