Implementing Actions in Java

Actions are stateless functions that can be used to implement different uses cases, such as:

  • a pure function

  • request conversion - you can use Actions to convert incoming data into a different format before forwarding a call to a different component.

  • publish and subscribe to events

Actions can be triggered in multiple ways. For example, by:

  • a gRPC service call

  • an HTTP service call

  • a forwarded call from another component

  • an incoming event from within the same service or a from different service

Defining the proto file

An Action may implement any service method defined in a Protobuf definition. In this first example, we will show how to implement an Action as a pure stateless function. We will define a FibonacciAction that takes a number and return the next number in the Fibonacci series.

src/main/proto/com/example/fibonacci/fibonacci.proto
syntax = "proto3";
package com.example.fibonacci; (1)

import "akkaserverless/annotations.proto"; (2)

option java_outer_classname = "FibonacciApi"; (3)

message Number {
  int64 value = 1;
}

service Fibonacci {
  option (akkaserverless.service) = {
    type : SERVICE_TYPE_ACTION  (4)
  };

  rpc NextNumber(Number) returns (Number) {}

}
1 Any classes generated from this protobuf file will be in the Java package com.example.fibonacci.
2 Import the Akka Serverless protobuf annotations, or options.
3 Let the messages declared in this protobuf file be inner classes to the Java class FibonacciApi.
4 The protobuf option (akkaserverless.service) is specific to code-generation as provided by the Akka Serverless Maven plugin. This annotation indicates to the code-generation that an Action must be generated.

Implementing the Action

An Action implementation is a Java class where you define how each message is handled. The class FibonacciAction gets generated for us based on the proto file defined above. Once the FibonacciAction.java file exist, it is not overwritten, so you can freely add logic to it. FibonacciAction extends the generated class AbstractFibonacciAction which we’re not supposed to change as it gets regenerated in case we update the protobuf descriptors.

AbstractFibonacciAction contains all method signatures corresponding to the API of the service. If you change the API you will see compilation errors in the FibonacciAction class, and you have to implement the methods required by AbstractFibonacciAction.

src/main/java/com/example/fibonacci/FibonacciAction.java
public class FibonacciAction extends AbstractFibonacciAction { (1)

  public FibonacciAction(ActionCreationContext creationContext) {
  }

  /**
   * Handler for "NextNumber".
   */
  @Override
  public Effect<FibonacciApi.Number> nextNumber(FibonacciApi.Number number) { (2)
    throw new RuntimeException("The command handler for `NextNumber` is not implemented, yet");
  }

}
1 Extends the generated AbstractFibonacciAction, which extends Action new tab.
2 A nextNumber method is generated. We will implement it next.

Next we can implement nextNumber method to complete our Action.

src/main/java/com/example/fibonacci/FibonacciAction.java
private boolean isFibonacci(long num) {  (1)
  Predicate<Long> isPerfectSquare = (n) -> {
    long square = (long) Math.sqrt(n);
    return square*square == n;
  };
  return isPerfectSquare.test(5*num*num + 4) || isPerfectSquare.test(5*num*num - 4);
}
private long nextFib(long num) {
  double result = num * (1 + Math.sqrt(5)) / 2.0;
  return Math.round(result);
}

/** Handler for "NextNumber". */
@Override
public Effect<FibonacciApi.Number> nextNumber(FibonacciApi.Number number) {
  long num = number.getValue();
  if (isFibonacci(num)) { (2)
    long nextFib = nextFib(num);
    FibonacciApi.Number response =
        FibonacciApi.Number
            .newBuilder()
            .setValue(nextFib)
            .build();
    return effects().reply(response);
  } else {
    return effects() (3)
             .error("Input number is not a Fibonacci number, received '" + num + "'");
  }
}
1 We add two private methods to support the computation. isFibonacci checks if a number is a Fibonacci number and nextFib calculates the next number.
2 The nextNumber implementation first checks if the input number belongs to the Fibonacci series. If so, it calculates the next number and builds a reply using effects().reply().
3 Otherwise, if the input number doesn’t belong to the Fibonacci series, it builds an Effect reply error.

Multiple replies / reply streaming

An Action may return data conditionally by marking the return type as stream in Protobuf. The Java method implementing that service must return an Akka Streams Source to fulfill that contract.

The Source may publish an arbitrary number of replies.

Registering the Action

To make Akka Serverless aware of the Action, we need to register it with the service.

From the code-generation, the registration gets automatically inserted in the generated AkkaServerlessFactory.withComponents method from the Main class.

/src/main/java/com/example/Main.java
/* This code was generated by Akka Serverless tooling.
 * As long as this file exists it will not be re-generated.
 * You are free to make changes to this file.
 */

package com.example;

import com.akkaserverless.javasdk.AkkaServerless;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.example.fibonacci.FibonacciAction;

public final class Main {

  private static final Logger LOG = LoggerFactory.getLogger(Main.class);

  public static AkkaServerless createAkkaServerless() {
    // The AkkaServerlessFactory automatically registers any generated Actions, Views or Entities,
    // and is kept up-to-date with any changes in your protobuf definitions.
    // If you prefer, you may remove this and manually register these components in a
    // `new AkkaServerless()` instance.
    return AkkaServerlessFactory.withComponents(
      FibonacciAction::new);
  }

  public static void main(String[] args) throws Exception {
    LOG.info("starting the Akka Serverless service");
    createAkkaServerless().start();
  }
}

By default, the generated constructor has a ActionCreationContext parameter, but you can change this to accept other parameters. If you change the constructor of the FibonacciAction class you will see a compilation error here, and you have to adjust the factory function that is passed to AkkaServerlessFactory.withComponents.

When more components are added the AkkaServerlessFactory is regenerated, and you have to adjust the registration from the Main class.