Implementing Actions

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

  • schedule and cancel timers

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

  • a gRPC service call

  • an HTTP service call

  • a forwarded call from another component

  • a scheduled call from a timer

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

In this first example, you will learn how to implement an Action as a pure stateless function. You will create a FibonacciAction that takes a number and returns the next number in the Fibonacci series.

Implementing the Action

To implement this action you need the following:

  • Extend our class from kalix.javasdk.action.Action. This is generic. No matter what action you want to create you always need to extend from Action new tab.

  • Add the Spring annotation @RequestMapping to provide a REST endpoint for the function. Here the stateless function should be reachable via HTTP.

  • Add the Spring annotations @GetMapping and @PostMapping to provide paths for GET and POST to calculate the Fibonacci of a number. Both functions do the same thing and implementation-wise the function exposed with GET calls the function exposed with POST.

src/main/java/com/example/fibonacci/FibonacciAction.java
import kalix.javasdk.action.Action;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.PathVariable;

@RequestMapping("/fibonacci")
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 isPerfectSquare.test(5*num*num + 4) || isPerfectSquare.test(5*num*num - 4);
  }
  private long nextFib(long num) {   (2)
    double result = num * (1 + Math.sqrt(5)) / 2.0;
    return Math.round(result);
  }

  @GetMapping("/{number}/next")
  public Effect<Number> nextNumber(@PathVariable Long number) { (3)
    return nextNumber(new Number(number));
  }

  @PostMapping("/next")
  public Effect<Number> nextNumber(@RequestBody Number number) {
    long num =  number.value();
    if (isFibonacci(num)) {                                     (4)
      return effects().reply(new Number(nextFib(num)));
    } else {
      return effects()                                          (5)
          .error("Input number is not a Fibonacci number, received '" + num + "'");
    }
  }
}
1 isFibonacci checks if a number is a Fibonacci number.
2 nextFib calculates the next number.
3 This nextNumber implementation calls the nextNumber implementation below.
4 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().
5 Otherwise, if the input number doesn’t belong to the Fibonacci series, it builds an Effect reply error.

Actions return effects (i.e. Action.Effect) and there are different types of effects: a reply, an error, a forward call to another component, and to all of those you can add side effects. Here you want only the result of the calculation or an error. Therefore you are using .reply and .error.

Multiple replies / reply streaming

An Action may return a stream of integers. To do this you need to define the return type as reactor.core.publisher.Flux<Effect<Integer>>.

The stream may publish an arbitrary number of replies.

Testing the Action

Unit tests

The following snippet shows how the ActionTestkit is used to test the FibonacciAction implementation.

With the ActionTestkit you can call the methods of FibonacciAction. Each call you pass over to the test kit returns an ActionResult that contains the effect produced by the underlying action method.

Actions are unique units of computation where no local state is shared with previous or subsequent calls. The framework does not reuse an Action instance but creates a new one for each command handled and therefore this is also how the test kit behaves.
Java
src/test/java/com/example/actions/FibonacciActionTest.java
import kalix.javasdk.testkit.ActionResult;
import kalix.javasdk.testkit.ActionTestkit;
import org.junit.jupiter.api.Test;

public class FibonacciActionTest {

  @Test
  public void testNextFib() {
    ActionTestkit<FibonacciAction> testkit = ActionTestkit.of(FibonacciAction::new); (1)
    ActionResult<Number> result = testkit.call(a -> a.nextNumber(3L));  (2)
    assertTrue(result.isReply());
    assertEquals(5L, result.getReply().value());
  }

  @Test
  public void testNextFibError() {
    ActionTestkit<FibonacciAction> testkit = ActionTestkit.of(FibonacciAction::new);  (1)
    ActionResult<Number> result = testkit.call(a -> a.nextNumber(4L));     (2)
    assertTrue(result.isError());
    assertTrue(result.getError().startsWith("Input number is not a Fibonacci number"));
  }
}
1 The test kit is created to allow us to test the Action’s method.
2 Calling nextNumber method with some value.

ActionResult

Calling an action method through the test kit gives us back an ActionResult new tab. This class has methods that you can use to assert your expectations, such as:

  • getReply() returns the reply message passed to effects().reply() or throws an exception failing the test, if the effect returned was not a reply.

  • 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).

TODO: add links to before and after