blob: c28d486b4b72e627fdbb7f1d99c514b1e8a84688 [file] [log] [blame]
= Section 8: Projection calling gRPC service
:page-supergroup-java-scala: Language
include::ROOT:partial$include.adoc[]
To complete the example, we need a way to handle orders. We will add another Projection from the events of the `ShoppingCart` entity. We will also create a gRPC Order Service, `ShoppingOrderService`. The Projection calls the Order Service when the shopping carts are checked out.
[caption=""]
image::example-projection-grpc-client.svg[Example gRPC client]
This part of the xref:overview.adoc[full example] focuses on the gRPC client in the `SendOrderProjection`. On this page you will learn how to:
* call another service with xref:concepts:akka-grpc.adoc[Akka gRPC]
* implement another gRPC service by adding the `ShoppingOrderService`
== Source downloads
If you prefer to simply view and run the example, download a zip file containing the completed code:
[.tabset]
Java::
+
****
* link:_attachments/5-shopping-cart-projection-kafka-java.zip[Source] that includes all previous tutorial steps and allows you to start with the steps on this page.
* link:_attachments/6-shopping-cart-complete-java.zip[Source] with the steps on this page completed.
****
Scala::
+
****
* link:_attachments/5-shopping-cart-projection-kafka-scala.zip[Source] that includes all previous tutorial steps and allows you to start with the steps on this page.
* link:_attachments/6-shopping-cart-complete-scala.zip[Source] with the steps on this page completed.
****
:sectnums:
== Add the Order gRPC service
Let's add the service that handles shopping cart orders. The template and source downloads include a directory named `shopping-order-service` for this purpose. To add the order gRPC service, follow these steps:
. Open the `shopping-order-service` in IntelliJ just as you did with xref:template.adoc#intellij[ the shopping-cart-service].
. Define the interface of the service in a protobuf service descriptor. It should be located in the `src/main/protobuf/ShoppingOrderService.proto` in the new `shopping-order-service` project.
+
[source,protobuf]
----
include::example$shopping-order-service-scala/src/main/protobuf/ShoppingOrderService.proto[]
----
. Generate code by compiling the project:
+
[.group-java]
[source,shell script]
----
mvn compile
----
+
[.group-scala]
[source,shell script]
----
sbt compile
----
. Implement the `ShoppingOrderService` in a new class `ShoppingOrderServiceImpl`:
+
[.tabset]
Java::
+
.src/main/java/shopping/order/ShoppingOrderServiceImpl.java:
[source,java,indent=0]
----
include::example$shopping-order-service-java/src/main/java/shopping/order/ShoppingOrderServiceImpl.java[]
----
Scala::
+
.src/main/scala/shopping/order/ShoppingOrderServiceImpl.scala:
[source,scala,indent=0]
----
include::example$shopping-order-service-scala/src/main/scala/shopping/order/ShoppingOrderServiceImpl.scala[]
----
. Similar to the xref:grpc-service.adoc[gRPC server for the ShoppingCartService] we need to initialize the gRPC server. Add a `ShoppingOrderServer` [.group-scala]#object# [.group-java]#class#:
+
[.tabset]
Java::
+
.src/main/java/shopping/order/ShoppingOrderServer.java:
[source,java,indent=0]
----
include::example$shopping-order-service-java/src/main/java/shopping/order/ShoppingOrderServer.java[]
----
Scala::
+
.src/main/scala/shopping/order/ShoppingOrderServer.scala:
[source,scala,indent=0]
----
include::example$shopping-order-service-scala/src/main/scala/shopping/order/ShoppingOrderServer.scala[]
----
. Call the `ShoppingOrderServer.start` from `Main`:
+
[.tabset]
Java::
+
[source,java,indent=0]
----
include::example$shopping-order-service-java/src/main/java/shopping/order/Main.java[]
----
Scala::
+
[source,scala,indent=0]
----
include::example$shopping-order-service-scala/src/main/scala/shopping/order/Main.scala[]
----
NOTE: The `grpc.port` configuration is defined in `local1.conf`, which is included in the generated template project.
== Create the Projection
The new Projection for `shopping-cart-service` events will be similar to the one we developed for Kafka on the xref:projection-kafka.adoc[previous page], but when it receives `ShoppingCart.CheckedOut` events, it will call the `ShoppingOrderService`.
Create the Projection as follows:
. Include the service definition by copying the `ShoppingOrderService.proto` file from the `shopping-order-service` to the `shopping-cart-service/src/main/protobuf` directory.
. Generate code by compiling the `shopping-cart-service` project:
+
[.group-java]
[source,shell script]
----
mvn compile
----
+
[.group-scala]
[source,shell script]
----
sbt compile
----
. Add a `SendOrderProjectionHandler` class in the `shopping-cart-service` project. This is the Projection `Handler` for processing the events:
+
[.tabset]
Java::
+
.src/main/java/shopping/cart/SendOrderProjectionHandler.java:
[source,java,indent=0]
----
include::example$shopping-cart-service-java/src/main/java/shopping/cart/SendOrderProjectionHandler.java[]
----
Scala::
+
.src/main/scala/shopping/cart/SendOrderProjectionHandler.scala:
[source,scala,indent=0]
----
include::example$shopping-cart-service-scala/src/main/scala/shopping/cart/SendOrderProjectionHandler.scala[]
----
<1> `ShoppingOrderService` is the gRPC client
<2> Retrieve the full shopping cart information from the entity. In the order we need to include the list of items and their quantities. That information is not included in the `ShoppingCart.CheckedOut` event, but we can retrieve it by asking the `ShoppingCart` entity for it.
<3> Call the `ShoppingOrderService`. If the call to the `ShoppingOrderService` fails, the returned [.group-scala]#`Future[Done]`# [.group-java]#`CompletionStage<Done>`# fails and the Projection is automatically restarted from the previously saved offset. This will result in retrying the call to the `ShoppingOrderService`. Since the Projection has `at-least-once` semantics, the `ShoppingOrderService` must be idempotent, that is, it must gracefully handle duplicate order attempts for the same `cartId`.
== Initialize the Projection
The tagging of the events is already in place from when we created the xref:projection-query.adoc#tagging[query Projection]. So, we just need to initialize it as follows:
. Place the initialization code of the Projection in an `SendOrderProjection` [.group-scala]#object# [.group-java]#class#:
+
[.tabset]
Java::
+
.src/main/java/shopping/cart/SendOrderProjection.java:
[source,java,indent=0]
----
include::example$shopping-cart-service-java/src/main/java/shopping/cart/SendOrderProjection.java[]
----
Scala::
+
.src/main/scala/shopping/cart/SendOrderProjection.scala:
[source,scala,indent=0]
----
include::example$shopping-cart-service-scala/src/main/scala/shopping/cart/SendOrderProjection.scala[]
----
. In `Main`, invoke the `SendOrderProjection.init` and create the gRPC client for the `ShoppingOrderService` like this:
+
[.tabset]
Java::
+
[source,java,indent=0]
----
include::example$shopping-cart-service-java/src/main/java/shopping/cart/Main.java[tag=SendOrderProjection]
----
+
<1> This example is only showing the new `SendOrderProjection.init` here, additional initialization from previous steps should be kept.
<2> The reason for placing the initialization of `orderServiceClient` in a method is that tests can then replace it with a stub implementation.
Scala::
+
[source,scala,indent=0]
----
include::example$shopping-cart-service-scala/src/main/scala/shopping/cart/Main.scala[tag=SendOrderProjection]
----
+
<1> This example is only showing the new `SendOrderProjection.init` here, additional initialization from previous steps should be kept.
<2> The reason for placing the initialization of `orderServiceClient` in a method is that tests can then replace it with a stub implementation.
. The gRPC client is using service discovery to locate the `ShoppingOrderService`. For local development add the following to `src/main/resources/local-shared.conf`, which is loaded when running locally:
+
[source,hocon]
----
include::example$shopping-cart-service-scala/src/main/resources/local-shared.conf[tag=grpc]
----
== Run locally
Follow these steps to run locally and exercise the new Projection and service:
. Start PostgresSQL and Kafka, unless they are already running:
+
[source,shell script]
----
docker-compose up -d
----
. Run the `shopping-order-service` with:
+
[.group-java]
[source,shell script]
----
# make sure to compile before running exec:exec
mvn compile exec:exec -DAPP_CONFIG=local1.conf
----
+
[.group-scala]
[source,shell script]
----
sbt -Dconfig.resource=local1.conf run
----
. Keep the `shopping-order-service` running, and in another terminal run the `shopping-cart-service` with:
+
[.group-java]
[source,shell script]
----
# make sure to compile before running exec:exec
mvn compile exec:exec -DAPP_CONFIG=local1.conf
----
+
[.group-scala]
[source,shell script]
----
sbt -Dconfig.resource=local1.conf run
----
=== Exercise the service
Use `https://github.com/fullstorydev/grpcurl[grpcurl]` to exercise the service:
. Try the new order service directly (on port 8301):
+
[source,shell script]
----
grpcurl -d '{"cartId":"cart1", "items":[{"itemId":"socks", "quantity":3}, {"itemId":"t-shirt", "quantity":2}]}' -plaintext 127.0.0.1:8301 shoppingorder.ShoppingOrderService.Order
----
. Use the checkout in the shopping cart service with `grpcurl` (note the different port number):
+
[source,shell script]
----
grpcurl -d '{"cartId":"cart1", "itemId":"scissors", "quantity":1}' -plaintext 127.0.0.1:8101 shoppingcart.ShoppingCartService.AddItem
----
+
[source,shell script]
----
grpcurl -d '{"cartId":"cart1"}' -plaintext 127.0.0.1:8101 shoppingcart.ShoppingCartService.Checkout
----
. In the terminal of the `shopping-order-service` you should see the log of the order:
+
----
Order 12 items from cart cart1
----
=== Stop the service
When finished:
. Stop the `shopping-cart-service` and `shopping-order-service` with `ctrl-c`.
. Stop PostgresSQL and Kafka with:
+
[source,shell script]
----
docker-compose stop
----
NOTE: The following steps for cloud deployment are optional. If you are only running locally, you have completed the tutorial Congratulations.
[#kubernetes]
== Run in Kubernetes
Create a xref:deployment:aws-install.adoc[Kubernetes cluster and install the Akka Operator] if you haven't already.
=== Build Docker image
Create a Docker repository and authenticate Docker.
[.tabset]
GCP::
+
Follow the instructions in https://cloud.google.com/container-registry/docs/using-with-google-cloud-platform[Using Container Registry with Google Cloud {tab-icon}, window="tab"] to deploy Docker images on GCP's container registry.
AWS::
+
Follow the instructions in xref:deployment:aws-ecr.adoc[Amazon Elastic Container Registry] to deploy Docker images on AWS's container registry.
=== Additional steps for Docker and AWS
If you are using AWS, you will also need to complete the following procedures.
Build and publish the Docker images for both `shopping-cart-service` and `shopping-order-service`.
include::partial$build-docker-for-kube.adoc[]
=== Update the deployment descriptor
Update the `shopping-cart-service/kubernetes/shopping-cart-service-cr.yml` and `shopping-order-service/kubernetes/shopping-order-service-cr.yml` deployment descriptors with the respective image tag produced on the previous step.
=== Apply to Kubernetes
Apply both `shopping-cart-service/kubernetes/shopping-cart-service-cr.yml` and `shopping-order-service/kubernetes/shopping-order-service-cr.yml` to Kubernetes:
[source,shell script]
----
kubectl apply -f shopping-cart-service/kubernetes/shopping-cart-service-cr.yml
----
[source,shell script]
----
kubectl apply -f shopping-order-service/kubernetes/shopping-order-service-cr.yml
----
You can see progress by viewing the status:
[source,shell script]
----
kubectl get akkamicroservices
----
See xref:deployment:troubleshooting.adoc[troubleshooting deployment status] for more details.
=== Exercise the service in Kubernetes
. You can list the pods with:
+
[source,shell script]
----
kubectl get pods
----
. Inspect logs from both services from a separate terminal window:
+
[source,shell script]
----
kubectl logs -f <shopping-cart-service pod name from above>
----
+
[source,shell script]
----
kubectl logs -f <shopping-order-service pod name from above>
----
. Add port forwarding for the `shopping-cart-service` gRPC endpoint from a separate terminal:
+
[source,shell script]
----
kubectl port-forward svc/shopping-cart-service-grpc 8101:8101
----
Use `https://github.com/fullstorydev/grpcurl[grpcurl]` to exercise the service:
. Use the checkout in the shopping cart service with `grpcurl`:
+
[source,shell script]
----
grpcurl -d '{"cartId":"cart1", "itemId":"scissors", "quantity":1}' -plaintext 127.0.0.1:8101 shoppingcart.ShoppingCartService.AddItem
----
+
[source,shell script]
----
grpcurl -d '{"cartId":"cart1"}' -plaintext 127.0.0.1:8101 shoppingcart.ShoppingCartService.Checkout
----
. In the terminal of the `shopping-order-service` you should see the log of the order:
+
----
Order 15 items from cart cart1
----
:!sectnums:
== Learn more
Congratulations, you finished the tutorial! The examples of gRPC, event sourcing, and Projections should be helpful when you create your own Reactive Microservices. The following sections go into more detail:
* xref:concepts:internal-and-external-communication.adoc[Internal and External Communication concepts].
* {akka-grpc}/client/index.html[Akka gRPC client reference documentation {tab-icon}, window="tab"].
* {akka-projection}/[Akka Projection reference documentation {tab-icon}, window="tab"].