All Downloads are FREE. Search and download functionalities are using the official Maven repository.

kalix.scalasdk.action.Action.scala Maven / Gradle / Ivy

/*
 * Copyright 2024 Lightbend Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package kalix.scalasdk.action

import scala.concurrent.ExecutionContext
import scala.concurrent.Future

import kalix.scalasdk.{ DeferredCall, Metadata, SideEffect }
import kalix.scalasdk.impl.action.ActionEffectImpl
import io.grpc.Status
import kalix.javasdk.impl.action.ActionContextImpl
import kalix.scalasdk.impl.action.ScalaActionContextAdapter
import kalix.scalasdk.timer.TimerScheduler
import kalix.scalasdk.impl.timer.TimerSchedulerImpl

object Action {

  /**
   * An Effect is a description of what Kalix needs to do after the command is handled. You can think of it as a set of
   * instructions you are passing to Kalix. Kalix will process the instructions on your behalf.
   *
   * Each Kalix component defines its own effects, which are a set of predefined operations that match the capabilities
   * of that component.
   *
   * An Action Effect can either:
   *
   *   - reply with a message to the caller
   *   - reply with a message to be published to a topic (in case the method is a publisher)
   *   - forward the message to another component
   *   - return an error
   *   - ignore the call
   *
   * @tparam T
   *   The type of the message that must be returned by this call.
   */
  trait Effect[+T] {

    /**
     * Attach the given side effects to this reply.
     *
     * @param sideEffects
     *   The effects to attach.
     * @return
     *   A new reply with the attached effects.
     */
    def addSideEffect(sideEffects: SideEffect*): Action.Effect[T]

    def addSideEffects(sideEffects: Seq[SideEffect]): Action.Effect[T]

    /**
     * @return
     *   true if this effect supports attaching side effects, if returning false addSideEffects will throw an exception.
     */
    def canHaveSideEffects: Boolean
  }

  /**
   * Construct the effect that is returned by the command handler. The effect describes next processing actions, such as
   * sending a reply.
   */
  object Effect {
    trait Builder {

      /**
       * Create a message reply.
       *
       * @param message
       *   The payload of the reply.
       * @return
       *   A message reply.
       * @tparam S
       *   The type of the message that must be returned by this call.
       */
      def reply[S](message: S): Action.Effect[S]

      /**
       * Create a message reply with custom Metadata.
       *
       * @param message
       *   The payload of the reply.
       * @param metadata
       *   The metadata for the message.
       * @return
       *   A message reply.
       * @tparam S
       *   The type of the message that must be returned by this call.
       */
      def reply[S](message: S, metadata: Metadata): Action.Effect[S]

      /**
       * Create a forward reply.
       *
       * @param serviceCall
       *   The service call representing the forward.
       * @return
       *   A forward reply.
       * @tparam S
       *   The type of the message that must be returned by this call.
       */
      def forward[S](serviceCall: DeferredCall[_, S]): Action.Effect[S]

      /**
       * Create an error reply.
       *
       * @param description
       *   The description of the error.
       * @return
       *   An error reply.
       * @tparam S
       *   The type of the message that must be returned by this call.
       */
      def error[S](description: String): Action.Effect[S]

      /**
       * Create an error reply.
       *
       * @param description
       *   The description of the error.
       * @param statusCode
       *   A gRPC status code.
       * @return
       *   An error reply.
       * @tparam S
       *   The type of the message that must be returned by this call.
       */
      def error[S](description: String, statusCode: Status.Code): Action.Effect[S]

      /**
       * Create a message reply from an async operation result.
       *
       * @param message
       *   The future payload of the reply.
       * @return
       *   A message reply.
       * @tparam S
       *   The type of the message that must be returned by this call.
       */
      def asyncReply[S](message: Future[S]): Action.Effect[S]

      /**
       * Create a message reply from an async operation result with custom Metadata.
       *
       * @param message
       *   The future payload of the reply.
       * @param metadata
       *   The metadata for the message.
       * @return
       *   A message reply.
       * @tparam S
       *   The type of the message that must be returned by this call.
       */
      def asyncReply[S](message: Future[S], metadata: Metadata): Action.Effect[S]

      /**
       * Create a reply from an async operation result returning an effect.
       *
       * @param futureEffect
       *   The future effect to reply with.
       * @return
       *   A reply, the actual type depends on the nested Effect.
       * @tparam S
       *   The type of the message that must be returned by this call.
       */
      def asyncEffect[S](futureEffect: Future[Action.Effect[S]]): Action.Effect[S]

      /**
       * Ignore the current element and proceed with processing the next element if returned for an element from
       * eventing in. If used as a response to a regular gRPC or HTTP request it is turned into a NotFound response.
       *
       * Ignore is not allowed to have side effects added with `addSideEffects`
       */
      def ignore[S]: Action.Effect[S]
    }
  }
}

/**
 * Actions are stateless components 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 messages to a Topic.
 *   - subscribe to events from an Event Sourced Entity.
 *   - subscribe to state changes from a Value Entity.
 *   - 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 message from a Topic.
 *   - an incoming event from an Event Sourced Entity, from within the same service or from a different service.
 *   - state changes notification from a Value Entity on the same service.
 *
 * An Action method should return an [[kalix.scalasdk.action.Action.Effect]] that describes what to do next.
 */
abstract class Action {
  @volatile
  private var _actionContext: Option[ActionContext] = None

  /**
   * An ExecutionContext to use when composing Futures inside Actions.
   */
  implicit def executionContext: ExecutionContext =
    scala.concurrent.ExecutionContext.Implicits.global

  /**
   * Additional context and metadata for a message handler.
   *
   * It will throw an exception if accessed from constructor.
   */
  protected final def actionContext: ActionContext =
    actionContext("ActionContext is only available when handling a message.")

  /**
   * INTERNAL API
   *
   * Same as actionContext, but if specific error message when accessing components.
   */
  protected final def contextForComponents: ActionContext =
    actionContext("Components can only be accessed when handling a message.")

  private def actionContext(errorMessage: String): ActionContext =
    _actionContext.getOrElse(throw new IllegalStateException(errorMessage))

  /** INTERNAL API */
  final def _internalSetActionContext(context: Option[ActionContext]): Unit = {
    _actionContext = context
  }

  /**
   * Returns a [[TimerScheduler]] that can be used to schedule further in time.
   */
  final def timers: TimerScheduler = {

    val javaActionContextImpl =
      actionContext("Timers can only be scheduled or cancelled when handling a message.") match {
        case ScalaActionContextAdapter(actionContext: ActionContextImpl) => actionContext
        // should not happen as we always need to pass ScalaActionContextAdapter(ActionContextImpl)
        case other =>
          throw new RuntimeException(
            s"Incompatible ActionContext instance. Found ${other.getClass}, expecting ${classOf[ActionContextImpl].getName}")
      }

    new TimerSchedulerImpl(javaActionContextImpl.messageCodec, javaActionContextImpl.system)
  }

  protected final def effects[T]: Action.Effect.Builder =
    ActionEffectImpl.builder()
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy