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

io.cloudstate.javasupport.CloudStateRunner.scala Maven / Gradle / Ivy

/*
 * Copyright 2019 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 io.cloudstate.javasupport

import java.util.concurrent.CompletionStage

import com.typesafe.config.{Config, ConfigFactory}
import akka.Done
import akka.actor.ActorSystem
import akka.http.scaladsl._
import akka.http.scaladsl.model._
import akka.stream.{ActorMaterializer, Materializer}
import com.google.protobuf.Descriptors
import io.cloudstate.javasupport.impl.eventsourced.{EventSourcedImpl, EventSourcedStatefulService}
import io.cloudstate.javasupport.impl.{EntityDiscoveryImpl, ResolvedServiceCallFactory, ResolvedServiceMethod}
import io.cloudstate.javasupport.impl.crdt.{CrdtImpl, CrdtStatefulService}
import io.cloudstate.protocol.crdt.CrdtHandler
import io.cloudstate.protocol.entity.EntityDiscoveryHandler
import io.cloudstate.protocol.event_sourced.EventSourcedHandler

import scala.compat.java8.FutureConverters
import scala.concurrent.Future
import scala.collection.JavaConverters._

object CloudStateRunner {
  final case class Configuration(userFunctionInterface: String, userFunctionPort: Int, snapshotEvery: Int) {
    validate()
    def this(config: Config) = {
      this(
        userFunctionInterface = config.getString("user-function-interface"),
        userFunctionPort = config.getInt("user-function-port"),
        snapshotEvery = config.getInt("eventsourced.snapshot-every")
      )
    }

    private def validate(): Unit = {
      require(userFunctionInterface.length > 0, s"user-function-interface must not be empty")
      require(userFunctionPort > 0, s"user-function-port must be greater than 0")
    }
  }
}

/**
 * The CloudStateRunner is responsible for handle the bootstrap of entities,
 * and is used by [[io.cloudstate.javasupport.CloudState.start()]] to set up the local
 * server with the given configuration.
 *
 * CloudStateRunner can be seen as a low-level API for cases where [[io.cloudstate.javasupport.CloudState.start()]] isn't enough.
 */
final class CloudStateRunner private[this] (_system: ActorSystem, services: Map[String, StatefulService]) {
  private[this] implicit final val system = _system
  private[this] implicit final val materializer: Materializer = ActorMaterializer()

  private[this] final val configuration =
    new CloudStateRunner.Configuration(system.settings.config.getConfig("cloudstate"))

  // TODO JavaDoc
  def this(services: java.util.Map[String, StatefulService]) {
    this(
      {
        val conf = ConfigFactory.load()
        // We do this to apply the cloud-state specific akka configuration to the ActorSystem we create for hosting the user function
        ActorSystem("StatefulService", conf.getConfig("cloudstate.system").withFallback(conf))
      },
      services.asScala.toMap
    )
  }

  private val rootContext = new Context {
    override val serviceCallFactory: ServiceCallFactory = new ResolvedServiceCallFactory(services)
  }

  private[this] def createRoutes(): PartialFunction[HttpRequest, Future[HttpResponse]] = {

    val serviceRoutes =
      services.groupBy(_._2.getClass).foldLeft(PartialFunction.empty[HttpRequest, Future[HttpResponse]]) {

        case (route, (serviceClass, eventSourcedServices: Map[String, EventSourcedStatefulService] @unchecked))
            if serviceClass == classOf[EventSourcedStatefulService] =>
          val eventSourcedImpl = new EventSourcedImpl(system, eventSourcedServices, rootContext, configuration)
          route orElse EventSourcedHandler.partial(eventSourcedImpl)

        case (route, (serviceClass, crdtServices: Map[String, CrdtStatefulService] @unchecked))
            if serviceClass == classOf[CrdtStatefulService] =>
          val crdtImpl = new CrdtImpl(system, crdtServices, rootContext)
          route orElse CrdtHandler.partial(crdtImpl)

        case (_, (serviceClass, _)) =>
          sys.error(s"Unknown StatefulService: $serviceClass")
      }

    val entityDiscovery = EntityDiscoveryHandler.partial(new EntityDiscoveryImpl(system, services))

    serviceRoutes orElse
    entityDiscovery orElse { case _ => Future.successful(HttpResponse(StatusCodes.NotFound)) }
  }

  /**
   * Starts a server with the configured entities.
   *
   * @return a CompletionStage which will be completed when the server has shut down.
   */
  def run(): CompletionStage[Done] = {
    val serverBindingFuture = Http
      .get(system)
      .bindAndHandleAsync(createRoutes(),
                          configuration.userFunctionInterface,
                          configuration.userFunctionPort,
                          HttpConnectionContext(UseHttp2.Always))
    // FIXME Register an onTerminate callback to unbind the Http server
    FutureConverters
      .toJava(serverBindingFuture)
      .thenCompose(
        binding => system.getWhenTerminated.thenCompose(_ => FutureConverters.toJava(binding.unbind()))
      )
      .thenApply(_ => Done)
  }

  /**
   * Terminates the server.
   *
   * @return a CompletionStage which will be completed when the server has shut down.
   */
  def terminate(): CompletionStage[Done] =
    FutureConverters.toJava(system.terminate()).thenApply(_ => Done)
}

/**
 * StatefulService describes an entitiy type in a way which makes it possible
 * to deploy.
 */
trait StatefulService {

  /**
   * @return a Protobuf ServiceDescriptor of its externally accessible gRPC API
   */
  def descriptor: Descriptors.ServiceDescriptor

  /**
   * Possible values are: "", "", "".
   * @return the type of entity represented by this StatefulService
   */
  def entityType: String

  /**
   * @return the persistence identifier used for the the entities represented by this service
   */
  def persistenceId: String = descriptor.getName

  // TODO JavaDoc
  def resolvedMethods: Option[Map[String, ResolvedServiceMethod[_, _]]]
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy