mirror of
https://github.com/jlengrand/baker.git
synced 2026-03-10 08:01:23 +00:00
Merge branch 'master' into fix_buildExampleDocker
This commit is contained in:
@@ -1,47 +0,0 @@
|
||||
package com.ing.baker.baas.akka
|
||||
|
||||
import akka.actor.ActorSystem
|
||||
import akka.http.scaladsl.Http
|
||||
import akka.http.scaladsl.marshalling.{Marshaller, ToEntityMarshaller}
|
||||
import akka.http.scaladsl.model.{MediaTypes, StatusCodes}
|
||||
import akka.http.scaladsl.server.Directives._
|
||||
import akka.http.scaladsl.server.Route
|
||||
import akka.http.scaladsl.unmarshalling.{FromEntityUnmarshaller, Unmarshaller}
|
||||
import akka.stream.Materializer
|
||||
import com.ing.baker.runtime.scaladsl.BakerEvent
|
||||
import com.ing.baker.runtime.serialization.{Encryption, ProtoMap}
|
||||
|
||||
import scala.concurrent.Future
|
||||
|
||||
object RemoteBakerEventListenerHttp {
|
||||
|
||||
def run(listenerFunction: BakerEvent => Unit)(host: String, port: Int)(implicit system: ActorSystem, mat: Materializer, encryption: Encryption): Future[Http.ServerBinding] = {
|
||||
import system.dispatcher
|
||||
val server = new RemoteBakerEventListenerHttp(listenerFunction)(system, mat, encryption)
|
||||
Http().bindAndHandle(server.route, host, port)
|
||||
}
|
||||
}
|
||||
|
||||
class RemoteBakerEventListenerHttp(listenerFunction: BakerEvent => Unit)(implicit system: ActorSystem, mat: Materializer, encryption: Encryption) {
|
||||
|
||||
import system.dispatcher
|
||||
|
||||
private type ProtoMessage[A] = scalapb.GeneratedMessage with scalapb.Message[A]
|
||||
|
||||
private implicit def protoMarshaller[A, P <: ProtoMessage[P]](implicit mapping: ProtoMap[A, P]): ToEntityMarshaller[A] =
|
||||
Marshaller.ByteArrayMarshaller.wrap(MediaTypes.`application/octet-stream`)(mapping.toByteArray)
|
||||
|
||||
private implicit def protoUnmarshaller[A, P <: ProtoMessage[P]](implicit mapping: ProtoMap[A, P]): FromEntityUnmarshaller[A] =
|
||||
Unmarshaller.byteArrayUnmarshaller.map(mapping.fromByteArray(_).get)
|
||||
|
||||
private def route: Route = concat(pathPrefix("api" / "v3")(concat(health, apply)))
|
||||
|
||||
private def health: Route = pathPrefix("health")(get(complete(StatusCodes.OK)))
|
||||
|
||||
private def apply: Route = post(path("apply") {
|
||||
entity(as[BakerEvent]) { event =>
|
||||
Future(listenerFunction(event))
|
||||
complete(StatusCodes.OK)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.ing.baker.baas.bakerlistener
|
||||
|
||||
import java.net.InetSocketAddress
|
||||
|
||||
import cats.effect.{ContextShift, IO, Resource, Timer}
|
||||
import com.ing.baker.runtime.scaladsl.BakerEvent
|
||||
import com.ing.baker.baas.bakerlistener.BakeryHttp.ProtoEntityEncoders._
|
||||
import org.http4s._
|
||||
import org.http4s.dsl.io._
|
||||
import org.http4s.implicits._
|
||||
import org.http4s.server.blaze.BlazeServerBuilder
|
||||
import org.http4s.server.{Router, Server}
|
||||
|
||||
object RemoteBakerEventListenerService {
|
||||
|
||||
def resource(listenerFunction: BakerEvent => Unit, address: InetSocketAddress)(implicit timer: Timer[IO], cs: ContextShift[IO]): Resource[IO, Server[IO]] =
|
||||
BlazeServerBuilder[IO]
|
||||
.bindSocketAddress(address)
|
||||
.withHttpApp(new RemoteBakerEventListenerService(listenerFunction).build)
|
||||
.resource
|
||||
}
|
||||
|
||||
final class RemoteBakerEventListenerService(listenerFunction: BakerEvent => Unit) {
|
||||
|
||||
def build: HttpApp[IO] =
|
||||
api.orNotFound
|
||||
|
||||
def api: HttpRoutes[IO] = Router("/api/v3" -> HttpRoutes.of[IO] {
|
||||
|
||||
case GET -> Root / "health" =>
|
||||
Ok("Ok")
|
||||
|
||||
case req@POST -> Root / "baker-event" =>
|
||||
for {
|
||||
event <- req.as[BakerEvent]
|
||||
_ <- IO.pure(IO(listenerFunction(event)).unsafeRunAsyncAndForget())
|
||||
response <- Ok()
|
||||
} yield response
|
||||
})
|
||||
}
|
||||
@@ -1,38 +1,34 @@
|
||||
package com.ing.baker.baas.scaladsl
|
||||
|
||||
import akka.actor.ActorSystem
|
||||
import akka.http.scaladsl.Http
|
||||
import akka.stream.{ActorMaterializer, Materializer}
|
||||
import com.ing.baker.baas.akka.RemoteBakerEventListenerHttp
|
||||
import java.net.InetSocketAddress
|
||||
|
||||
import cats.effect.{ContextShift, IO, Timer}
|
||||
import com.ing.baker.baas.bakerlistener.RemoteBakerEventListenerService
|
||||
import com.ing.baker.baas.common
|
||||
import com.ing.baker.runtime.common.LanguageDataStructures.ScalaApi
|
||||
import com.ing.baker.runtime.scaladsl.BakerEvent
|
||||
import com.ing.baker.runtime.serialization.Encryption
|
||||
import com.typesafe.config.ConfigFactory
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.{Await, Future}
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
object RemoteBakerEventListener extends common.RemoteBakerEventListener[Future] with ScalaApi {
|
||||
|
||||
override type BakerEventType = BakerEvent
|
||||
|
||||
private[baas] def runWith(listenerFunction: BakerEvent => Unit, port: Int, timeout: FiniteDuration)(implicit system: ActorSystem, mat: Materializer, encryption: Encryption): Future[Http.ServerBinding] = {
|
||||
import system.dispatcher
|
||||
RemoteBakerEventListenerHttp.run(listenerFunction)( "0.0.0.0", port).map { hook =>
|
||||
sys.addShutdownHook(Await.result(hook.unbind(), timeout))
|
||||
hook
|
||||
}
|
||||
}
|
||||
|
||||
override def load(listenerFunction: BakerEvent => Unit): Unit = {
|
||||
val timeout: FiniteDuration = 20.seconds
|
||||
|
||||
val config = ConfigFactory.load()
|
||||
val port = config.getInt("baas-component.http-api-port")
|
||||
implicit val system: ActorSystem = ActorSystem("RemoteBakerEventListenerSystem")
|
||||
implicit val materializer: Materializer = ActorMaterializer()(system)
|
||||
implicit val encryption: Encryption = Encryption.from(config)
|
||||
Await.result(runWith(listenerFunction, port, timeout), timeout)
|
||||
|
||||
val address = InetSocketAddress.createUnresolved("0.0.0.0", port)
|
||||
|
||||
implicit val contextShift: ContextShift[IO] = IO.contextShift(ExecutionContext.Implicits.global)
|
||||
implicit val timer: Timer[IO] = IO.timer(ExecutionContext.Implicits.global)
|
||||
|
||||
RemoteBakerEventListenerService
|
||||
.resource(listenerFunction, address)
|
||||
.use(_ => IO.never)
|
||||
.unsafeRunAsyncAndForget()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<configuration>
|
||||
|
||||
<logger name="org.mockserver.log.MockServerEventLog" level="OFF"/>
|
||||
|
||||
<root level="info">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
</root>
|
||||
|
||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<layout class="ch.qos.logback.classic.PatternLayout">
|
||||
<Pattern>
|
||||
%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n
|
||||
</Pattern>
|
||||
</layout>
|
||||
</appender>
|
||||
|
||||
</configuration>
|
||||
@@ -1,73 +0,0 @@
|
||||
package com.ing.baker.baas.akka
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
import akka.actor.ActorSystem
|
||||
import akka.stream.{ActorMaterializer, Materializer}
|
||||
import cats.data.StateT
|
||||
import cats.implicits._
|
||||
import com.ing.baker.baas.akka.RemoteBakerEventListenerSpec._
|
||||
import com.ing.baker.baas.scaladsl.RemoteBakerEventListener
|
||||
import com.ing.baker.runtime.common.LanguageDataStructures.LanguageApi
|
||||
import com.ing.baker.runtime.scaladsl.{BakerEvent, RecipeInstanceCreated}
|
||||
import com.ing.baker.runtime.serialization.Encryption
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import org.scalatest.AsyncFlatSpec
|
||||
import org.scalatest.compatible.Assertion
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.{ExecutionContext, Future, Promise}
|
||||
import scala.util.Try
|
||||
|
||||
class RemoteBakerEventListenerSpec extends AsyncFlatSpec {
|
||||
|
||||
"The Remote Event Listener" should "execute on apply" in {
|
||||
val eventReceived: Promise[BakerEvent] = Promise()
|
||||
val listenerFunction: BakerEvent => Unit =
|
||||
event => eventReceived.complete(Try(event))
|
||||
testWith (listenerFunction, { client =>
|
||||
for {
|
||||
_ <- client(event)
|
||||
result <- eventReceived.future
|
||||
} yield assert(event == result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
object RemoteBakerEventListenerSpec {
|
||||
|
||||
val event: BakerEvent =
|
||||
RecipeInstanceCreated(
|
||||
timeStamp = 42,
|
||||
recipeId = "recipe-id",
|
||||
recipeName = "recipe-name",
|
||||
recipeInstanceId = "recipe-instance-id"
|
||||
)
|
||||
|
||||
def testWith[F[_], Lang <: LanguageApi]
|
||||
(listenerFunction: BakerEvent => Unit, test: RemoteBakerEventListenerClient => Future[Assertion])
|
||||
(implicit ec: ExecutionContext): Future[Assertion] = {
|
||||
val testId: UUID = UUID.randomUUID()
|
||||
val systemName: String = "baas-node-interaction-test-" + testId
|
||||
implicit val system: ActorSystem = ActorSystem(systemName, ConfigFactory.parseString("""akka.loglevel = "OFF" """))
|
||||
implicit val materializer: Materializer = ActorMaterializer()
|
||||
implicit val encryption: Encryption = Encryption.NoEncryption
|
||||
for {
|
||||
(binding, port) <- withOpenPort(5000, port => RemoteBakerEventListener.runWith(listenerFunction, port, 20.seconds))
|
||||
client = RemoteBakerEventListenerClient(s"http://localhost:$port/")
|
||||
assertion <- test(client)
|
||||
_ <- binding.unbind()
|
||||
} yield assertion
|
||||
}
|
||||
|
||||
private def withOpenPort[T](from: Int, f: Int => Future[T])(implicit ec: ExecutionContext): Future[(T, Int)] = {
|
||||
def search(ports: Stream[Int]): Future[(Stream[Int], (T, Int))] =
|
||||
ports match {
|
||||
case #::(port, tail) => f(port).map(tail -> (_, port)).recoverWith {
|
||||
case _: java.net.BindException => search(tail)
|
||||
case other => println("REVIEW withOpenPort function implementation, uncaught exception: " + Console.RED + other + Console.RESET); Future.failed(other)
|
||||
}
|
||||
}
|
||||
StateT(search).run(Stream.from(from, 1)).map(_._2)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.ing.baker.baas.bakerlistener
|
||||
|
||||
import java.net.InetSocketAddress
|
||||
|
||||
import cats.effect.{IO, Resource}
|
||||
import com.ing.baker.baas.testing.BakeryFunSpec
|
||||
import com.ing.baker.runtime.scaladsl.{BakerEvent, RecipeInstanceCreated}
|
||||
import org.http4s.Status
|
||||
import org.scalatest.ConfigMap
|
||||
|
||||
import scala.concurrent.Promise
|
||||
import scala.util.Success
|
||||
|
||||
class RemoteBakerEventListenerSpec extends BakeryFunSpec {
|
||||
|
||||
val event: BakerEvent =
|
||||
RecipeInstanceCreated(
|
||||
timeStamp = 42,
|
||||
recipeId = "recipe-id",
|
||||
recipeName = "recipe-name",
|
||||
recipeInstanceId = "recipe-instance-id"
|
||||
)
|
||||
|
||||
describe("The remote baker recipe event listener") {
|
||||
|
||||
test("execute apply") { context =>
|
||||
val eventReceived: Promise[BakerEvent] = Promise()
|
||||
val listenerFunction: BakerEvent => Unit =
|
||||
event => eventReceived.complete(Success(event))
|
||||
val receivedEvents = IO.fromFuture(IO(eventReceived.future))
|
||||
for {
|
||||
_ <- context.withEventListener(listenerFunction)
|
||||
status <- context.clientFiresEvent(event)
|
||||
result <- receivedEvents
|
||||
} yield {
|
||||
assert(status.code == 200)
|
||||
assert(event == result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case class Context(
|
||||
withEventListener: (BakerEvent => Unit) => IO[Unit],
|
||||
clientFiresEvent: BakerEvent => IO[Status]
|
||||
)
|
||||
|
||||
/** Represents the "sealed resources context" that each test can use. */
|
||||
override type TestContext = Context
|
||||
/** Represents external arguments to the test context builder. */
|
||||
override type TestArguments = Unit
|
||||
|
||||
/** Creates a `Resource` which allocates and liberates the expensive resources each test can use.
|
||||
* For example web servers, network connection, database mocks.
|
||||
*
|
||||
* The objective of this function is to provide "sealed resources context" to each test, that means context
|
||||
* that other tests simply cannot touch.
|
||||
*
|
||||
* @param testArguments arguments built by the `argumentsBuilder` function.
|
||||
* @return the resources each test can use
|
||||
*/
|
||||
override def contextBuilder(testArguments: TestArguments): Resource[IO, TestContext] = {
|
||||
val eventListenerPromise: Promise[BakerEvent => Unit] =
|
||||
Promise()
|
||||
val localEventListener: BakerEvent => Unit =
|
||||
event => eventListenerPromise.future.foreach(_.apply(event))
|
||||
for {
|
||||
server <- RemoteBakerEventListenerService.resource(localEventListener, InetSocketAddress.createUnresolved("localhost", 0))
|
||||
client <- RemoteBakerEventListenerClient.resource(server.baseUri, executionContext)
|
||||
} yield Context(
|
||||
withEventListener = listener => IO(eventListenerPromise.complete(Success(listener))),
|
||||
clientFiresEvent = client.fireEvent
|
||||
)
|
||||
}
|
||||
|
||||
/** Refines the `ConfigMap` populated with the -Dkey=value arguments coming from the "sbt testOnly" command.
|
||||
*
|
||||
* @param config map populated with the -Dkey=value arguments.
|
||||
* @return the data structure used by the `contextBuilder` function.
|
||||
*/
|
||||
override def argumentsBuilder(config: ConfigMap): TestArguments = ()
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.ing.baker.baas.testing
|
||||
|
||||
import cats.effect.{ContextShift, IO, Resource, Timer}
|
||||
import cats.syntax.apply._
|
||||
import org.scalactic.source
|
||||
import org.scalatest.compatible.Assertion
|
||||
import org.scalatest.{ConfigMap, FutureOutcome, Tag, fixture}
|
||||
|
||||
import scala.concurrent.duration._
|
||||
|
||||
/** Abstracts the common test practices across the Bakery project. */
|
||||
abstract class BakeryFunSpec extends fixture.AsyncFunSpecLike {
|
||||
|
||||
implicit val contextShift: ContextShift[IO] =
|
||||
IO.contextShift(executionContext)
|
||||
|
||||
implicit val timer: Timer[IO] =
|
||||
IO.timer(executionContext)
|
||||
|
||||
/** Represents the "sealed resources context" that each test can use. */
|
||||
type TestContext
|
||||
|
||||
/** Represents external arguments to the test context builder. */
|
||||
type TestArguments
|
||||
|
||||
/** Creates a `Resource` which allocates and liberates the expensive resources each test can use.
|
||||
* For example web servers, network connection, database mocks.
|
||||
*
|
||||
* The objective of this function is to provide "sealed resources context" to each test, that means context
|
||||
* that other tests simply cannot touch.
|
||||
*
|
||||
* @param testArguments arguments built by the `argumentsBuilder` function.
|
||||
* @return the resources each test can use
|
||||
*/
|
||||
def contextBuilder(testArguments: TestArguments): Resource[IO, TestContext]
|
||||
|
||||
/** Refines the `ConfigMap` populated with the -Dkey=value arguments coming from the "sbt testOnly" command.
|
||||
*
|
||||
* @param config map populated with the -Dkey=value arguments.
|
||||
* @return the data structure used by the `contextBuilder` function.
|
||||
*/
|
||||
def argumentsBuilder(config: ConfigMap): TestArguments
|
||||
|
||||
/** Runs a single test with a clean sealed context. */
|
||||
def test(specText: String, testTags: Tag*)(runTest: TestContext => IO[Assertion])(implicit pos: source.Position): Unit =
|
||||
it(specText, testTags: _*)(args =>
|
||||
contextBuilder(args).use(runTest).unsafeToFuture())
|
||||
|
||||
/** Tries every second f until it succeeds or until 20 attempts have been made. */
|
||||
def eventually[A](f: IO[A]): IO[A] =
|
||||
within(20.seconds, 20)(f)
|
||||
|
||||
/** Retries the argument f until it succeeds or time/split attempts have been made,
|
||||
* there exists a delay of time for each retry.
|
||||
*/
|
||||
def within[A](time: FiniteDuration, split: Int)(f: IO[A]): IO[A] = {
|
||||
def inner(count: Int, times: FiniteDuration): IO[A] = {
|
||||
if (count < 1) f else f.attempt.flatMap {
|
||||
case Left(_) => IO.sleep(times) *> inner(count - 1, times)
|
||||
case Right(a) => IO(a)
|
||||
}
|
||||
}
|
||||
|
||||
inner(split, time / split)
|
||||
}
|
||||
|
||||
override type FixtureParam = TestArguments
|
||||
|
||||
override def withFixture(test: OneArgAsyncTest): FutureOutcome =
|
||||
test.apply(argumentsBuilder(test.configMap))
|
||||
}
|
||||
@@ -1,13 +1,10 @@
|
||||
package com.ing.baker.baas.javadsl
|
||||
|
||||
import akka.actor.ActorSystem
|
||||
import akka.stream.Materializer
|
||||
import com.ing.baker.baas.scaladsl.{BakerClient => ScalaRemoteBaker}
|
||||
import com.ing.baker.runtime.javadsl.{Baker => JavaBaker}
|
||||
import com.ing.baker.runtime.serialization.Encryption
|
||||
|
||||
object BakerClient {
|
||||
|
||||
def build(hostname: String, actorSystem: ActorSystem, mat: Materializer, encryption: Encryption = Encryption.NoEncryption): JavaBaker =
|
||||
new JavaBaker(ScalaRemoteBaker.build(hostname)(actorSystem, mat, encryption))
|
||||
def build(hostname: String): JavaBaker =
|
||||
new JavaBaker(ScalaRemoteBaker.bounded(hostname).unsafeRunSync())
|
||||
}
|
||||
|
||||
@@ -1,35 +1,47 @@
|
||||
package com.ing.baker.baas.scaladsl
|
||||
|
||||
import akka.actor.ActorSystem
|
||||
import akka.http.scaladsl.Http
|
||||
import akka.http.scaladsl.marshalling.Marshal
|
||||
import akka.http.scaladsl.model.Uri.Path
|
||||
import akka.http.scaladsl.model.{HttpMethods, HttpRequest, MessageEntity, Uri}
|
||||
import akka.stream.Materializer
|
||||
import cats.data.EitherT
|
||||
import cats.effect.{ContextShift, IO, Resource, Timer}
|
||||
import com.ing.baker.baas.protocol.BaaSProto._
|
||||
import com.ing.baker.baas.protocol.BaaSProtocol
|
||||
import com.ing.baker.baas.protocol.MarshallingUtils._
|
||||
import com.ing.baker.baas.protocol.BakeryHttp.ProtoEntityEncoders._
|
||||
import com.ing.baker.il.{CompiledRecipe, RecipeVisualStyle}
|
||||
import com.ing.baker.runtime.common.SensoryEventStatus
|
||||
import com.ing.baker.runtime.scaladsl.{BakerEvent, EventInstance, EventMoment, EventResolutions, InteractionInstance, RecipeEventMetadata, RecipeInformation, RecipeInstanceMetadata, RecipeInstanceState, SensoryEventResult, Baker => ScalaBaker}
|
||||
import com.ing.baker.runtime.serialization.Encryption
|
||||
import com.ing.baker.runtime.serialization.ProtoMap
|
||||
import com.ing.baker.types.Value
|
||||
import org.http4s.EntityDecoder.collectBinary
|
||||
import org.http4s.Method._
|
||||
import org.http4s.client.Client
|
||||
import org.http4s.client.blaze.BlazeClientBuilder
|
||||
import org.http4s.client.dsl.io._
|
||||
import org.http4s._
|
||||
|
||||
import scala.concurrent.Future
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
import scala.util.{Failure, Success, Try}
|
||||
|
||||
object BakerClient {
|
||||
|
||||
def build(hostname: String)(implicit system: ActorSystem, mat: Materializer, encryption: Encryption) =
|
||||
BakerClient(Uri(hostname))
|
||||
/** Uses the global execution context, which is limited to the amount of available cores in the machine. */
|
||||
def bounded(hostname: String): IO[BakerClient] = {
|
||||
val ec = ExecutionContext.Implicits.global
|
||||
resource(Uri.unsafeFromString(hostname), ec)(IO.contextShift(ec), IO.timer(ec)).use(IO.pure)
|
||||
}
|
||||
|
||||
/** use method `use` of the Resource, the client will be acquired and shut down automatically each time
|
||||
* the resulting `IO` is run, each time using the common connection pool.
|
||||
*/
|
||||
def resource(hostname: Uri, pool: ExecutionContext)(implicit cs: ContextShift[IO], timer: Timer[IO]): Resource[IO, BakerClient] = {
|
||||
implicit val ev0 = pool
|
||||
BlazeClientBuilder[IO](pool)
|
||||
.resource
|
||||
.map(new BakerClient(_, hostname))
|
||||
}
|
||||
}
|
||||
|
||||
case class BakerClient(hostname: Uri)(implicit system: ActorSystem, mat: Materializer, encryption: Encryption) extends ScalaBaker {
|
||||
final class BakerClient(client: Client[IO], hostname: Uri)(implicit ec: ExecutionContext) extends ScalaBaker {
|
||||
|
||||
import system.dispatcher
|
||||
|
||||
val root: Path = Path./("api")./("v3")
|
||||
|
||||
def withPath(path: Path): Uri = hostname.withPath(path)
|
||||
val Root = hostname / "api" / "v3"
|
||||
|
||||
/**
|
||||
* Adds a recipe to baker and returns a recipeId for the recipe.
|
||||
@@ -39,13 +51,13 @@ case class BakerClient(hostname: Uri)(implicit system: ActorSystem, mat: Materia
|
||||
* @param compiledRecipe The compiled recipe.
|
||||
* @return A recipeId
|
||||
*/
|
||||
override def addRecipe(compiledRecipe: CompiledRecipe): Future[String] =
|
||||
for {
|
||||
encoded <- Marshal(BaaSProtocol.AddRecipeRequest(compiledRecipe)).to[MessageEntity]
|
||||
request = HttpRequest(method = HttpMethods.POST, uri = withPath(root./("addRecipe")), entity = encoded)
|
||||
response <- Http().singleRequest(request)
|
||||
decoded <- unmarshal[BaaSProtocol.AddRecipeResponse](response).withBakerExceptions
|
||||
} yield decoded.recipeId
|
||||
override def addRecipe(compiledRecipe: CompiledRecipe): Future[String] = {
|
||||
val request = POST(
|
||||
BaaSProtocol.AddRecipeRequest(compiledRecipe),
|
||||
Root / "addRecipe"
|
||||
)
|
||||
handleBakerResponse[BaaSProtocol.AddRecipeResponse, String](request)(_.recipeId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the recipe information for the given RecipeId
|
||||
@@ -53,13 +65,13 @@ case class BakerClient(hostname: Uri)(implicit system: ActorSystem, mat: Materia
|
||||
* @param recipeId
|
||||
* @return
|
||||
*/
|
||||
override def getRecipe(recipeId: String): Future[RecipeInformation] =
|
||||
for {
|
||||
encoded <- Marshal(BaaSProtocol.GetRecipeRequest(recipeId)).to[MessageEntity]
|
||||
request = HttpRequest(method = HttpMethods.POST, uri = withPath(root./("getRecipe")), entity = encoded)
|
||||
response <- Http().singleRequest(request)
|
||||
decoded <- unmarshal[BaaSProtocol.GetRecipeResponse](response).withBakerExceptions
|
||||
} yield decoded.recipeInformation
|
||||
override def getRecipe(recipeId: String): Future[RecipeInformation] = {
|
||||
val request = POST(
|
||||
BaaSProtocol.GetRecipeRequest(recipeId),
|
||||
Root / "getRecipe"
|
||||
)
|
||||
handleBakerResponse[BaaSProtocol.GetRecipeResponse, RecipeInformation](request)(_.recipeInformation)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all recipes added to this baker instance.
|
||||
@@ -67,11 +79,10 @@ case class BakerClient(hostname: Uri)(implicit system: ActorSystem, mat: Materia
|
||||
* @return All recipes in the form of map of recipeId -> CompiledRecipe
|
||||
*/
|
||||
override def getAllRecipes: Future[Map[String, RecipeInformation]] = {
|
||||
val request = HttpRequest(method = HttpMethods.POST, uri = withPath(root./("getAllRecipes")))
|
||||
for {
|
||||
response <- Http().singleRequest(request)
|
||||
decoded <- unmarshal[BaaSProtocol.GetAllRecipesResponse](response).withBakerExceptions
|
||||
} yield decoded.map
|
||||
val request = GET(
|
||||
Root / "getAllRecipes"
|
||||
)
|
||||
handleBakerResponse[BaaSProtocol.GetAllRecipesResponse, Map[String, RecipeInformation]](request)(_.map)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -81,13 +92,13 @@ case class BakerClient(hostname: Uri)(implicit system: ActorSystem, mat: Materia
|
||||
* @param recipeInstanceId The identifier for the newly baked process
|
||||
* @return
|
||||
*/
|
||||
override def bake(recipeId: String, recipeInstanceId: String): Future[Unit] =
|
||||
for {
|
||||
encoded <- Marshal(BaaSProtocol.BakeRequest(recipeId, recipeInstanceId)).to[MessageEntity]
|
||||
request = HttpRequest(method = HttpMethods.POST, uri = withPath(root./("bake")), entity = encoded)
|
||||
response <- Http().singleRequest(request)
|
||||
_ <- unmarshalBakerExceptions(response)
|
||||
} yield ()
|
||||
override def bake(recipeId: String, recipeInstanceId: String): Future[Unit] = {
|
||||
val request = POST(
|
||||
BaaSProtocol.BakeRequest(recipeId, recipeInstanceId),
|
||||
Root / "bake"
|
||||
)
|
||||
handleBakerFailure(request)
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies Baker that an event has happened and waits until the event was accepted but not executed by the process.
|
||||
@@ -100,13 +111,13 @@ case class BakerClient(hostname: Uri)(implicit system: ActorSystem, mat: Materia
|
||||
* @param event The event object
|
||||
* @param correlationId Id used to ensure the process instance handles unique events
|
||||
*/
|
||||
override def fireEventAndResolveWhenReceived(recipeInstanceId: String, event: EventInstance, correlationId: Option[String]): Future[SensoryEventStatus] =
|
||||
for {
|
||||
encoded <- Marshal(BaaSProtocol.FireEventAndResolveWhenReceivedRequest(recipeInstanceId, event, correlationId)).to[MessageEntity]
|
||||
request = HttpRequest(method = HttpMethods.POST, uri = withPath(root./("fireEventAndResolveWhenReceived")), entity = encoded)
|
||||
response <- Http().singleRequest(request)
|
||||
decoded <- unmarshal[BaaSProtocol.FireEventAndResolveWhenReceivedResponse](response).withBakerExceptions
|
||||
} yield decoded.sensoryEventStatus
|
||||
override def fireEventAndResolveWhenReceived(recipeInstanceId: String, event: EventInstance, correlationId: Option[String]): Future[SensoryEventStatus] = {
|
||||
val request = POST(
|
||||
BaaSProtocol.FireEventAndResolveWhenReceivedRequest(recipeInstanceId, event, correlationId),
|
||||
Root / "fireEventAndResolveWhenReceived"
|
||||
)
|
||||
handleBakerResponse[BaaSProtocol.FireEventAndResolveWhenReceivedResponse, SensoryEventStatus](request)(_.sensoryEventStatus)
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies Baker that an event has happened and waits until all the actions which depend on this event are executed.
|
||||
@@ -119,13 +130,13 @@ case class BakerClient(hostname: Uri)(implicit system: ActorSystem, mat: Materia
|
||||
* @param event The event object
|
||||
* @param correlationId Id used to ensure the process instance handles unique events
|
||||
*/
|
||||
override def fireEventAndResolveWhenCompleted(recipeInstanceId: String, event: EventInstance, correlationId: Option[String]): Future[SensoryEventResult] =
|
||||
for {
|
||||
encoded <- Marshal(BaaSProtocol.FireEventAndResolveWhenCompletedRequest(recipeInstanceId, event, correlationId)).to[MessageEntity]
|
||||
request = HttpRequest(method = HttpMethods.POST, uri = withPath(root./("fireEventAndResolveWhenCompleted")), entity = encoded)
|
||||
response <- Http().singleRequest(request)
|
||||
decoded <- unmarshal[BaaSProtocol.FireEventAndResolveWhenCompletedResponse](response).withBakerExceptions
|
||||
} yield decoded.sensoryEventResult
|
||||
override def fireEventAndResolveWhenCompleted(recipeInstanceId: String, event: EventInstance, correlationId: Option[String]): Future[SensoryEventResult] = {
|
||||
val request = POST(
|
||||
BaaSProtocol.FireEventAndResolveWhenCompletedRequest(recipeInstanceId, event, correlationId),
|
||||
Root / "fireEventAndResolveWhenCompleted"
|
||||
)
|
||||
handleBakerResponse[BaaSProtocol.FireEventAndResolveWhenCompletedResponse, SensoryEventResult](request)(_.sensoryEventResult)
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies Baker that an event has happened and waits until an specific event has executed.
|
||||
@@ -139,13 +150,13 @@ case class BakerClient(hostname: Uri)(implicit system: ActorSystem, mat: Materia
|
||||
* @param onEvent The name of the event to wait for
|
||||
* @param correlationId Id used to ensure the process instance handles unique events
|
||||
*/
|
||||
override def fireEventAndResolveOnEvent(recipeInstanceId: String, event: EventInstance, onEvent: String, correlationId: Option[String]): Future[SensoryEventResult] =
|
||||
for {
|
||||
encoded <- Marshal(BaaSProtocol.FireEventAndResolveOnEventRequest(recipeInstanceId, event, onEvent, correlationId)).to[MessageEntity]
|
||||
request = HttpRequest(method = HttpMethods.POST, uri = withPath(root./("fireEventAndResolveOnEvent")), entity = encoded)
|
||||
response <- Http().singleRequest(request)
|
||||
decoded <- unmarshal[BaaSProtocol.FireEventAndResolveOnEventResponse](response).withBakerExceptions
|
||||
} yield decoded.sensoryEventResult
|
||||
override def fireEventAndResolveOnEvent(recipeInstanceId: String, event: EventInstance, onEvent: String, correlationId: Option[String]): Future[SensoryEventResult] = {
|
||||
val request = POST(
|
||||
BaaSProtocol.FireEventAndResolveOnEventRequest(recipeInstanceId, event, onEvent, correlationId),
|
||||
Root / "fireEventAndResolveOnEvent"
|
||||
)
|
||||
handleBakerResponse[BaaSProtocol.FireEventAndResolveOnEventResponse, SensoryEventResult](request)(_.sensoryEventResult)
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies Baker that an event has happened and provides 2 async handlers, one for when the event was accepted by
|
||||
@@ -160,12 +171,6 @@ case class BakerClient(hostname: Uri)(implicit system: ActorSystem, mat: Materia
|
||||
* @param correlationId Id used to ensure the process instance handles unique events
|
||||
*/
|
||||
override def fireEvent(recipeInstanceId: String, event: EventInstance, correlationId: Option[String]): EventResolutions = {
|
||||
for {
|
||||
encoded <- Marshal(BaaSProtocol.FireEventRequest(recipeInstanceId, event, correlationId)).to[MessageEntity]
|
||||
request = HttpRequest(method = HttpMethods.POST, uri = withPath(root./("fireEvent")), entity = encoded)
|
||||
response <- Http().singleRequest(request)
|
||||
//decoded <- unmarshal(response)[BaaSProtocol.???] TODO f.withBakerExceptionsigure out what to do on this situation with the two futures
|
||||
} yield () //decoded.recipeInformation
|
||||
???
|
||||
}
|
||||
|
||||
@@ -180,11 +185,10 @@ case class BakerClient(hostname: Uri)(implicit system: ActorSystem, mat: Materia
|
||||
* @return An index of all processes
|
||||
*/
|
||||
override def getAllRecipeInstancesMetadata: Future[Set[RecipeInstanceMetadata]] = {
|
||||
val request = HttpRequest(method = HttpMethods.POST, uri = withPath(root./("getAllRecipeInstancesMetadata")))
|
||||
for {
|
||||
response <- Http().singleRequest(request)
|
||||
decoded <- unmarshal[BaaSProtocol.GetAllRecipeInstancesMetadataResponse](response).withBakerExceptions
|
||||
} yield decoded.set
|
||||
val request = GET(
|
||||
Root / "getAllRecipeInstancesMetadata"
|
||||
)
|
||||
handleBakerResponse[BaaSProtocol.GetAllRecipeInstancesMetadataResponse, Set[RecipeInstanceMetadata]](request)(_.set)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -193,13 +197,13 @@ case class BakerClient(hostname: Uri)(implicit system: ActorSystem, mat: Materia
|
||||
* @param recipeInstanceId The process identifier
|
||||
* @return The process state.
|
||||
*/
|
||||
override def getRecipeInstanceState(recipeInstanceId: String): Future[RecipeInstanceState] =
|
||||
for {
|
||||
encoded <- Marshal(BaaSProtocol.GetRecipeInstanceStateRequest(recipeInstanceId)).to[MessageEntity]
|
||||
request = HttpRequest(method = HttpMethods.POST, uri = withPath(root./("getRecipeInstanceState")), entity = encoded)
|
||||
response <- Http().singleRequest(request)
|
||||
decoded <- unmarshal[BaaSProtocol.GetRecipeInstanceStateResponse](response).withBakerExceptions
|
||||
} yield decoded.recipeInstanceState
|
||||
override def getRecipeInstanceState(recipeInstanceId: String): Future[RecipeInstanceState] = {
|
||||
val request = POST(
|
||||
BaaSProtocol.GetRecipeInstanceStateRequest(recipeInstanceId),
|
||||
Root / "getRecipeInstanceState"
|
||||
)
|
||||
handleBakerResponse[BaaSProtocol.GetRecipeInstanceStateResponse, RecipeInstanceState](request)(_.recipeInstanceState)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all provided ingredients for a given RecipeInstance id.
|
||||
@@ -234,13 +238,13 @@ case class BakerClient(hostname: Uri)(implicit system: ActorSystem, mat: Materia
|
||||
* @param recipeInstanceId The process identifier.
|
||||
* @return A visual (.dot) representation of the process state.
|
||||
*/
|
||||
override def getVisualState(recipeInstanceId: String, style: RecipeVisualStyle): Future[String] =
|
||||
for {
|
||||
encoded <- Marshal(BaaSProtocol.GetVisualStateRequest(recipeInstanceId)).to[MessageEntity]
|
||||
request = HttpRequest(method = HttpMethods.POST, uri = withPath(root./("getVisualState")), entity = encoded)
|
||||
response <- Http().singleRequest(request)
|
||||
decoded <- unmarshal[BaaSProtocol.GetVisualStateResponse](response).withBakerExceptions
|
||||
} yield decoded.state
|
||||
override def getVisualState(recipeInstanceId: String, style: RecipeVisualStyle): Future[String] = {
|
||||
val request = POST(
|
||||
BaaSProtocol.GetVisualStateRequest(recipeInstanceId),
|
||||
Root / "getVisualState"
|
||||
)
|
||||
handleBakerResponse[BaaSProtocol.GetVisualStateResponse, String](request)(_.state)
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a listener to all runtime events for recipes with the given name run in this baker instance.
|
||||
@@ -289,20 +293,20 @@ case class BakerClient(hostname: Uri)(implicit system: ActorSystem, mat: Materia
|
||||
* Attempts to gracefully shutdown the baker system.
|
||||
*/
|
||||
override def gracefulShutdown(): Future[Unit] =
|
||||
system.terminate().map(_ => ())
|
||||
throw new NotImplementedError("Use the cats.effect.Resource mechanisms for this, you probably don't need to do anything, safe to ignore")
|
||||
|
||||
/**
|
||||
* Retries a blocked interaction.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
override def retryInteraction(recipeInstanceId: String, interactionName: String): Future[Unit] =
|
||||
for {
|
||||
encoded <- Marshal(BaaSProtocol.RetryInteractionRequest(recipeInstanceId, interactionName)).to[MessageEntity]
|
||||
request = HttpRequest(method = HttpMethods.POST, uri = withPath(root./("retryInteraction")), entity = encoded)
|
||||
response <- Http().singleRequest(request)
|
||||
_ <- unmarshalBakerExceptions(response)
|
||||
} yield ()
|
||||
override def retryInteraction(recipeInstanceId: String, interactionName: String): Future[Unit] = {
|
||||
val request = POST(
|
||||
BaaSProtocol.RetryInteractionRequest(recipeInstanceId, interactionName),
|
||||
Root / "retryInteraction"
|
||||
)
|
||||
handleBakerFailure(request)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a blocked interaction by specifying it's output.
|
||||
@@ -311,24 +315,61 @@ case class BakerClient(hostname: Uri)(implicit system: ActorSystem, mat: Materia
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
override def resolveInteraction(recipeInstanceId: String, interactionName: String, event: EventInstance): Future[Unit] =
|
||||
for {
|
||||
encoded <- Marshal(BaaSProtocol.ResolveInteractionRequest(recipeInstanceId, interactionName, event)).to[MessageEntity]
|
||||
request = HttpRequest(method = HttpMethods.POST, uri = withPath(root./("resolveInteraction")), entity = encoded)
|
||||
response <- Http().singleRequest(request)
|
||||
_ <- unmarshalBakerExceptions(response)
|
||||
} yield ()
|
||||
override def resolveInteraction(recipeInstanceId: String, interactionName: String, event: EventInstance): Future[Unit] = {
|
||||
val request = POST(
|
||||
BaaSProtocol.ResolveInteractionRequest(recipeInstanceId, interactionName, event),
|
||||
Root / "resolveInteraction"
|
||||
)
|
||||
handleBakerFailure(request)
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the retrying of an interaction.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
override def stopRetryingInteraction(recipeInstanceId: String, interactionName: String): Future[Unit] =
|
||||
for {
|
||||
encoded <- Marshal(BaaSProtocol.StopRetryingInteractionRequest(recipeInstanceId, interactionName)).to[MessageEntity]
|
||||
request = HttpRequest(method = HttpMethods.POST, uri = withPath(root./("stopRetryingInteraction")), entity = encoded)
|
||||
response <- Http().singleRequest(request)
|
||||
_ <- unmarshalBakerExceptions(response)
|
||||
} yield ()
|
||||
override def stopRetryingInteraction(recipeInstanceId: String, interactionName: String): Future[Unit] = {
|
||||
val request = POST(
|
||||
BaaSProtocol.StopRetryingInteractionRequest(recipeInstanceId, interactionName),
|
||||
Root / "stopRetryingInteraction"
|
||||
)
|
||||
handleBakerFailure(request)
|
||||
}
|
||||
|
||||
private type WithBakerException[A] = Either[BaaSProtocol.BaaSRemoteFailure, A]
|
||||
|
||||
private implicit def withBakerExeptionEntityDecoder[A, P <: ProtoMessage[P]](implicit protoMap: ProtoMap[A, P]): EntityDecoder[IO, WithBakerException[A]] =
|
||||
EntityDecoder.decodeBy(MediaType.application.`octet-stream`)(collectBinary[IO]).map(_.toArray)
|
||||
.flatMapR { bytes =>
|
||||
val eitherTry: Try[WithBakerException[A]] =
|
||||
baaSRemoteFailureProto.fromByteArray(bytes).map[WithBakerException[A]](Left(_))
|
||||
.orElse(protoMap.fromByteArray(bytes).map[WithBakerException[A]](Right(_)))
|
||||
eitherTry match {
|
||||
case Success(a) =>
|
||||
EitherT.fromEither[IO](Right(a))
|
||||
case Failure(exception) =>
|
||||
EitherT.fromEither[IO](Left(MalformedMessageBodyFailure(exception.getMessage, Some(exception))))
|
||||
}
|
||||
}
|
||||
|
||||
final class HandleBakerResponsePartial[A, R] {
|
||||
def apply[P <: ProtoMessage[P]](request: IO[Request[IO]])(f: A => R)(implicit protoMap: ProtoMap[A, P]): Future[R] =
|
||||
client
|
||||
.expect[WithBakerException[A]](request)
|
||||
.flatMap(_.fold(failure => IO.raiseError(failure.error), IO.pure))
|
||||
.map(f)
|
||||
.unsafeToFuture()
|
||||
}
|
||||
|
||||
private def handleBakerResponse[A, R]: HandleBakerResponsePartial[A, R] = new HandleBakerResponsePartial[A, R]
|
||||
|
||||
private def handleBakerFailure(request: IO[Request[IO]]): Future[Unit] =
|
||||
request.flatMap(client.run(_).use(response => response.contentType match {
|
||||
case Some(contentType) if contentType.mediaType == MediaType.application.`octet-stream` =>
|
||||
EntityDecoder[IO, BaaSProtocol.BaaSRemoteFailure]
|
||||
.decode(response, strict = true)
|
||||
.value
|
||||
.flatMap(_.fold(IO.raiseError(_), e => IO.raiseError(e.error)))
|
||||
case _ => IO.unit
|
||||
})).unsafeToFuture()
|
||||
}
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
package com.ing.baker.baas.akka
|
||||
|
||||
import akka.actor.ActorSystem
|
||||
import akka.http.scaladsl.Http
|
||||
import akka.http.scaladsl.marshalling.{Marshaller, ToEntityMarshaller}
|
||||
import akka.http.scaladsl.model.{MediaTypes, StatusCodes}
|
||||
import akka.http.scaladsl.server.Directives._
|
||||
import akka.http.scaladsl.server.Route
|
||||
import akka.http.scaladsl.unmarshalling.{FromEntityUnmarshaller, Unmarshaller}
|
||||
import akka.stream.Materializer
|
||||
import com.ing.baker.baas.protocol
|
||||
import com.ing.baker.baas.protocol.DistributedEventPublishingProto._
|
||||
import com.ing.baker.runtime.scaladsl.{EventInstance, RecipeEventMetadata}
|
||||
import com.ing.baker.runtime.serialization.{Encryption, ProtoMap}
|
||||
|
||||
import scala.concurrent.Future
|
||||
|
||||
object RemoteEventListenerHttp {
|
||||
|
||||
def run(listenerFunction: (RecipeEventMetadata, EventInstance) => Unit)(host: String, port: Int)(implicit system: ActorSystem, mat: Materializer, encryption: Encryption): Future[Http.ServerBinding] = {
|
||||
import system.dispatcher
|
||||
val server = new RemoteEventListenerHttp(listenerFunction)(system, encryption)
|
||||
Http().bindAndHandle(server.route, host, port)
|
||||
}
|
||||
}
|
||||
|
||||
class RemoteEventListenerHttp(listenerFunction: (RecipeEventMetadata, EventInstance) => Unit)(implicit system: ActorSystem, encryption: Encryption) {
|
||||
|
||||
import system.dispatcher
|
||||
|
||||
private type ProtoMessage[A] = scalapb.GeneratedMessage with scalapb.Message[A]
|
||||
|
||||
private implicit def protoMarshaller[A, P <: ProtoMessage[P]](implicit mapping: ProtoMap[A, P]): ToEntityMarshaller[A] =
|
||||
Marshaller.ByteArrayMarshaller.wrap(MediaTypes.`application/octet-stream`)(mapping.toByteArray)
|
||||
|
||||
private implicit def protoUnmarshaller[A, P <: ProtoMessage[P]](implicit mapping: ProtoMap[A, P]): FromEntityUnmarshaller[A] =
|
||||
Unmarshaller.byteArrayUnmarshaller.map(mapping.fromByteArray(_).get)
|
||||
|
||||
private implicit def protoEitherUnmarshaller[A, P0 <: ProtoMessage[P0], B, P1 <: ProtoMessage[P1]](implicit m1: ProtoMap[A, P0], m2: ProtoMap[B, P1]): FromEntityUnmarshaller[Either[A, B]] =
|
||||
Unmarshaller.byteArrayUnmarshaller.map { byteArray =>
|
||||
m1.fromByteArray(byteArray).map(Left(_)).orElse(m2.fromByteArray(byteArray).map(Right(_))).get
|
||||
}
|
||||
|
||||
private def route: Route = concat(pathPrefix("api" / "v3")(concat(health, apply)))
|
||||
|
||||
private def health: Route = pathPrefix("health")(get(complete(StatusCodes.OK)))
|
||||
|
||||
private def apply: Route = post(path("apply") {
|
||||
entity(as[protocol.ProtocolDistributedEventPublishing.Event]) { request =>
|
||||
Future(listenerFunction(request.recipeEventMetadata, request.event))
|
||||
complete(StatusCodes.OK)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -3,14 +3,10 @@ package com.ing.baker.baas.javadsl
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.function.BiConsumer
|
||||
|
||||
import akka.actor.ActorSystem
|
||||
import com.ing.baker.baas.common
|
||||
import com.ing.baker.baas.scaladsl
|
||||
import com.ing.baker.baas.{common, scaladsl}
|
||||
import com.ing.baker.runtime.common.LanguageDataStructures.JavaApi
|
||||
import com.ing.baker.runtime.javadsl.{EventInstance, RecipeEventMetadata}
|
||||
|
||||
import scala.compat.java8.FutureConverters
|
||||
|
||||
object RemoteEventListener extends common.RemoteEventListener[CompletableFuture] with JavaApi {
|
||||
|
||||
override type EventInstanceType = EventInstance
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.ing.baker.baas.recipelistener
|
||||
|
||||
import java.net.InetSocketAddress
|
||||
|
||||
import cats.effect.{ContextShift, IO, Resource, Timer}
|
||||
import org.http4s._
|
||||
import org.http4s.dsl.io._
|
||||
import org.http4s.implicits._
|
||||
import org.http4s.server.{Router, Server}
|
||||
import com.ing.baker.baas.protocol
|
||||
import com.ing.baker.baas.protocol.DistributedEventPublishingProto._
|
||||
import com.ing.baker.baas.recipelistener.BakeryHttp.ProtoEntityEncoders._
|
||||
import com.ing.baker.runtime.scaladsl.{EventInstance, RecipeEventMetadata}
|
||||
import org.http4s.server.blaze.BlazeServerBuilder
|
||||
|
||||
object RemoteEventListenerService {
|
||||
|
||||
def resource(listenerFunction: (RecipeEventMetadata, EventInstance) => Unit, address: InetSocketAddress)(implicit timer: Timer[IO], cs: ContextShift[IO]): Resource[IO, Server[IO]] =
|
||||
BlazeServerBuilder[IO]
|
||||
.bindSocketAddress(address)
|
||||
.withHttpApp(new RemoteEventListenerService(listenerFunction).build)
|
||||
.resource
|
||||
}
|
||||
|
||||
final class RemoteEventListenerService(listenerFunction: (RecipeEventMetadata, EventInstance) => Unit)(implicit timer: Timer[IO], cs: ContextShift[IO]) {
|
||||
|
||||
def build: HttpApp[IO] =
|
||||
api.orNotFound
|
||||
|
||||
def api: HttpRoutes[IO] = Router("/api/v3" -> HttpRoutes.of[IO] {
|
||||
|
||||
case GET -> Root / "health" =>
|
||||
Ok("Ok")
|
||||
|
||||
case req@POST -> Root / "recipe-event" =>
|
||||
for {
|
||||
event <- req.as[protocol.ProtocolDistributedEventPublishing.Event]
|
||||
_ <- IO.pure(IO(listenerFunction(event.recipeEventMetadata, event.event)).unsafeRunAsyncAndForget())
|
||||
response <- Ok()
|
||||
} yield response
|
||||
})
|
||||
}
|
||||
@@ -1,17 +1,15 @@
|
||||
package com.ing.baker.baas.scaladsl
|
||||
|
||||
import akka.actor.ActorSystem
|
||||
import akka.http.scaladsl.Http
|
||||
import akka.stream.{ActorMaterializer, Materializer}
|
||||
import com.ing.baker.baas.akka.RemoteEventListenerHttp
|
||||
import java.net.InetSocketAddress
|
||||
|
||||
import cats.effect.{ContextShift, IO, Timer}
|
||||
import com.ing.baker.baas.common
|
||||
import com.ing.baker.baas.recipelistener.RemoteEventListenerService
|
||||
import com.ing.baker.runtime.common.LanguageDataStructures.ScalaApi
|
||||
import com.ing.baker.runtime.scaladsl.{EventInstance, RecipeEventMetadata}
|
||||
import com.ing.baker.runtime.serialization.Encryption
|
||||
import com.typesafe.config.ConfigFactory
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.{Await, Future}
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
object RemoteEventListener extends common.RemoteEventListener[Future] with ScalaApi {
|
||||
|
||||
@@ -19,28 +17,19 @@ object RemoteEventListener extends common.RemoteEventListener[Future] with Scala
|
||||
|
||||
override type RecipeEventMetadataType = RecipeEventMetadata
|
||||
|
||||
private[baas] def runWith(listenerFunction: (RecipeEventMetadata, EventInstance) => Unit, port: Int, timeout: FiniteDuration)(implicit system: ActorSystem, mat: Materializer, encryption: Encryption): Future[Http.ServerBinding] = {
|
||||
// Temp
|
||||
println(Console.YELLOW + s"Starting remote interaction [${listenerFunction.toString}]" + Console.RESET)
|
||||
// Temp
|
||||
println(Console.YELLOW + s"Starting the http server on port $port" + Console.RESET)
|
||||
import system.dispatcher
|
||||
RemoteEventListenerHttp.run(listenerFunction)( "0.0.0.0", port).map { hook =>
|
||||
println(Console.GREEN + "Http server started" + Console.RESET)
|
||||
println(hook.localAddress)
|
||||
sys.addShutdownHook(Await.result(hook.unbind(), timeout))
|
||||
hook
|
||||
}
|
||||
}
|
||||
|
||||
override def load(listenerFunction: (RecipeEventMetadata, EventInstance) => Unit): Unit = {
|
||||
val timeout: FiniteDuration = 20.seconds
|
||||
val config = ConfigFactory.load()
|
||||
val port = config.getInt("baas-component.http-api-port")
|
||||
implicit val system: ActorSystem = ActorSystem("RemoteEventListenerSystem")
|
||||
implicit val materializer: Materializer = ActorMaterializer()(system)
|
||||
implicit val encryption: Encryption = Encryption.from(config)
|
||||
Await.result(runWith(listenerFunction, port, timeout), timeout)
|
||||
|
||||
val address = InetSocketAddress.createUnresolved("0.0.0.0", port)
|
||||
|
||||
implicit val contextShift: ContextShift[IO] = IO.contextShift(ExecutionContext.Implicits.global)
|
||||
implicit val timer: Timer[IO] = IO.timer(ExecutionContext.Implicits.global)
|
||||
|
||||
RemoteEventListenerService
|
||||
.resource(listenerFunction, address)
|
||||
.use(_ => IO.never)
|
||||
.unsafeRunAsyncAndForget()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
17
baas-node-event-listener/src/test/resources/logback-test.xml
Normal file
17
baas-node-event-listener/src/test/resources/logback-test.xml
Normal file
@@ -0,0 +1,17 @@
|
||||
<configuration>
|
||||
|
||||
<logger name="org.mockserver.log.MockServerEventLog" level="OFF"/>
|
||||
|
||||
<root level="info">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
</root>
|
||||
|
||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<layout class="ch.qos.logback.classic.PatternLayout">
|
||||
<Pattern>
|
||||
%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n
|
||||
</Pattern>
|
||||
</layout>
|
||||
</appender>
|
||||
|
||||
</configuration>
|
||||
@@ -1,82 +0,0 @@
|
||||
package com.ing.baker.baas.akka
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
import akka.actor.ActorSystem
|
||||
import akka.stream.{ActorMaterializer, Materializer}
|
||||
import cats.data.StateT
|
||||
import cats.implicits._
|
||||
import com.ing.baker.baas.akka.RemoteEventListenerSpec._
|
||||
import com.ing.baker.baas.scaladsl.RemoteEventListener
|
||||
import com.ing.baker.runtime.common.LanguageDataStructures.LanguageApi
|
||||
import com.ing.baker.runtime.scaladsl.{EventInstance, RecipeEventMetadata}
|
||||
import com.ing.baker.runtime.serialization.Encryption
|
||||
import com.ing.baker.types.PrimitiveValue
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import org.scalatest.AsyncFlatSpec
|
||||
import org.scalatest.compatible.Assertion
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.{ExecutionContext, Future, Promise}
|
||||
import scala.util.Try
|
||||
|
||||
class RemoteEventListenerSpec extends AsyncFlatSpec {
|
||||
|
||||
"The Remote Event Listener" should "execute on apply" in {
|
||||
val eventReceived: Promise[(RecipeEventMetadata, EventInstance)] = Promise()
|
||||
val listenerFunction: (RecipeEventMetadata, EventInstance) => Unit =
|
||||
(metadata, event) => eventReceived.complete(Try(metadata -> event))
|
||||
testWith (listenerFunction, { client =>
|
||||
for {
|
||||
_ <- client(recipeEventMetadata, event)
|
||||
result <- eventReceived.future
|
||||
} yield assert(recipeEventMetadata == result._1 && event == result._2)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
object RemoteEventListenerSpec {
|
||||
|
||||
val recipeEventMetadata: RecipeEventMetadata =
|
||||
RecipeEventMetadata(
|
||||
recipeId = "recipe-id",
|
||||
recipeName = "Recipe",
|
||||
recipeInstanceId = "instance-id"
|
||||
)
|
||||
|
||||
val event: EventInstance =
|
||||
EventInstance(
|
||||
name = "Result",
|
||||
providedIngredients = Map(
|
||||
"data" -> PrimitiveValue("hello-world")
|
||||
)
|
||||
)
|
||||
|
||||
def testWith[F[_], Lang <: LanguageApi]
|
||||
(listenerFunction: (RecipeEventMetadata, EventInstance) => Unit, test: (RemoteEventListenerClient) => Future[Assertion])
|
||||
(implicit ec: ExecutionContext): Future[Assertion] = {
|
||||
val testId: UUID = UUID.randomUUID()
|
||||
val systemName: String = "baas-node-interaction-test-" + testId
|
||||
implicit val system: ActorSystem = ActorSystem(systemName, ConfigFactory.parseString("""akka.loglevel = "OFF" """))
|
||||
implicit val materializer: Materializer = ActorMaterializer()
|
||||
implicit val encryption: Encryption = Encryption.NoEncryption
|
||||
for {
|
||||
(binding, port) <- withOpenPort(5000, port => RemoteEventListener.runWith(listenerFunction, port, 20.seconds))
|
||||
client = RemoteEventListenerClient(s"http://localhost:$port/")
|
||||
assertion <- test(client)
|
||||
_ <- binding.unbind()
|
||||
} yield assertion
|
||||
}
|
||||
|
||||
private def withOpenPort[T](from: Int, f: Int => Future[T])(implicit ec: ExecutionContext): Future[(T, Int)] = {
|
||||
def search(ports: Stream[Int]): Future[(Stream[Int], (T, Int))] =
|
||||
ports match {
|
||||
case #::(port, tail) => f(port).map(tail -> (_, port)).recoverWith {
|
||||
case _: java.net.BindException => search(tail)
|
||||
//case _: ChannelException => search(tail)
|
||||
case other => println("REVIEW withOpenPort function implementation, uncaught exception: " + Console.RED + other + Console.RESET); Future.failed(other)
|
||||
}
|
||||
}
|
||||
StateT(search).run(Stream.from(from, 1)).map(_._2)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package com.ing.baker.baas.recipelistener
|
||||
|
||||
import java.net.InetSocketAddress
|
||||
|
||||
import cats.effect.{IO, Resource}
|
||||
import com.ing.baker.baas.testing.BakeryFunSpec
|
||||
import com.ing.baker.runtime.scaladsl.{EventInstance, RecipeEventMetadata}
|
||||
import com.ing.baker.types.PrimitiveValue
|
||||
import org.http4s.{Status, Uri}
|
||||
import org.scalatest.ConfigMap
|
||||
|
||||
import scala.concurrent.Promise
|
||||
import scala.util.Success
|
||||
|
||||
class RemoteEventListenerSpec extends BakeryFunSpec {
|
||||
|
||||
val recipeEventMetadata: RecipeEventMetadata =
|
||||
RecipeEventMetadata(
|
||||
recipeId = "recipe-id",
|
||||
recipeName = "Recipe",
|
||||
recipeInstanceId = "instance-id"
|
||||
)
|
||||
|
||||
val event: EventInstance =
|
||||
EventInstance(
|
||||
name = "Result",
|
||||
providedIngredients = Map(
|
||||
"data" -> PrimitiveValue("hello-world")
|
||||
)
|
||||
)
|
||||
|
||||
describe("The remote recipe event listener") {
|
||||
|
||||
test("execute apply") { context =>
|
||||
val eventReceived: Promise[(RecipeEventMetadata, EventInstance)] = Promise()
|
||||
val listenerFunction: (RecipeEventMetadata, EventInstance) => Unit =
|
||||
(metadata, event) => eventReceived.complete(Success(metadata -> event))
|
||||
val receivedEvents = IO.fromFuture(IO(eventReceived.future))
|
||||
for {
|
||||
_ <- context.withEventListener(listenerFunction)
|
||||
status <- context.clientFiresEvent(recipeEventMetadata, event)
|
||||
result <- receivedEvents
|
||||
} yield {
|
||||
assert(status.code == 200)
|
||||
assert(recipeEventMetadata == result._1 && event == result._2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case class Context(
|
||||
withEventListener: ((RecipeEventMetadata, EventInstance) => Unit) => IO[Unit],
|
||||
clientFiresEvent: (RecipeEventMetadata, EventInstance) => IO[Status]
|
||||
)
|
||||
|
||||
/** Represents the "sealed resources context" that each test can use. */
|
||||
override type TestContext = Context
|
||||
/** Represents external arguments to the test context builder. */
|
||||
override type TestArguments = Unit
|
||||
|
||||
/** Creates a `Resource` which allocates and liberates the expensive resources each test can use.
|
||||
* For example web servers, network connection, database mocks.
|
||||
*
|
||||
* The objective of this function is to provide "sealed resources context" to each test, that means context
|
||||
* that other tests simply cannot touch.
|
||||
*
|
||||
* @param testArguments arguments built by the `argumentsBuilder` function.
|
||||
* @return the resources each test can use
|
||||
*/
|
||||
override def contextBuilder(testArguments: TestArguments): Resource[IO, TestContext] = {
|
||||
val eventListenerPromise: Promise[(RecipeEventMetadata, EventInstance) => Unit] =
|
||||
Promise()
|
||||
val localEventListener: (RecipeEventMetadata, EventInstance) => Unit =
|
||||
(m, e) => eventListenerPromise.future.foreach(_.apply(m, e))
|
||||
for {
|
||||
server <- RemoteEventListenerService.resource(localEventListener, InetSocketAddress.createUnresolved("localhost", 0))
|
||||
client <- RemoteEventListenerClient.resource(server.baseUri, executionContext)
|
||||
} yield Context(
|
||||
withEventListener = listener => IO(eventListenerPromise.complete(Success(listener))),
|
||||
clientFiresEvent = client.fireEvent
|
||||
)
|
||||
}
|
||||
|
||||
/** Refines the `ConfigMap` populated with the -Dkey=value arguments coming from the "sbt testOnly" command.
|
||||
*
|
||||
* @param config map populated with the -Dkey=value arguments.
|
||||
* @return the data structure used by the `contextBuilder` function.
|
||||
*/
|
||||
override def argumentsBuilder(config: ConfigMap): TestArguments = ()
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.ing.baker.baas.testing
|
||||
|
||||
import cats.effect.{ContextShift, IO, Resource, Timer}
|
||||
import cats.syntax.apply._
|
||||
import org.scalactic.source
|
||||
import org.scalatest.compatible.Assertion
|
||||
import org.scalatest.{ConfigMap, FutureOutcome, Tag, fixture}
|
||||
|
||||
import scala.concurrent.duration._
|
||||
|
||||
/** Abstracts the common test practices across the Bakery project. */
|
||||
abstract class BakeryFunSpec extends fixture.AsyncFunSpecLike {
|
||||
|
||||
implicit val contextShift: ContextShift[IO] =
|
||||
IO.contextShift(executionContext)
|
||||
|
||||
implicit val timer: Timer[IO] =
|
||||
IO.timer(executionContext)
|
||||
|
||||
/** Represents the "sealed resources context" that each test can use. */
|
||||
type TestContext
|
||||
|
||||
/** Represents external arguments to the test context builder. */
|
||||
type TestArguments
|
||||
|
||||
/** Creates a `Resource` which allocates and liberates the expensive resources each test can use.
|
||||
* For example web servers, network connection, database mocks.
|
||||
*
|
||||
* The objective of this function is to provide "sealed resources context" to each test, that means context
|
||||
* that other tests simply cannot touch.
|
||||
*
|
||||
* @param testArguments arguments built by the `argumentsBuilder` function.
|
||||
* @return the resources each test can use
|
||||
*/
|
||||
def contextBuilder(testArguments: TestArguments): Resource[IO, TestContext]
|
||||
|
||||
/** Refines the `ConfigMap` populated with the -Dkey=value arguments coming from the "sbt testOnly" command.
|
||||
*
|
||||
* @param config map populated with the -Dkey=value arguments.
|
||||
* @return the data structure used by the `contextBuilder` function.
|
||||
*/
|
||||
def argumentsBuilder(config: ConfigMap): TestArguments
|
||||
|
||||
/** Runs a single test with a clean sealed context. */
|
||||
def test(specText: String, testTags: Tag*)(runTest: TestContext => IO[Assertion])(implicit pos: source.Position): Unit =
|
||||
it(specText, testTags: _*)(args =>
|
||||
contextBuilder(args).use(runTest).unsafeToFuture())
|
||||
|
||||
/** Tries every second f until it succeeds or until 20 attempts have been made. */
|
||||
def eventually[A](f: IO[A]): IO[A] =
|
||||
within(20.seconds, 20)(f)
|
||||
|
||||
/** Retries the argument f until it succeeds or time/split attempts have been made,
|
||||
* there exists a delay of time for each retry.
|
||||
*/
|
||||
def within[A](time: FiniteDuration, split: Int)(f: IO[A]): IO[A] = {
|
||||
def inner(count: Int, times: FiniteDuration): IO[A] = {
|
||||
if (count < 1) f else f.attempt.flatMap {
|
||||
case Left(_) => IO.sleep(times) *> inner(count - 1, times)
|
||||
case Right(a) => IO(a)
|
||||
}
|
||||
}
|
||||
|
||||
inner(split, time / split)
|
||||
}
|
||||
|
||||
override type FixtureParam = TestArguments
|
||||
|
||||
override def withFixture(test: OneArgAsyncTest): FutureOutcome =
|
||||
test.apply(argumentsBuilder(test.configMap))
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
package com.ing.baker.baas.akka
|
||||
|
||||
import akka.actor.ActorSystem
|
||||
import akka.http.scaladsl.Http
|
||||
import akka.http.scaladsl.marshalling.{Marshaller, ToEntityMarshaller}
|
||||
import akka.http.scaladsl.model.{MediaTypes, StatusCodes}
|
||||
import akka.http.scaladsl.server.Directives._
|
||||
import akka.http.scaladsl.server.Route
|
||||
import akka.http.scaladsl.unmarshalling.{FromEntityUnmarshaller, Unmarshaller}
|
||||
import akka.stream.Materializer
|
||||
import com.ing.baker.baas.protocol.InteractionSchedulingProto._
|
||||
import com.ing.baker.baas.protocol.ProtocolInteractionExecution
|
||||
import com.ing.baker.runtime.scaladsl.InteractionInstance
|
||||
import com.ing.baker.runtime.serialization.{Encryption, ProtoMap}
|
||||
|
||||
import scala.concurrent.Future
|
||||
|
||||
object RemoteInteractionHttp {
|
||||
|
||||
def run(interaction: InteractionInstance)(host: String, port: Int)(implicit system: ActorSystem, mat: Materializer, encryption: Encryption): Future[Http.ServerBinding] = {
|
||||
import system.dispatcher
|
||||
val server = new RemoteInteractionHttp(interaction)(system, mat, encryption)
|
||||
Http().bindAndHandle(server.route, host, port)
|
||||
}
|
||||
}
|
||||
|
||||
class RemoteInteractionHttp(interaction: InteractionInstance)(implicit system: ActorSystem, mat: Materializer, encryption: Encryption) {
|
||||
|
||||
import system.dispatcher
|
||||
|
||||
private type ProtoMessage[A] = scalapb.GeneratedMessage with scalapb.Message[A]
|
||||
|
||||
private implicit def protoMarshaller[A, P <: ProtoMessage[P]](implicit mapping: ProtoMap[A, P]): ToEntityMarshaller[A] =
|
||||
Marshaller.ByteArrayMarshaller.wrap(MediaTypes.`application/octet-stream`)(mapping.toByteArray)
|
||||
|
||||
private implicit def protoUnmarshaller[A, P <: ProtoMessage[P]](implicit mapping: ProtoMap[A, P]): FromEntityUnmarshaller[A] =
|
||||
Unmarshaller.byteArrayUnmarshaller.map(mapping.fromByteArray(_).get)
|
||||
|
||||
private implicit def protoEitherMarshaller[A, P0 <: ProtoMessage[P0], B, P1 <: ProtoMessage[P1]](implicit m1: ProtoMap[A, P0], m2: ProtoMap[B, P1]): ToEntityMarshaller[Either[A, B]] =
|
||||
Marshaller.ByteArrayMarshaller.wrap(MediaTypes.`application/octet-stream`) {
|
||||
case Left(a) => m1.toByteArray(a)
|
||||
case Right(b) => m2.toByteArray(b)
|
||||
}
|
||||
|
||||
private implicit def protoEitherUnmarshaller[A, P0 <: ProtoMessage[P0], B, P1 <: ProtoMessage[P1]](implicit m1: ProtoMap[A, P0], m2: ProtoMap[B, P1]): FromEntityUnmarshaller[Either[A, B]] =
|
||||
Unmarshaller.byteArrayUnmarshaller.map { byteArray =>
|
||||
m1.fromByteArray(byteArray).map(Left(_)).orElse(m2.fromByteArray(byteArray).map(Right(_))).get
|
||||
}
|
||||
|
||||
private def route: Route = concat(pathPrefix("api" / "v3")(concat(health, interface, apply)))
|
||||
|
||||
private def health: Route = pathPrefix("health")(get(complete(StatusCodes.OK)))
|
||||
|
||||
private def interface: Route = get(path("interface") {
|
||||
complete(ProtocolInteractionExecution.InstanceInterface(interaction.name, interaction.input))
|
||||
})
|
||||
|
||||
private def apply: Route = post(path("apply") {
|
||||
entity(as[ProtocolInteractionExecution.ExecuteInstance]) { request =>
|
||||
val result = for {
|
||||
optionalEventInstance <- interaction.run(request.input)
|
||||
.map(result => Left(ProtocolInteractionExecution.InstanceExecutedSuccessfully(result)))
|
||||
.recover { case e => Right(ProtocolInteractionExecution.InstanceExecutionFailed(e.getMessage)) }
|
||||
} yield optionalEventInstance
|
||||
complete(result)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.ing.baker.baas.interaction
|
||||
|
||||
import java.net.InetSocketAddress
|
||||
|
||||
import cats.effect.{ContextShift, IO, Resource, Timer}
|
||||
import com.ing.baker.baas.interaction.BakeryHttp.ProtoEntityEncoders._
|
||||
import com.ing.baker.baas.protocol.InteractionSchedulingProto._
|
||||
import com.ing.baker.baas.protocol.ProtocolInteractionExecution
|
||||
import com.ing.baker.runtime.scaladsl.InteractionInstance
|
||||
import org.http4s._
|
||||
import org.http4s.dsl.io._
|
||||
import org.http4s.implicits._
|
||||
import org.http4s.server.blaze.BlazeServerBuilder
|
||||
import org.http4s.server.{Router, Server}
|
||||
|
||||
object RemoteInteractionService {
|
||||
|
||||
def resource(interaction: InteractionInstance, address: InetSocketAddress)(implicit timer: Timer[IO], cs: ContextShift[IO]): Resource[IO, Server[IO]] =
|
||||
BlazeServerBuilder[IO]
|
||||
.bindSocketAddress(address)
|
||||
.withHttpApp(new RemoteInteractionService(interaction).build)
|
||||
.resource
|
||||
}
|
||||
|
||||
final class RemoteInteractionService(interaction: InteractionInstance)(implicit timer: Timer[IO], cs: ContextShift[IO]) {
|
||||
|
||||
def build: HttpApp[IO] =
|
||||
api.orNotFound
|
||||
|
||||
def api: HttpRoutes[IO] = Router("/api/v3" -> HttpRoutes.of[IO] {
|
||||
|
||||
case GET -> Root / "health" =>
|
||||
Ok("Ok")
|
||||
|
||||
case GET -> Root / "interface" =>
|
||||
Ok(ProtocolInteractionExecution.InstanceInterface(interaction.name, interaction.input))
|
||||
|
||||
case req@POST -> Root / "run-interaction" =>
|
||||
for {
|
||||
request <- req.as[ProtocolInteractionExecution.ExecuteInstance]
|
||||
response <- IO.fromFuture(IO(interaction.run(request.input))).attempt.flatMap {
|
||||
case Right(value) =>
|
||||
Ok(ProtocolInteractionExecution.InstanceExecutedSuccessfully(value))
|
||||
case Left(e) =>
|
||||
Ok(ProtocolInteractionExecution.InstanceExecutionFailed(e.getMessage))
|
||||
}
|
||||
} yield response
|
||||
})
|
||||
}
|
||||
@@ -1,44 +1,32 @@
|
||||
package com.ing.baker.baas.scaladsl
|
||||
|
||||
import akka.actor.ActorSystem
|
||||
import akka.http.scaladsl.Http
|
||||
import akka.stream.{ActorMaterializer, Materializer}
|
||||
import com.ing.baker.baas.akka.RemoteInteractionHttp
|
||||
import java.net.InetSocketAddress
|
||||
|
||||
import cats.effect.{ContextShift, IO, Timer}
|
||||
import com.ing.baker.baas.common
|
||||
import com.ing.baker.baas.interaction.RemoteInteractionService
|
||||
import com.ing.baker.runtime.common.LanguageDataStructures.ScalaApi
|
||||
import com.ing.baker.runtime.scaladsl.InteractionInstance
|
||||
import com.ing.baker.runtime.serialization.Encryption
|
||||
import com.typesafe.config.{Config, ConfigFactory}
|
||||
import com.typesafe.config.ConfigFactory
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.{Await, Future}
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
object RemoteInteraction extends common.RemoteInteraction[Future] with ScalaApi {
|
||||
|
||||
override type InteractionInstanceType = InteractionInstance
|
||||
|
||||
private[baas] def runWith(implementation: InteractionInstance, port: Int, timeout: FiniteDuration)(implicit system: ActorSystem, mat: Materializer, encryption: Encryption): Future[Http.ServerBinding] = {
|
||||
// Temp
|
||||
println(Console.YELLOW + s"Starting remote interaction [${implementation.name}]" + Console.RESET)
|
||||
// Temp
|
||||
println(Console.YELLOW + s"Starting the http server on port $port" + Console.RESET)
|
||||
import system.dispatcher
|
||||
RemoteInteractionHttp.run(implementation)( "0.0.0.0", port).map { hook =>
|
||||
println(Console.GREEN + "Http server started" + Console.RESET)
|
||||
println(hook.localAddress)
|
||||
sys.addShutdownHook(Await.result(hook.unbind(), timeout))
|
||||
hook
|
||||
}
|
||||
}
|
||||
|
||||
override def load(implementation: InteractionInstance): Unit = {
|
||||
val timeout: FiniteDuration = 20.seconds
|
||||
val config = ConfigFactory.load()
|
||||
val port = config.getInt("baas-component.http-api-port")
|
||||
implicit val system: ActorSystem = ActorSystem("RemoteInteractionSystem")
|
||||
implicit val materializer: Materializer = ActorMaterializer()(system)
|
||||
implicit val encryption: Encryption = Encryption.from(config)
|
||||
Await.result(runWith(implementation, port, timeout), timeout)
|
||||
|
||||
val address = InetSocketAddress.createUnresolved("0.0.0.0", port)
|
||||
|
||||
implicit val contextShift: ContextShift[IO] = IO.contextShift(ExecutionContext.Implicits.global)
|
||||
implicit val timer: Timer[IO] = IO.timer(ExecutionContext.Implicits.global)
|
||||
|
||||
RemoteInteractionService
|
||||
.resource(implementation, address)
|
||||
.use(_ => IO.never)
|
||||
.unsafeRunAsyncAndForget()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
17
baas-node-interaction/src/test/resources/logback-test.xml
Normal file
17
baas-node-interaction/src/test/resources/logback-test.xml
Normal file
@@ -0,0 +1,17 @@
|
||||
<configuration>
|
||||
|
||||
<logger name="org.mockserver.log.MockServerEventLog" level="OFF"/>
|
||||
|
||||
<root level="info">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
</root>
|
||||
|
||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<layout class="ch.qos.logback.classic.PatternLayout">
|
||||
<Pattern>
|
||||
%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n
|
||||
</Pattern>
|
||||
</layout>
|
||||
</appender>
|
||||
|
||||
</configuration>
|
||||
@@ -1,87 +0,0 @@
|
||||
package com.ing.baker.baas.akka
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
import akka.actor.ActorSystem
|
||||
import akka.stream.{ActorMaterializer, Materializer}
|
||||
import cats.data.StateT
|
||||
import cats.implicits._
|
||||
import com.ing.baker.baas.akka.RemoteInteractionSpec._
|
||||
import com.ing.baker.baas.scaladsl.RemoteInteraction
|
||||
import com.ing.baker.runtime.common.LanguageDataStructures.LanguageApi
|
||||
import com.ing.baker.runtime.scaladsl.{EventInstance, IngredientInstance, InteractionInstance}
|
||||
import com.ing.baker.runtime.serialization.Encryption
|
||||
import com.ing.baker.types.{CharArray, Int64, PrimitiveValue, Type}
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import org.jboss.netty.channel.ChannelException
|
||||
import org.scalatest.AsyncFlatSpec
|
||||
import org.scalatest.compatible.Assertion
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
class RemoteInteractionSpec extends AsyncFlatSpec {
|
||||
|
||||
"The Remote Interaction" should "execute on apply" in {
|
||||
testWith (implementation, { client =>
|
||||
val ingredient0 = IngredientInstance("input0", PrimitiveValue("A"))
|
||||
val ingredient1 = IngredientInstance("input1", PrimitiveValue(1))
|
||||
for {
|
||||
result0 <- client(Seq(ingredient0, ingredient1))
|
||||
} yield assert(result0 === Some(result("A", 1)))
|
||||
})
|
||||
}
|
||||
|
||||
it should "publish its interface" in {
|
||||
testWith(implementation, { client =>
|
||||
for {
|
||||
result <- client.interface
|
||||
(name: String, input: Seq[Type]) = result
|
||||
} yield assert(name == implementation.name && input === implementation.input)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
object RemoteInteractionSpec {
|
||||
|
||||
def result(input0: String, input1: Int) = EventInstance(
|
||||
name = "Result",
|
||||
providedIngredients = Map(
|
||||
"data" -> PrimitiveValue(input0 + input1)
|
||||
)
|
||||
)
|
||||
|
||||
val implementation = InteractionInstance(
|
||||
name = "TestInteraction",
|
||||
input = Seq(CharArray, Int64),
|
||||
run = input => Future.successful(Some(result(input.head.value.as[String], input(1).value.as[Int])))
|
||||
)
|
||||
|
||||
def testWith[F[_], Lang <: LanguageApi]
|
||||
(implementation: InteractionInstance, test: (RemoteInteractionClient) => Future[Assertion])
|
||||
(implicit ec: ExecutionContext): Future[Assertion] = {
|
||||
val testId: UUID = UUID.randomUUID()
|
||||
val systemName: String = "baas-node-interaction-test-" + testId
|
||||
implicit val system: ActorSystem = ActorSystem(systemName, ConfigFactory.parseString("""akka.loglevel = "OFF" """))
|
||||
implicit val materializer: Materializer = ActorMaterializer()
|
||||
implicit val encryption: Encryption = Encryption.NoEncryption
|
||||
for {
|
||||
(binding, port) <- withOpenPort(5000, port => RemoteInteraction.runWith(implementation, port, 20.seconds))
|
||||
client = RemoteInteractionClient(s"http://localhost:$port/")
|
||||
assertion <- test(client)
|
||||
_ <- binding.unbind()
|
||||
} yield assertion
|
||||
}
|
||||
|
||||
private def withOpenPort[T](from: Int, f: Int => Future[T])(implicit ec: ExecutionContext): Future[(T, Int)] = {
|
||||
def search(ports: Stream[Int]): Future[(Stream[Int], (T, Int))] =
|
||||
ports match {
|
||||
case #::(port, tail) => f(port).map(tail -> (_, port)).recoverWith {
|
||||
case _: java.net.BindException => search(tail)
|
||||
case _: ChannelException => search(tail)
|
||||
case other => println("REVIEW withOpenPort function implementation, uncaught exception: " + Console.RED + other + Console.RESET); Future.failed(other)
|
||||
}
|
||||
}
|
||||
StateT(search).run(Stream.from(from, 1)).map(_._2)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.ing.baker.baas.interaction
|
||||
|
||||
import java.net.InetSocketAddress
|
||||
|
||||
import cats.effect.{IO, Resource}
|
||||
import com.ing.baker.baas.testing.BakeryFunSpec
|
||||
import com.ing.baker.runtime.scaladsl.{EventInstance, IngredientInstance, InteractionInstance}
|
||||
import com.ing.baker.types.{CharArray, Int64, PrimitiveValue, Type}
|
||||
import org.scalatest.ConfigMap
|
||||
import org.scalatest.compatible.Assertion
|
||||
|
||||
import scala.concurrent.Future
|
||||
|
||||
class RemoteInteractionSpec extends BakeryFunSpec {
|
||||
|
||||
case class Context(
|
||||
withInteractionInstance: InteractionInstance => (RemoteInteractionClient => IO[Assertion]) => IO[Assertion]
|
||||
)
|
||||
|
||||
/** Represents the "sealed resources context" that each test can use. */
|
||||
type TestContext = Context
|
||||
|
||||
/** Represents external arguments to the test context builder. */
|
||||
type TestArguments = Unit
|
||||
|
||||
/** Creates a `Resource` which allocates and liberates the expensive resources each test can use.
|
||||
* For example web servers, network connection, database mocks.
|
||||
*
|
||||
* The objective of this function is to provide "sealed resources context" to each test, that means context
|
||||
* that other tests simply cannot touch.
|
||||
*
|
||||
* @param testArguments arguments built by the `argumentsBuilder` function.
|
||||
* @return the resources each test can use
|
||||
*/
|
||||
def contextBuilder(testArguments: TestArguments): Resource[IO, TestContext] = {
|
||||
val context = Context({ interaction => runTest =>
|
||||
RemoteInteractionService.resource(interaction, InetSocketAddress.createUnresolved("localhost", 0))
|
||||
.flatMap(server => RemoteInteractionClient.resource(server.baseUri, executionContext))
|
||||
.use(runTest)
|
||||
})
|
||||
Resource.pure[IO, Context](context)
|
||||
}
|
||||
|
||||
/** Refines the `ConfigMap` populated with the -Dkey=value arguments coming from the "sbt testOnly" command.
|
||||
*
|
||||
* @param config map populated with the -Dkey=value arguments.
|
||||
* @return the data structure used by the `contextBuilder` function.
|
||||
*/
|
||||
def argumentsBuilder(config: ConfigMap): TestArguments = ()
|
||||
|
||||
describe("The remote interaction") {
|
||||
|
||||
def result(input0: String, input1: Int) = EventInstance(
|
||||
name = "Result",
|
||||
providedIngredients = Map(
|
||||
"data" -> PrimitiveValue(input0 + input1)
|
||||
)
|
||||
)
|
||||
|
||||
val implementation = InteractionInstance(
|
||||
name = "TestInteraction",
|
||||
input = Seq(CharArray, Int64),
|
||||
run = input => Future.successful(Some(result(input.head.value.as[String], input(1).value.as[Int])))
|
||||
)
|
||||
|
||||
test("publishes its interface") { context =>
|
||||
context.withInteractionInstance(implementation) { client =>
|
||||
for {
|
||||
result <- client.interface
|
||||
(name: String, input: Seq[Type]) = result
|
||||
} yield assert(name == implementation.name && input === implementation.input)
|
||||
}
|
||||
}
|
||||
|
||||
test("executes the interaction") { context =>
|
||||
context.withInteractionInstance(implementation) { client =>
|
||||
val ingredient0 = IngredientInstance("input0", PrimitiveValue("A"))
|
||||
val ingredient1 = IngredientInstance("input1", PrimitiveValue(1))
|
||||
for {
|
||||
result0 <- client.runInteraction(Seq(ingredient0, ingredient1))
|
||||
} yield assert(result0 === Some(result("A", 1)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.ing.baker.baas.testing
|
||||
|
||||
import cats.effect.{ContextShift, IO, Resource, Timer}
|
||||
import cats.syntax.apply._
|
||||
import org.scalactic.source
|
||||
import org.scalatest.compatible.Assertion
|
||||
import org.scalatest.{ConfigMap, FutureOutcome, Tag, fixture}
|
||||
|
||||
import scala.concurrent.duration._
|
||||
|
||||
/** Abstracts the common test practices across the Bakery project. */
|
||||
abstract class BakeryFunSpec extends fixture.AsyncFunSpecLike {
|
||||
|
||||
implicit val contextShift: ContextShift[IO] =
|
||||
IO.contextShift(executionContext)
|
||||
|
||||
implicit val timer: Timer[IO] =
|
||||
IO.timer(executionContext)
|
||||
|
||||
/** Represents the "sealed resources context" that each test can use. */
|
||||
type TestContext
|
||||
|
||||
/** Represents external arguments to the test context builder. */
|
||||
type TestArguments
|
||||
|
||||
/** Creates a `Resource` which allocates and liberates the expensive resources each test can use.
|
||||
* For example web servers, network connection, database mocks.
|
||||
*
|
||||
* The objective of this function is to provide "sealed resources context" to each test, that means context
|
||||
* that other tests simply cannot touch.
|
||||
*
|
||||
* @param testArguments arguments built by the `argumentsBuilder` function.
|
||||
* @return the resources each test can use
|
||||
*/
|
||||
def contextBuilder(testArguments: TestArguments): Resource[IO, TestContext]
|
||||
|
||||
/** Refines the `ConfigMap` populated with the -Dkey=value arguments coming from the "sbt testOnly" command.
|
||||
*
|
||||
* @param config map populated with the -Dkey=value arguments.
|
||||
* @return the data structure used by the `contextBuilder` function.
|
||||
*/
|
||||
def argumentsBuilder(config: ConfigMap): TestArguments
|
||||
|
||||
/** Runs a single test with a clean sealed context. */
|
||||
def test(specText: String, testTags: Tag*)(runTest: TestContext => IO[Assertion])(implicit pos: source.Position): Unit =
|
||||
it(specText, testTags: _*)(args =>
|
||||
contextBuilder(args).use(runTest).unsafeToFuture())
|
||||
|
||||
/** Tries every second f until it succeeds or until 20 attempts have been made. */
|
||||
def eventually[A](f: IO[A]): IO[A] =
|
||||
within(20.seconds, 20)(f)
|
||||
|
||||
/** Retries the argument f until it succeeds or time/split attempts have been made,
|
||||
* there exists a delay of time for each retry.
|
||||
*/
|
||||
def within[A](time: FiniteDuration, split: Int)(f: IO[A]): IO[A] = {
|
||||
def inner(count: Int, times: FiniteDuration): IO[A] = {
|
||||
if (count < 1) f else f.attempt.flatMap {
|
||||
case Left(_) => IO.sleep(times) *> inner(count - 1, times)
|
||||
case Right(a) => IO(a)
|
||||
}
|
||||
}
|
||||
|
||||
inner(split, time / split)
|
||||
}
|
||||
|
||||
override type FixtureParam = TestArguments
|
||||
|
||||
override def withFixture(test: OneArgAsyncTest): FutureOutcome =
|
||||
test.apply(argumentsBuilder(test.configMap))
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
package com.ing.baker.baas.state
|
||||
|
||||
import akka.actor.ActorSystem
|
||||
import akka.stream.Materializer
|
||||
import com.ing.baker.baas.akka.{RemoteBakerEventListenerClient, RemoteEventListenerClient}
|
||||
import com.ing.baker.runtime.scaladsl.Baker
|
||||
import com.ing.baker.runtime.serialization.Encryption
|
||||
import com.typesafe.scalalogging.LazyLogging
|
||||
|
||||
import scala.concurrent.Future
|
||||
import scala.concurrent.duration._
|
||||
|
||||
// TODO make this more efficient and thread safe (making it an actor is fine)
|
||||
class EventListenersServiceDiscovery(discovery: ServiceDiscovery, baker: Baker)(implicit system: ActorSystem, mat: Materializer, encryption: Encryption) extends LazyLogging {
|
||||
|
||||
import system.dispatcher
|
||||
|
||||
type RecipeName = String
|
||||
|
||||
private var recipeListenersCache: Map[RecipeName, List[RemoteEventListenerClient]] = Map.empty
|
||||
private var bakerListenersCache: List[RemoteBakerEventListenerClient] = List.empty
|
||||
|
||||
private def loadListeners: Future[Map[RecipeName, List[RemoteEventListenerClient]]] = {
|
||||
discovery
|
||||
.getEventListenersAddresses
|
||||
.map(_
|
||||
.map { case (recipe, address) => (recipe, RemoteEventListenerClient(address)) }
|
||||
.toList
|
||||
.foldLeft(Map.empty[RecipeName, List[RemoteEventListenerClient]]) { case (acc, (recipeName, client)) =>
|
||||
acc + (recipeName -> (client :: acc.getOrElse(recipeName, List.empty[RemoteEventListenerClient])))
|
||||
})
|
||||
}
|
||||
|
||||
private def loadBakerListeners: Future[List[RemoteBakerEventListenerClient]] = {
|
||||
discovery
|
||||
.getBakerEventListenersAddresses
|
||||
.map(_
|
||||
.map(RemoteBakerEventListenerClient(_))
|
||||
.toList)
|
||||
}
|
||||
|
||||
private def updateCache: Runnable = () => {
|
||||
loadListeners.foreach { listeners =>
|
||||
recipeListenersCache = listeners
|
||||
}
|
||||
loadBakerListeners.foreach { listeners =>
|
||||
bakerListenersCache = listeners
|
||||
}
|
||||
}
|
||||
|
||||
system.scheduler.schedule(0.seconds, 10.seconds, updateCache)
|
||||
|
||||
def initializeEventListeners: Future[Unit] = {
|
||||
baker.registerEventListener((metadata, event) => {
|
||||
recipeListenersCache.get(metadata.recipeName).foreach(_.foreach(_.apply(metadata, event)))
|
||||
recipeListenersCache.get("All-Recipes").foreach(_.foreach(_.apply(metadata, event)))
|
||||
})
|
||||
baker.registerBakerEventListener(event => {
|
||||
bakerListenersCache.foreach(_.apply(event))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
package com.ing.baker.baas.state
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
import akka.actor.ActorSystem
|
||||
import akka.stream.Materializer
|
||||
import cats.implicits._
|
||||
import com.ing.baker.baas.akka.RemoteInteractionClient
|
||||
import com.ing.baker.il.petrinet.InteractionTransition
|
||||
import com.ing.baker.runtime.akka.internal.{FatalInteractionException, InteractionManager}
|
||||
import com.ing.baker.runtime.scaladsl.{EventInstance, IngredientInstance, InteractionInstance}
|
||||
import com.ing.baker.runtime.serialization.Encryption
|
||||
import com.typesafe.scalalogging.LazyLogging
|
||||
|
||||
import scala.compat.java8.FunctionConverters._
|
||||
import scala.concurrent.Future
|
||||
import scala.concurrent.duration._
|
||||
|
||||
class InteractionsServiceDiscovery(discovery: ServiceDiscovery)(implicit system: ActorSystem, mat: Materializer, encryption: Encryption)
|
||||
extends InteractionManager with LazyLogging {
|
||||
|
||||
private var interactionImplementations: Seq[InteractionInstance] = Seq.empty
|
||||
|
||||
private val implementationCache: ConcurrentHashMap[InteractionTransition, InteractionInstance] =
|
||||
new ConcurrentHashMap[InteractionTransition, InteractionInstance]
|
||||
|
||||
//TODO changes this to an Actor
|
||||
|
||||
import system.dispatcher
|
||||
|
||||
def loadInteractions: Future[List[InteractionInstance]] = {
|
||||
discovery
|
||||
.getInteractionAddresses
|
||||
.flatMap(_.map(RemoteInteractionClient(_))
|
||||
.toList
|
||||
.traverse(client => client.interface.map {
|
||||
case (name, types) => Some(InteractionInstance(
|
||||
name = name,
|
||||
input = types,
|
||||
run = client.apply
|
||||
))
|
||||
}.recover({ case e: Exception =>
|
||||
None
|
||||
}))
|
||||
).map(_.flatten)
|
||||
}
|
||||
|
||||
def updateInteractions: Runnable = () => {
|
||||
logger.info("Updating the InteractionManager")
|
||||
loadInteractions.map(_.foreach(instance => {
|
||||
if (!interactionImplementations.contains(instance))
|
||||
interactionImplementations :+= instance
|
||||
}))
|
||||
}
|
||||
|
||||
system.scheduler.schedule(0 seconds, 10 seconds, updateInteractions)
|
||||
|
||||
private def isCompatibleImplementation(interaction: InteractionTransition, implementation: InteractionInstance): Boolean = {
|
||||
val interactionNameMatches =
|
||||
interaction.originalInteractionName == implementation.name
|
||||
val inputSizeMatches =
|
||||
implementation.input.size == interaction.requiredIngredients.size
|
||||
val inputNamesAndTypesMatches =
|
||||
interaction
|
||||
.requiredIngredients
|
||||
.forall { descriptor =>
|
||||
implementation.input.exists(_.isAssignableFrom(descriptor.`type`))
|
||||
}
|
||||
interactionNameMatches && inputSizeMatches && inputNamesAndTypesMatches
|
||||
}
|
||||
|
||||
private def findInteractionImplementation(interaction: InteractionTransition): InteractionInstance =
|
||||
interactionImplementations.find(implementation => isCompatibleImplementation(interaction, implementation)).orNull
|
||||
|
||||
override def executeImplementation(interaction: InteractionTransition, input: Seq[IngredientInstance]): Future[Option[EventInstance]] = {
|
||||
this.getImplementation(interaction) match {
|
||||
case Some(implementation) => implementation.run(input)
|
||||
case None => Future.failed(new FatalInteractionException("No implementation available for interaction"))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an implementation is available for the given interaction.
|
||||
* It checks:
|
||||
* 1. Name
|
||||
* 2. Input variable sizes
|
||||
* 3. Input variable types
|
||||
*
|
||||
* @param interaction The interaction to check
|
||||
* @return An option containing the implementation if available
|
||||
*/
|
||||
private def getImplementation(interaction: InteractionTransition): Option[InteractionInstance] =
|
||||
Option(implementationCache.computeIfAbsent(interaction, (findInteractionImplementation _).asJava))
|
||||
|
||||
def hasImplementation(interaction: InteractionTransition): Future[Boolean] =
|
||||
Future.successful(getImplementation(interaction).isDefined)
|
||||
|
||||
override def addImplementation(interaction: InteractionInstance): Future[Unit] =
|
||||
Future.failed(new IllegalStateException("Adding implmentation instances is not supported on a BaaS cluster"))
|
||||
|
||||
}
|
||||
@@ -1,46 +1,54 @@
|
||||
package com.ing.baker.baas.state
|
||||
|
||||
import java.net.InetSocketAddress
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
import akka.actor.ActorSystem
|
||||
import akka.cluster.Cluster
|
||||
import akka.stream.{ActorMaterializer, Materializer}
|
||||
import cats.effect.{ExitCode, IO, IOApp, Resource}
|
||||
import cats.implicits._
|
||||
import com.ing.baker.runtime.akka.{AkkaBaker, AkkaBakerConfig}
|
||||
import com.ing.baker.runtime.serialization.Encryption
|
||||
import com.ing.baker.runtime.scaladsl.Baker
|
||||
import com.typesafe.config.ConfigFactory
|
||||
|
||||
import scala.concurrent.Await
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.ExecutionContext
|
||||
|
||||
object Main extends App {
|
||||
object Main extends IOApp {
|
||||
|
||||
// Config
|
||||
val config = ConfigFactory.load()
|
||||
val httpServerPort = config.getInt("baas-component.http-api-port")
|
||||
val namespace = config.getString("baas-component.kubernetes-namespace")
|
||||
override def run(args: List[String]): IO[ExitCode] = {
|
||||
// Config
|
||||
val config = ConfigFactory.load()
|
||||
val httpServerPort = config.getInt("baas-component.http-api-port")
|
||||
val namespace = config.getString("baas-component.kubernetes-namespace")
|
||||
|
||||
// Core dependencies
|
||||
implicit val system: ActorSystem = ActorSystem("BaaSStateNodeSystem")
|
||||
implicit val materializer: Materializer = ActorMaterializer()
|
||||
implicit val encryption: Encryption = Encryption.from(config)
|
||||
// Core dependencies
|
||||
implicit val system: ActorSystem =
|
||||
ActorSystem("BaaSStateNodeSystem")
|
||||
implicit val materializer: Materializer =
|
||||
ActorMaterializer()
|
||||
implicit val blockingEC: ExecutionContext =
|
||||
ExecutionContext.fromExecutor(Executors.newCachedThreadPool())
|
||||
val connectionPool: ExecutionContext =
|
||||
ExecutionContext.fromExecutor(Executors.newCachedThreadPool())
|
||||
val hostname: InetSocketAddress =
|
||||
InetSocketAddress.createUnresolved("0.0.0.0", httpServerPort)
|
||||
|
||||
// Dependencies
|
||||
val kubernetes = new ServiceDiscoveryKubernetes(namespace)
|
||||
val interactionManager = new InteractionsServiceDiscovery(kubernetes)
|
||||
val stateNodeBaker = AkkaBaker.withConfig(AkkaBakerConfig(
|
||||
interactionManager = interactionManager,
|
||||
bakerActorProvider = AkkaBakerConfig.bakerProviderFrom(config),
|
||||
readJournal = AkkaBakerConfig.persistenceQueryFrom(config, system),
|
||||
timeouts = AkkaBakerConfig.Timeouts.from(config),
|
||||
bakerValidationSettings = AkkaBakerConfig.BakerValidationSettings.from(config)
|
||||
)(system))
|
||||
val eventListeners = new EventListenersServiceDiscovery(kubernetes, stateNodeBaker)
|
||||
val mainResource = for {
|
||||
serviceDiscovery <- ServiceDiscovery.resource(connectionPool, namespace)
|
||||
baker: Baker = AkkaBaker.withConfig(AkkaBakerConfig(
|
||||
interactionManager = serviceDiscovery.buildInteractionManager,
|
||||
bakerActorProvider = AkkaBakerConfig.bakerProviderFrom(config),
|
||||
readJournal = AkkaBakerConfig.persistenceQueryFrom(config, system),
|
||||
timeouts = AkkaBakerConfig.Timeouts.from(config),
|
||||
bakerValidationSettings = AkkaBakerConfig.BakerValidationSettings.from(config)
|
||||
)(system))
|
||||
_ <- Resource.liftF(serviceDiscovery.plugBakerEventListeners(baker))
|
||||
_ <- StateNodeService.resource(baker, hostname)
|
||||
} yield ()
|
||||
|
||||
import system.dispatcher
|
||||
|
||||
// Server init
|
||||
Cluster(system).registerOnMemberUp {
|
||||
StateNodeHttp.run(eventListeners, stateNodeBaker, "0.0.0.0", httpServerPort).foreach { hook =>
|
||||
sys.addShutdownHook(Await.result(hook.unbind(), 20.seconds))
|
||||
}
|
||||
IO(Cluster(system).registerOnMemberUp {
|
||||
mainResource.use(_ => IO.never).unsafeRunAsyncAndForget()
|
||||
}).as(ExitCode.Success)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,13 +1,168 @@
|
||||
package com.ing.baker.baas.state
|
||||
|
||||
import scala.concurrent.Future
|
||||
import cats.effect.concurrent.Ref
|
||||
import cats.effect.{ContextShift, IO, Resource, Timer}
|
||||
import cats.implicits._
|
||||
import com.ing.baker.baas.bakerlistener.RemoteBakerEventListenerClient
|
||||
import com.ing.baker.baas.interaction.RemoteInteractionClient
|
||||
import com.ing.baker.baas.recipelistener.RemoteEventListenerClient
|
||||
import com.ing.baker.baas.state.ServiceDiscovery.{BakerListener, RecipeListener, RecipeName}
|
||||
import com.ing.baker.il.petrinet.InteractionTransition
|
||||
import com.ing.baker.runtime.akka.internal.InteractionManager
|
||||
import com.ing.baker.runtime.scaladsl.{Baker, InteractionInstance}
|
||||
import com.typesafe.scalalogging.LazyLogging
|
||||
import fs2.Stream
|
||||
import io.kubernetes.client.openapi.ApiClient
|
||||
import io.kubernetes.client.openapi.apis.CoreV1Api
|
||||
import io.kubernetes.client.openapi.models.V1Service
|
||||
import io.kubernetes.client.util.ClientBuilder
|
||||
import org.http4s.Uri
|
||||
|
||||
trait ServiceDiscovery {
|
||||
import scala.collection.JavaConverters._
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
def getInteractionAddresses: Future[Seq[String]]
|
||||
object ServiceDiscovery extends LazyLogging {
|
||||
|
||||
def getEventListenersAddresses: Future[Seq[(String, String)]]
|
||||
private[state] type RecipeName = String
|
||||
|
||||
def getBakerEventListenersAddresses: Future[Seq[String]]
|
||||
private[state] type RecipeListener = Resource[IO, RemoteEventListenerClient]
|
||||
|
||||
private[state] type BakerListener = Resource[IO, RemoteBakerEventListenerClient]
|
||||
|
||||
/** Creates resource of a ServiceDiscovery module, when acquired a stream of kubernetes services starts and feeds the
|
||||
* ServiceDiscovery module to give corresponding InteractionInstances and clients to the event listeners.
|
||||
* When the resource is released the polling to the Kubernetes API stops.
|
||||
*
|
||||
* Current hard coded polling periods: 2 seconds
|
||||
*
|
||||
* @param connectionPool to be used for client connections
|
||||
* @param namespace Kubernetes namespace to be queried
|
||||
* @param client Kubernetes java client to be used
|
||||
* @param contextShift to be used by the streams
|
||||
* @param timer to be used by the streams
|
||||
* @param blockingEC pool used for the blocking operation of the java library
|
||||
* @return
|
||||
*/
|
||||
def resource(connectionPool: ExecutionContext, namespace: String, client: ApiClient = ClientBuilder.cluster.build)(implicit contextShift: ContextShift[IO], timer: Timer[IO], blockingEC: ExecutionContext): Resource[IO, ServiceDiscovery] = {
|
||||
val api = new CoreV1Api(client)
|
||||
|
||||
def fetchServices(namespace: String, api: CoreV1Api)(implicit contextShift: ContextShift[IO], blockingEC: ExecutionContext): IO[List[V1Service]] =
|
||||
contextShift.evalOn(blockingEC)(IO {
|
||||
api.listNamespacedService(namespace, null, null, null, null, null, null, null, null, null)
|
||||
.getItems
|
||||
.asScala
|
||||
.toList
|
||||
}).attempt.flatMap {
|
||||
case Right(services) =>
|
||||
IO.pure(services)
|
||||
case Left(e) =>
|
||||
IO(logger.warn("Failed to communicate with the Kubernetes service: " + e.getMessage))
|
||||
IO.pure(List.empty)
|
||||
}
|
||||
|
||||
def getInteractionAddresses(currentServices: List[V1Service]): List[Uri] =
|
||||
currentServices
|
||||
.filter(_.getMetadata.getLabels.getOrDefault("baas-component", "Wrong")
|
||||
.equals("remote-interaction"))
|
||||
.map(service => "http://" + service.getMetadata.getName + ":" + service.getSpec.getPorts.asScala.head.getPort)
|
||||
.map(Uri.unsafeFromString)
|
||||
|
||||
def getEventListenersAddresses(currentServices: List[V1Service]): List[(String, Uri)] =
|
||||
currentServices
|
||||
.filter(_.getMetadata.getLabels.getOrDefault("baas-component", "Wrong")
|
||||
.equals("remote-event-listener"))
|
||||
.map { service =>
|
||||
val recipe = service.getMetadata.getLabels.getOrDefault("baker-recipe", "All-Recipes")
|
||||
val address = Uri.unsafeFromString("http://" + service.getMetadata.getName + ":" + + service.getSpec.getPorts.asScala.head.getPort)
|
||||
recipe -> address
|
||||
}
|
||||
|
||||
def getBakerEventListenersAddresses(currentServices: List[V1Service]): List[Uri] =
|
||||
currentServices
|
||||
.filter(_.getMetadata.getLabels.getOrDefault("baas-component", "Wrong")
|
||||
.equals("remote-baker-event-listener"))
|
||||
.map(service => "http://" + service.getMetadata.getName + ":" + service.getSpec.getPorts.asScala.head.getPort)
|
||||
.map(Uri.unsafeFromString)
|
||||
|
||||
def buildInteractions(currentServices: List[V1Service]): IO[List[InteractionInstance]] =
|
||||
getInteractionAddresses(currentServices)
|
||||
.map(RemoteInteractionClient.resource(_, connectionPool))
|
||||
.parTraverse[IO, Option[InteractionInstance]](buildInteractionInstance)
|
||||
.map(_.flatten)
|
||||
|
||||
def buildInteractionInstance(resource: Resource[IO, RemoteInteractionClient]): IO[Option[InteractionInstance]] =
|
||||
resource.use { client =>
|
||||
for {
|
||||
interface <- client.interface.attempt
|
||||
interactionsOpt = interface match {
|
||||
case Right((name, types)) => Some(InteractionInstance(
|
||||
name = name,
|
||||
input = types,
|
||||
run = input => resource.use(_.runInteraction(input)).unsafeToFuture()
|
||||
))
|
||||
case Left(_) => None
|
||||
}
|
||||
} yield interactionsOpt
|
||||
}
|
||||
|
||||
def buildRecipeListeners(currentServices: List[V1Service]): Map[RecipeName, List[RecipeListener]] =
|
||||
getEventListenersAddresses(currentServices)
|
||||
.map { case (recipe, address) => (recipe, RemoteEventListenerClient.resource(address, connectionPool)) }
|
||||
.foldLeft(Map.empty[RecipeName, List[RecipeListener]]) { case (acc, (recipeName, client)) =>
|
||||
acc + (recipeName -> (client :: acc.getOrElse(recipeName, List.empty)))
|
||||
}
|
||||
|
||||
def buildBakerListeners(currentServices: List[V1Service]): List[BakerListener] =
|
||||
getBakerEventListenersAddresses(currentServices)
|
||||
.map(RemoteBakerEventListenerClient.resource(_, connectionPool))
|
||||
|
||||
val stream = for {
|
||||
cacheInteractions <- Stream.eval(Ref.of[IO, List[InteractionInstance]](List.empty))
|
||||
cacheRecipeListeners <- Stream.eval(Ref.of[IO, Map[RecipeName, List[RecipeListener]]](Map.empty))
|
||||
cacheBakerListeners <- Stream.eval(Ref.of[IO, List[BakerListener]](List.empty))
|
||||
service = new ServiceDiscovery(cacheInteractions, cacheRecipeListeners, cacheBakerListeners)
|
||||
updateServices = fetchServices(namespace, api)
|
||||
.flatMap { currentServices =>
|
||||
List(
|
||||
buildInteractions(currentServices).flatMap(cacheInteractions.set),
|
||||
cacheRecipeListeners.set(buildRecipeListeners(currentServices)),
|
||||
cacheBakerListeners.set(buildBakerListeners(currentServices))
|
||||
).parSequence
|
||||
}
|
||||
updater = Stream.fixedRate(5.seconds).evalMap(_ => updateServices)
|
||||
_ <- Stream.eval(updateServices).concurrently(updater)
|
||||
} yield service
|
||||
|
||||
stream.compile.resource.lastOrError
|
||||
}
|
||||
}
|
||||
|
||||
final class ServiceDiscovery private(
|
||||
cacheInteractions: Ref[IO, List[InteractionInstance]],
|
||||
cacheRecipeListeners: Ref[IO, Map[RecipeName, List[RecipeListener]]],
|
||||
cacheBakerListeners: Ref[IO, List[BakerListener]]
|
||||
) {
|
||||
|
||||
def plugBakerEventListeners(baker: Baker): IO[Unit] = IO {
|
||||
baker.registerBakerEventListener { event =>
|
||||
cacheBakerListeners.get.map { listeners =>
|
||||
listeners.foreach(_.use(_.fireEvent(event)).unsafeRunAsyncAndForget())
|
||||
}.unsafeRunAsyncAndForget()
|
||||
}
|
||||
baker.registerEventListener { (metadata, event) =>
|
||||
cacheRecipeListeners.get.map { listeners =>
|
||||
listeners.get(metadata.recipeName).foreach(_.foreach(_.use(_.fireEvent(metadata, event)).unsafeRunAsyncAndForget()))
|
||||
listeners.get("All-Recipes").foreach(_.foreach(_.use(_.fireEvent(metadata, event)).unsafeRunAsyncAndForget()))
|
||||
}.unsafeRunAsyncAndForget()
|
||||
}
|
||||
}
|
||||
|
||||
def buildInteractionManager: InteractionManager =
|
||||
new InteractionManager {
|
||||
override def addImplementation(interaction: InteractionInstance): Future[Unit] =
|
||||
Future.failed(new IllegalStateException("Adding implmentation instances is not supported on a Bakery cluster."))
|
||||
override def getImplementation(interaction: InteractionTransition): Future[Option[InteractionInstance]] =
|
||||
cacheInteractions.get.map(_.find(isCompatibleImplementation(interaction, _))).unsafeToFuture()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
package com.ing.baker.baas.state
|
||||
|
||||
import io.kubernetes.client.openapi.ApiClient
|
||||
import io.kubernetes.client.openapi.apis.CoreV1Api
|
||||
import io.kubernetes.client.openapi.models.V1Service
|
||||
import io.kubernetes.client.util.ClientBuilder
|
||||
|
||||
import scala.collection.JavaConverters._
|
||||
import scala.collection.mutable
|
||||
import scala.concurrent.Future
|
||||
|
||||
class ServiceDiscoveryKubernetes(namespace: String, client: ApiClient = ClientBuilder.cluster.build) extends ServiceDiscovery {
|
||||
|
||||
private val api = new CoreV1Api(client)
|
||||
|
||||
def getInteractionAddresses: Future[Seq[String]] = {
|
||||
Future.successful(getInteractionServices().map(service => "http://" + service.getMetadata.getName + ":" + service.getSpec.getPorts.asScala.head.getPort))
|
||||
}
|
||||
|
||||
def getEventListenersAddresses: Future[Seq[(String, String)]] = {
|
||||
Future.successful(getEventListenerServices().map { service =>
|
||||
(service.getMetadata.getLabels.getOrDefault("baker-recipe", "All-Recipes"), "http://" + service.getMetadata.getName + ":" + + service.getSpec.getPorts.asScala.head.getPort)
|
||||
})
|
||||
}
|
||||
|
||||
override def getBakerEventListenersAddresses: Future[Seq[String]] = {
|
||||
Future.successful(getBakerEventListenerServices().map { service =>
|
||||
"http://" + service.getMetadata.getName + ":8080"
|
||||
})
|
||||
}
|
||||
|
||||
private def getInteractionServices(): mutable.Seq[V1Service] = {
|
||||
api.listNamespacedService(namespace, null, null, null, null, null, null, null, null, null)
|
||||
.getItems
|
||||
.asScala
|
||||
.filter(_.getMetadata.getLabels.getOrDefault("baas-component", "Wrong")
|
||||
.equals("remote-interaction"))
|
||||
}
|
||||
|
||||
private def getEventListenerServices(): mutable.Seq[V1Service] = {
|
||||
api.listNamespacedService(namespace, null, null, null, null, null, null, null, null, null)
|
||||
.getItems
|
||||
.asScala
|
||||
.filter(_.getMetadata.getLabels.getOrDefault("baas-component", "Wrong")
|
||||
.equals("remote-event-listener"))
|
||||
}
|
||||
|
||||
private def getBakerEventListenerServices(): mutable.Seq[V1Service] = {
|
||||
api.listNamespacedService(namespace, null, null, null, null, null, null, null, null, null)
|
||||
.getItems
|
||||
.asScala
|
||||
.filter(_.getMetadata.getLabels.getOrDefault("baas-component", "Wrong")
|
||||
.equals("remote-baker-event-listener"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
package com.ing.baker.baas.state
|
||||
|
||||
import akka.actor.ActorSystem
|
||||
import akka.http.scaladsl.Http
|
||||
import akka.http.scaladsl.model.StatusCodes
|
||||
import akka.http.scaladsl.server.Directives._
|
||||
import akka.http.scaladsl.server.Route
|
||||
import akka.stream.Materializer
|
||||
import com.ing.baker.baas.protocol.BaaSProto._
|
||||
import com.ing.baker.baas.protocol.BaaSProtocol
|
||||
import com.ing.baker.baas.protocol.MarshallingUtils._
|
||||
import com.ing.baker.runtime.akka.actor.serialization.AkkaSerializerProvider
|
||||
import com.ing.baker.runtime.scaladsl.Baker
|
||||
import com.ing.baker.runtime.serialization.Encryption
|
||||
|
||||
import scala.concurrent.Future
|
||||
|
||||
object StateNodeHttp {
|
||||
|
||||
def run(listeners: EventListenersServiceDiscovery, baker: Baker, host: String, port: Int)(implicit system: ActorSystem, mat: Materializer, encryption: Encryption): Future[Http.ServerBinding] = {
|
||||
import system.dispatcher
|
||||
for {
|
||||
_ <- listeners.initializeEventListeners
|
||||
binding <- Http().bindAndHandle(new StateNodeHttp(listeners, baker).route, host, port)
|
||||
} yield binding
|
||||
}
|
||||
}
|
||||
|
||||
class StateNodeHttp(listeners: EventListenersServiceDiscovery, baker: Baker)(implicit system: ActorSystem, mat: Materializer, encryption: Encryption) {
|
||||
|
||||
import system.dispatcher
|
||||
|
||||
implicit private val serializersProvider: AkkaSerializerProvider =
|
||||
AkkaSerializerProvider(system, encryption)
|
||||
|
||||
def route: Route = concat(pathPrefix("api" / "v3")(concat(health, addRecipe, getRecipe, getAllRecipes, bake,
|
||||
fireEventAndResolveWhenReceived, fireEventAndResolveWhenCompleted, fireEventAndResolveOnEvent, fireEvent,
|
||||
getAllRecipeInstancesMetadata, getRecipeInstanceState, getVisualState, retryInteraction, resolveInteraction,
|
||||
stopRetryingInteraction
|
||||
)))
|
||||
|
||||
private def health: Route = pathPrefix("health")(get(complete(StatusCodes.OK)))
|
||||
|
||||
private def addRecipe: Route = post(path("addRecipe") {
|
||||
entity(as[BaaSProtocol.AddRecipeRequest]) { request =>
|
||||
val result = baker.addRecipe(request.compiledRecipe).map(BaaSProtocol.AddRecipeResponse)
|
||||
completeWithBakerFailures(result)
|
||||
}
|
||||
})
|
||||
|
||||
private def getRecipe: Route = post(path("getRecipe") {
|
||||
entity(as[BaaSProtocol.GetRecipeRequest]) { request =>
|
||||
completeWithBakerFailures(baker.getRecipe(request.recipeId).map(BaaSProtocol.GetRecipeResponse))
|
||||
}
|
||||
})
|
||||
|
||||
private def getAllRecipes: Route = post(path("getAllRecipes") {
|
||||
completeWithBakerFailures(baker.getAllRecipes.map(BaaSProtocol.GetAllRecipesResponse))
|
||||
})
|
||||
|
||||
private def bake: Route = post(path("bake") {
|
||||
entity(as[BaaSProtocol.BakeRequest]) { request =>
|
||||
completeWithBakerFailures(baker.bake(request.recipeId, request.recipeInstanceId))
|
||||
}
|
||||
})
|
||||
|
||||
private def fireEventAndResolveWhenReceived: Route = post(path("fireEventAndResolveWhenReceived") {
|
||||
entity(as[BaaSProtocol.FireEventAndResolveWhenReceivedRequest]) { request =>
|
||||
completeWithBakerFailures(baker.fireEventAndResolveWhenReceived(request.recipeInstanceId, request.event, request.correlationId)
|
||||
.map(BaaSProtocol.FireEventAndResolveWhenReceivedResponse))
|
||||
}
|
||||
})
|
||||
|
||||
private def fireEventAndResolveWhenCompleted: Route = post(path("fireEventAndResolveWhenCompleted") {
|
||||
entity(as[BaaSProtocol.FireEventAndResolveWhenCompletedRequest]) { request =>
|
||||
completeWithBakerFailures(baker.fireEventAndResolveWhenCompleted(request.recipeInstanceId, request.event, request.correlationId)
|
||||
.map(BaaSProtocol.FireEventAndResolveWhenCompletedResponse))
|
||||
}
|
||||
})
|
||||
|
||||
private def fireEventAndResolveOnEvent: Route = post(path("fireEventAndResolveOnEvent") {
|
||||
entity(as[BaaSProtocol.FireEventAndResolveOnEventRequest]) { request =>
|
||||
completeWithBakerFailures(baker.fireEventAndResolveOnEvent(request.recipeInstanceId, request.event, request.onEvent, request.correlationId)
|
||||
.map(BaaSProtocol.FireEventAndResolveOnEventResponse))
|
||||
}
|
||||
})
|
||||
|
||||
private def fireEvent: Route = post(path("fireEvent") {
|
||||
entity(as[BaaSProtocol.FireEventRequest]) { request =>
|
||||
complete(baker.fireEvent(request.recipeInstanceId, request.event, request.correlationId).resolveWhenReceived
|
||||
.map(_ => "TODO")) // TODO figure out what to do here with the 2 different futures
|
||||
}
|
||||
})
|
||||
|
||||
private def getAllRecipeInstancesMetadata: Route = post(path("getAllRecipeInstancesMetadata") {
|
||||
completeWithBakerFailures(baker.getAllRecipeInstancesMetadata
|
||||
.map(BaaSProtocol.GetAllRecipeInstancesMetadataResponse))
|
||||
})
|
||||
|
||||
private def getRecipeInstanceState: Route = post(path("getRecipeInstanceState") {
|
||||
entity(as[BaaSProtocol.GetRecipeInstanceStateRequest]) { request =>
|
||||
completeWithBakerFailures(baker.getRecipeInstanceState(request.recipeInstanceId)
|
||||
.map(BaaSProtocol.GetRecipeInstanceStateResponse))
|
||||
}
|
||||
})
|
||||
|
||||
private def getVisualState: Route = post(path("getVisualState") {
|
||||
entity(as[BaaSProtocol.GetVisualStateRequest]) { request =>
|
||||
completeWithBakerFailures(baker.getVisualState(request.recipeInstanceId)
|
||||
.map(BaaSProtocol.GetVisualStateResponse))
|
||||
}
|
||||
})
|
||||
|
||||
private def retryInteraction: Route = post(path("retryInteraction") {
|
||||
entity(as[BaaSProtocol.RetryInteractionRequest]) { request =>
|
||||
completeWithBakerFailures(baker.retryInteraction(request.recipeInstanceId, request.interactionName))
|
||||
}
|
||||
})
|
||||
|
||||
private def resolveInteraction: Route = post(path("resolveInteraction") {
|
||||
entity(as[BaaSProtocol.ResolveInteractionRequest]) { request =>
|
||||
completeWithBakerFailures(baker.resolveInteraction(request.recipeInstanceId, request.interactionName, request.event))
|
||||
}
|
||||
})
|
||||
|
||||
private def stopRetryingInteraction: Route = post(path("stopRetryingInteraction") {
|
||||
entity(as[BaaSProtocol.StopRetryingInteractionRequest]) { request =>
|
||||
completeWithBakerFailures(baker.stopRetryingInteraction(request.recipeInstanceId, request.interactionName))
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package com.ing.baker.baas.state
|
||||
|
||||
import java.net.InetSocketAddress
|
||||
|
||||
import cats.effect.{ContextShift, IO, Resource, Timer}
|
||||
import com.ing.baker.baas.protocol.BaaSProto._
|
||||
import com.ing.baker.baas.protocol.BaaSProtocol
|
||||
import com.ing.baker.baas.protocol.BakeryHttp.ProtoEntityEncoders._
|
||||
import com.ing.baker.runtime.common.BakerException
|
||||
import com.ing.baker.runtime.scaladsl.Baker
|
||||
import org.http4s._
|
||||
import org.http4s.dsl.io._
|
||||
import org.http4s.implicits._
|
||||
import org.http4s.server.blaze.BlazeServerBuilder
|
||||
import org.http4s.server.{Router, Server}
|
||||
|
||||
import scala.concurrent.Future
|
||||
|
||||
object StateNodeService {
|
||||
|
||||
def resource(baker: Baker, hostname: InetSocketAddress)(implicit cs: ContextShift[IO], timer: Timer[IO]): Resource[IO, Server[IO]] = {
|
||||
for {
|
||||
binding <- BlazeServerBuilder[IO]
|
||||
.bindSocketAddress(hostname)
|
||||
.withHttpApp(new StateNodeService(baker).build)
|
||||
.resource
|
||||
} yield binding
|
||||
}
|
||||
}
|
||||
|
||||
final class StateNodeService private(baker: Baker)(implicit cs: ContextShift[IO]) {
|
||||
|
||||
def build: HttpApp[IO] =
|
||||
api.orNotFound
|
||||
|
||||
def completeWithBakerFailures[A, R](result: IO[Future[A]])(f: A => R)(implicit decoder: EntityEncoder[IO, R]): IO[Response[IO]] =
|
||||
IO.fromFuture(result).attempt.flatMap {
|
||||
case Left(e: BakerException) => Ok(BaaSProtocol.BaaSRemoteFailure(e))
|
||||
case Left(e) => IO.raiseError(new IllegalStateException("No other exception but BakerExceptions should be thrown here.", e))
|
||||
case Right(a) => Ok(f(a))
|
||||
}
|
||||
|
||||
def api: HttpRoutes[IO] = Router("/api/v3" -> HttpRoutes.of[IO] {
|
||||
|
||||
case GET -> Root / "health" =>
|
||||
Ok("Ok")
|
||||
|
||||
case req@POST -> Root / "addRecipe" =>
|
||||
req.as[BaaSProtocol.AddRecipeRequest]
|
||||
.map(r => IO(baker.addRecipe(r.compiledRecipe)))
|
||||
.flatMap(completeWithBakerFailures(_)(BaaSProtocol.AddRecipeResponse))
|
||||
|
||||
case req@POST -> Root / "getRecipe" =>
|
||||
req.as[BaaSProtocol.GetRecipeRequest]
|
||||
.map(r => IO(baker.getRecipe(r.recipeId)))
|
||||
.flatMap(completeWithBakerFailures(_)(BaaSProtocol.GetRecipeResponse))
|
||||
|
||||
case GET -> Root / "getAllRecipes" =>
|
||||
completeWithBakerFailures(IO(baker.getAllRecipes))(BaaSProtocol.GetAllRecipesResponse)
|
||||
|
||||
case req@POST -> Root / "bake" =>
|
||||
req.as[BaaSProtocol.BakeRequest]
|
||||
.map(r => IO(baker.bake(r.recipeId, r.recipeInstanceId)))
|
||||
.flatMap(completeWithBakerFailures(_)(_ => ""))
|
||||
|
||||
case req@POST -> Root / "fireEventAndResolveWhenReceived" =>
|
||||
req.as[BaaSProtocol.FireEventAndResolveWhenReceivedRequest]
|
||||
.map(request => IO(baker.fireEventAndResolveWhenReceived(request.recipeInstanceId, request.event, request.correlationId)))
|
||||
.flatMap(completeWithBakerFailures(_)(BaaSProtocol.FireEventAndResolveWhenReceivedResponse))
|
||||
|
||||
case req@POST -> Root / "fireEventAndResolveWhenCompleted" =>
|
||||
req.as[BaaSProtocol.FireEventAndResolveWhenCompletedRequest]
|
||||
.map(request => IO(baker.fireEventAndResolveWhenCompleted(request.recipeInstanceId, request.event, request.correlationId)))
|
||||
.flatMap(completeWithBakerFailures(_)(BaaSProtocol.FireEventAndResolveWhenCompletedResponse))
|
||||
|
||||
case req@POST -> Root / "fireEventAndResolveOnEvent" =>
|
||||
req.as[BaaSProtocol.FireEventAndResolveOnEventRequest]
|
||||
.map(request => IO(baker.fireEventAndResolveOnEvent(request.recipeInstanceId, request.event, request.onEvent, request.correlationId)))
|
||||
.flatMap(completeWithBakerFailures(_)(BaaSProtocol.FireEventAndResolveOnEventResponse))
|
||||
|
||||
case POST -> Root / "fireEvent" =>
|
||||
Ok("") // TODO figure out what to do here with the 2 different futures
|
||||
|
||||
case GET -> Root / "getAllRecipeInstancesMetadata" =>
|
||||
completeWithBakerFailures(IO(baker.getAllRecipeInstancesMetadata))(BaaSProtocol.GetAllRecipeInstancesMetadataResponse)
|
||||
|
||||
case req@POST -> Root / "getRecipeInstanceState" =>
|
||||
req.as[BaaSProtocol.GetRecipeInstanceStateRequest]
|
||||
.map(request => IO(baker.getRecipeInstanceState(request.recipeInstanceId)))
|
||||
.flatMap(completeWithBakerFailures(_)(BaaSProtocol.GetRecipeInstanceStateResponse))
|
||||
|
||||
case req@POST -> Root / "getVisualState" =>
|
||||
req.as[BaaSProtocol.GetVisualStateRequest]
|
||||
.map(request => IO(baker.getVisualState(request.recipeInstanceId)))
|
||||
.flatMap(completeWithBakerFailures(_)(BaaSProtocol.GetVisualStateResponse))
|
||||
|
||||
case req@POST -> Root / "retryInteraction" =>
|
||||
req.as[BaaSProtocol.RetryInteractionRequest]
|
||||
.map(request => IO(baker.retryInteraction(request.recipeInstanceId, request.interactionName)))
|
||||
.flatMap(completeWithBakerFailures(_)(_ => ""))
|
||||
|
||||
case req@POST -> Root / "resolveInteraction" =>
|
||||
req.as[BaaSProtocol.ResolveInteractionRequest]
|
||||
.map(request => IO(baker.resolveInteraction(request.recipeInstanceId, request.interactionName, request.event)))
|
||||
.flatMap(completeWithBakerFailures(_)(_ => ""))
|
||||
|
||||
case req@POST -> Root / "stopRetryingInteraction" =>
|
||||
req.as[BaaSProtocol.StopRetryingInteractionRequest]
|
||||
.map(request => IO(baker.stopRetryingInteraction(request.recipeInstanceId, request.interactionName)))
|
||||
.flatMap(completeWithBakerFailures(_)(_ => ""))
|
||||
})
|
||||
}
|
||||
@@ -9,7 +9,7 @@
|
||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<layout class="ch.qos.logback.classic.PatternLayout">
|
||||
<Pattern>
|
||||
%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - aoushdoauhd;ouawhd %msg%n
|
||||
%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n
|
||||
</Pattern>
|
||||
</layout>
|
||||
</appender>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.ing.baker.baas.mocks
|
||||
|
||||
import cats.effect.IO
|
||||
import com.ing.baker.baas.kubeapi
|
||||
import com.ing.baker.baas.recipe.ItemReservationRecipe
|
||||
import org.mockserver.integration.ClientAndServer
|
||||
@@ -7,14 +8,12 @@ import org.mockserver.model.HttpRequest.request
|
||||
import org.mockserver.model.HttpResponse.response
|
||||
import org.mockserver.model.MediaType
|
||||
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
class KubeApiServer(mock: ClientAndServer) {
|
||||
|
||||
class KubeApiServer(mock: ClientAndServer)(implicit ec: ExecutionContext) {
|
||||
|
||||
def registersRemoteComponents: Future[Unit] =
|
||||
def registersRemoteComponents: IO[Unit] =
|
||||
willRespondWith(interactionAndEventListenersServices)
|
||||
|
||||
private def willRespondWith(services: kubeapi.Services): Future[Unit] = Future {
|
||||
private def willRespondWith(services: kubeapi.Services): IO[Unit] = IO {
|
||||
mock.when(
|
||||
request()
|
||||
.withMethod("GET")
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
package com.ing.baker.baas.mocks
|
||||
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
import cats.effect.IO
|
||||
|
||||
class RemoteComponents(kubeApiServer: KubeApiServer, remoteInteraction: RemoteInteraction, remoteEventListener: RemoteEventListener)(implicit ec: ExecutionContext) {
|
||||
class RemoteComponents(kubeApiServer: KubeApiServer, remoteInteraction: RemoteInteraction, remoteEventListener: RemoteEventListener) {
|
||||
|
||||
def registerToTheCluster: Future[Unit] =
|
||||
def registerToTheCluster: IO[Unit] =
|
||||
for {
|
||||
_ <- kubeApiServer.registersRemoteComponents
|
||||
_ <- remoteInteraction.publishesItsInterface
|
||||
_ <- remoteEventListener.listensToEvents
|
||||
_ <- kubeApiServer.registersRemoteComponents
|
||||
} yield ()
|
||||
}
|
||||
|
||||
@@ -1,37 +1,32 @@
|
||||
package com.ing.baker.baas.mocks
|
||||
|
||||
import akka.actor.ActorSystem
|
||||
import com.ing.baker.runtime.serialization.Encryption
|
||||
import cats.effect.IO
|
||||
import org.mockserver.integration.ClientAndServer
|
||||
import org.mockserver.model.HttpRequest
|
||||
import org.mockserver.model.HttpRequest.request
|
||||
import org.mockserver.model.HttpResponse.response
|
||||
import org.mockserver.verify.VerificationTimes
|
||||
|
||||
import scala.concurrent.Future
|
||||
|
||||
class RemoteEventListener(mock: ClientAndServer)(implicit system: ActorSystem, encryption: Encryption) {
|
||||
|
||||
import system.dispatcher
|
||||
class RemoteEventListener(mock: ClientAndServer) {
|
||||
|
||||
private val eventApply: HttpRequest =
|
||||
request()
|
||||
.withMethod("POST")
|
||||
.withPath("/api/v3/apply")
|
||||
.withPath("/api/v3/recipe-event")
|
||||
.withHeader("X-Bakery-Intent", s"Remote-Event-Listener:localhost")
|
||||
|
||||
def listensToEvents: Future[Unit] = Future {
|
||||
def listensToEvents: IO[Unit] = IO {
|
||||
mock.when(eventApply).respond(
|
||||
response()
|
||||
.withStatusCode(200)
|
||||
)
|
||||
}
|
||||
|
||||
def verifyEventsReceived(times: Int): Future[Unit] = Future {
|
||||
def verifyEventsReceived(times: Int): IO[Unit] = IO {
|
||||
mock.verify(eventApply, VerificationTimes.exactly(times))
|
||||
}
|
||||
|
||||
def verifyNoEventsArrived: Future[Unit] = Future {
|
||||
def verifyNoEventsArrived: IO[Unit] = IO {
|
||||
mock.verify(eventApply, VerificationTimes.exactly(0))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,20 @@
|
||||
package com.ing.baker.baas.mocks
|
||||
|
||||
import akka.actor.ActorSystem
|
||||
import cats.effect.IO
|
||||
import com.ing.baker.baas.mocks.Utils._
|
||||
import com.ing.baker.baas.protocol.InteractionSchedulingProto._
|
||||
import com.ing.baker.baas.protocol.ProtocolInteractionExecution
|
||||
import com.ing.baker.recipe.scaladsl.Interaction
|
||||
import com.ing.baker.runtime.scaladsl.EventInstance
|
||||
import com.ing.baker.runtime.serialization.Encryption
|
||||
import org.mockserver.integration.ClientAndServer
|
||||
import org.mockserver.matchers.Times
|
||||
import org.mockserver.model.HttpRequest.request
|
||||
import org.mockserver.model.HttpResponse.response
|
||||
import org.mockserver.verify.VerificationTimes
|
||||
|
||||
import scala.concurrent.Future
|
||||
class RemoteInteraction(mock: ClientAndServer, interaction: Interaction) {
|
||||
|
||||
class RemoteInteraction(mock: ClientAndServer, interaction: Interaction)(implicit system: ActorSystem, encryption: Encryption) {
|
||||
|
||||
import system.dispatcher
|
||||
|
||||
def publishesItsInterface: Future[Unit] = Future {
|
||||
def publishesItsInterface: IO[Unit] = IO {
|
||||
mock.when(
|
||||
request()
|
||||
.withMethod("GET")
|
||||
@@ -32,7 +27,7 @@ class RemoteInteraction(mock: ClientAndServer, interaction: Interaction)(implici
|
||||
)
|
||||
}
|
||||
|
||||
def processesSuccessfullyAndFires(event: EventInstance): Future[Unit] = Future {
|
||||
def processesSuccessfullyAndFires(event: EventInstance): IO[Unit] = IO {
|
||||
mock.when(
|
||||
applyMatch,
|
||||
Times.exactly(1)
|
||||
@@ -43,7 +38,7 @@ class RemoteInteraction(mock: ClientAndServer, interaction: Interaction)(implici
|
||||
)
|
||||
}
|
||||
|
||||
def processesWithFailure(e: Throwable): Future[Unit] = Future {
|
||||
def processesWithFailure(e: Throwable): IO[Unit] = IO {
|
||||
mock.when(
|
||||
applyMatch,
|
||||
Times.exactly(1)
|
||||
@@ -54,18 +49,18 @@ class RemoteInteraction(mock: ClientAndServer, interaction: Interaction)(implici
|
||||
)
|
||||
}
|
||||
|
||||
def didNothing: Future[Unit] = Future {
|
||||
def didNothing: IO[Unit] = IO {
|
||||
mock.verify(applyMatch, VerificationTimes.exactly(0))
|
||||
}
|
||||
|
||||
def receivedEvent(event: EventInstance): Future[Unit] = Future {
|
||||
def receivedEvent(event: EventInstance): IO[Unit] = IO {
|
||||
mock.verify(applyMatch, VerificationTimes.exactly(1))
|
||||
}
|
||||
|
||||
private def applyMatch =
|
||||
request()
|
||||
.withMethod("POST")
|
||||
.withPath("/api/v3/apply")
|
||||
.withPath("/api/v3/run-interaction")
|
||||
.withHeader("X-Bakery-Intent", s"Remote-Interaction:localhost")
|
||||
|
||||
}
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
package com.ing.baker.baas.state
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
import akka.actor.ActorSystem
|
||||
import akka.http.scaladsl.Http.ServerBinding
|
||||
import akka.http.scaladsl.model.Uri
|
||||
import akka.stream.{ActorMaterializer, Materializer}
|
||||
import cats.data.StateT
|
||||
import cats.effect.{ContextShift, IO, Timer}
|
||||
import cats.implicits._
|
||||
import com.ing.baker.baas.mocks.{KubeApiServer, RemoteComponents, RemoteEventListener, RemoteInteraction}
|
||||
import com.ing.baker.baas.recipe.Interactions
|
||||
import com.ing.baker.baas.scaladsl.BakerClient
|
||||
import com.ing.baker.runtime.akka.{AkkaBaker, AkkaBakerConfig}
|
||||
import com.ing.baker.runtime.scaladsl.Baker
|
||||
import com.ing.baker.runtime.serialization.Encryption
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import io.kubernetes.client.openapi.ApiClient
|
||||
import org.jboss.netty.channel.ChannelException
|
||||
import org.mockserver.integration.ClientAndServer.startClientAndServer
|
||||
import org.scalactic.source
|
||||
import org.scalatest.compatible.Assertion
|
||||
import org.scalatest.{AsyncFunSpecLike, Tag}
|
||||
|
||||
import scala.concurrent.duration.{FiniteDuration, _}
|
||||
import scala.concurrent.{Future, Promise}
|
||||
import scala.util.Success
|
||||
|
||||
abstract class BakeryFunSpec extends AsyncFunSpecLike {
|
||||
|
||||
implicit val contextShift: ContextShift[IO] =
|
||||
IO.contextShift(executionContext)
|
||||
|
||||
implicit val timer: Timer[IO] =
|
||||
IO.timer(executionContext)
|
||||
|
||||
def eventually[A](f: => Future[A]): Future[A] =
|
||||
within(5.seconds)(f)
|
||||
|
||||
def within[A](time: FiniteDuration)(f: => Future[A]): Future[A] = {
|
||||
def inner(count: Int, times: FiniteDuration, fio: IO[A]): IO[A] = {
|
||||
if (count < 1) fio else fio.attempt.flatMap {
|
||||
case Left(_) => IO.sleep(times) *> inner(count - 1, times, fio)
|
||||
case Right(a) => IO(a)
|
||||
}
|
||||
}
|
||||
val split = 5
|
||||
inner(split, time / split, IO.fromFuture(IO(f))).unsafeToFuture()
|
||||
}
|
||||
|
||||
case class TestContext(
|
||||
bakeryBoots: () => Future[Baker],
|
||||
remoteComponents: RemoteComponents,
|
||||
remoteInteraction: RemoteInteraction,
|
||||
remoteEventListener: RemoteEventListener,
|
||||
kubeApiServer: KubeApiServer
|
||||
)
|
||||
|
||||
// Core dependencies
|
||||
def test(specText: String, testTags: Tag*)(runTest: TestContext => Future[Assertion])(implicit pos: source.Position): Unit = {
|
||||
|
||||
val testId: UUID = UUID.randomUUID()
|
||||
val systemName: String = "baas-node-interaction-test-" + testId
|
||||
implicit val system: ActorSystem = ActorSystem(systemName, ConfigFactory.parseString("""akka.loglevel = "OFF" """))
|
||||
implicit val materializer: Materializer = ActorMaterializer()
|
||||
implicit val encryption: Encryption = Encryption.NoEncryption
|
||||
|
||||
it(specText, testTags: _*) {
|
||||
|
||||
for {
|
||||
// Build mocks
|
||||
(mock, mocksPort) <- withOpenPort(5000, port => Future(startClientAndServer(port)))
|
||||
remoteInteraction = new RemoteInteraction(mock, Interactions.ReserveItemsInteraction)
|
||||
remoteEventListener = new RemoteEventListener(mock)
|
||||
kubeApiServer = new KubeApiServer(mock)
|
||||
remoteComponents = new RemoteComponents(kubeApiServer, remoteInteraction, remoteEventListener)
|
||||
|
||||
// Build the bakery ecosystem
|
||||
serverBindingPromise: Promise[ServerBinding] = Promise()
|
||||
startBakery = () => {
|
||||
val kubernetesApi = new ApiClient().setBasePath(s"http://localhost:$mocksPort")
|
||||
val kubernetes = new ServiceDiscoveryKubernetes("default", kubernetesApi)
|
||||
val interactionManager = new InteractionsServiceDiscovery(kubernetes)
|
||||
val stateNodeBaker = AkkaBaker.withConfig(
|
||||
AkkaBakerConfig.localDefault(system).copy(
|
||||
interactionManager = interactionManager,
|
||||
bakerValidationSettings = AkkaBakerConfig.BakerValidationSettings(
|
||||
allowAddingRecipeWithoutRequiringInstances = true)))
|
||||
val eventListeners = new EventListenersServiceDiscovery(kubernetes, stateNodeBaker)
|
||||
withOpenPort(5010, port => StateNodeHttp.run(eventListeners, stateNodeBaker, "0.0.0.0", port)).map {
|
||||
case (serverBinding, serverPort) =>
|
||||
serverBindingPromise.complete(Success(serverBinding))
|
||||
BakerClient(Uri(s"http://localhost:$serverPort"))
|
||||
}
|
||||
}
|
||||
|
||||
// Run the test
|
||||
assertionOrError <- runTest(TestContext(
|
||||
startBakery,
|
||||
remoteComponents,
|
||||
remoteInteraction,
|
||||
remoteEventListener,
|
||||
kubeApiServer
|
||||
)).transform(Success(_))
|
||||
|
||||
// Clean
|
||||
serverBinding <- serverBindingPromise.future
|
||||
_ <- serverBinding.unbind()
|
||||
_ <- system.terminate()
|
||||
_ <- system.whenTerminated
|
||||
_ = mock.stop()
|
||||
assertion <- Future.fromTry(assertionOrError)
|
||||
} yield assertion
|
||||
}
|
||||
}
|
||||
|
||||
private def withOpenPort[T](from: Int, f: Int => Future[T]): Future[(T, Int)] = {
|
||||
def search(ports: Stream[Int]): Future[(Stream[Int], (T, Int))] =
|
||||
ports match {
|
||||
case #::(port, tail) => f(port).map(tail -> (_, port)).recoverWith {
|
||||
case _: java.net.BindException => search(tail)
|
||||
case _: ChannelException => search(tail)
|
||||
case other =>
|
||||
println("REVIEW withOpenPort function implementation, uncaught exception: " +
|
||||
Console.RED + other + Console.RESET); Future.failed(other)
|
||||
}
|
||||
}
|
||||
StateT(search).run(Stream.from(from, 1)).map(_._2)
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,27 @@
|
||||
package com.ing.baker.baas.state
|
||||
|
||||
import java.net.InetSocketAddress
|
||||
import java.util.UUID
|
||||
|
||||
import akka.actor.ActorSystem
|
||||
import cats.effect.{IO, Resource}
|
||||
import cats.implicits._
|
||||
import com.ing.baker.baas.mocks.{KubeApiServer, RemoteComponents, RemoteEventListener, RemoteInteraction}
|
||||
import com.ing.baker.baas.recipe.Events.{ItemsReserved, OrderPlaced}
|
||||
import com.ing.baker.baas.recipe.Ingredients.{Item, OrderId, ReservedItems}
|
||||
import com.ing.baker.baas.recipe.ItemReservationRecipe
|
||||
import com.ing.baker.baas.recipe.{Interactions, ItemReservationRecipe}
|
||||
import com.ing.baker.baas.scaladsl.BakerClient
|
||||
import com.ing.baker.baas.testing.BakeryFunSpec
|
||||
import com.ing.baker.il.CompiledRecipe
|
||||
import com.ing.baker.runtime.akka.{AkkaBaker, AkkaBakerConfig}
|
||||
import com.ing.baker.runtime.common.{BakerException, SensoryEventStatus}
|
||||
import com.ing.baker.runtime.scaladsl.EventInstance
|
||||
import org.scalatest.Matchers
|
||||
import com.ing.baker.runtime.scaladsl.{Baker, EventInstance}
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import io.kubernetes.client.openapi.ApiClient
|
||||
import org.mockserver.integration.ClientAndServer
|
||||
import org.scalatest.{ConfigMap, Matchers}
|
||||
|
||||
import scala.concurrent.Future
|
||||
|
||||
class StateNodeSpec extends BakeryFunSpec with Matchers {
|
||||
|
||||
@@ -30,19 +43,20 @@ class StateNodeSpec extends BakeryFunSpec with Matchers {
|
||||
Array.fill(1)(Byte.MaxValue)
|
||||
)))
|
||||
|
||||
def io[A](f: => Future[A]): IO[A] =
|
||||
IO.fromFuture(IO(f))
|
||||
|
||||
describe("Bakery State Node") {
|
||||
|
||||
test("Recipe management") { context =>
|
||||
for {
|
||||
_ <- context.remoteComponents.registerToTheCluster
|
||||
client <- context.bakeryBoots()
|
||||
recipeId <- client.addRecipe(recipe)
|
||||
recipeInformation <- client.getRecipe(recipeId)
|
||||
noSuchRecipeError <- client
|
||||
recipeId <- io(context.client.addRecipe(recipe))
|
||||
recipeInformation <- io(context.client.getRecipe(recipeId))
|
||||
noSuchRecipeError <- io(context.client
|
||||
.getRecipe("non-existent")
|
||||
.map(_ => None)
|
||||
.recover { case e: BakerException => Some(e) }
|
||||
allRecipes <- client.getAllRecipes
|
||||
.recover { case e: BakerException => Some(e) })
|
||||
allRecipes <- io(context.client.getAllRecipes)
|
||||
} yield {
|
||||
recipeInformation.compiledRecipe shouldBe recipe
|
||||
noSuchRecipeError shouldBe Some(BakerException.NoSuchRecipeException("non-existent"))
|
||||
@@ -53,12 +67,10 @@ class StateNodeSpec extends BakeryFunSpec with Matchers {
|
||||
test("Baker.bake") { context =>
|
||||
val recipeInstanceId: String = UUID.randomUUID().toString
|
||||
for {
|
||||
_ <- context.remoteComponents.registerToTheCluster
|
||||
client <- context.bakeryBoots()
|
||||
_ <- context.remoteInteraction.processesSuccessfullyAndFires(ItemsReservedEvent)
|
||||
recipeId <- client.addRecipe(recipe)
|
||||
_ <- client.bake(recipeId, recipeInstanceId)
|
||||
state <- client.getRecipeInstanceState(recipeInstanceId)
|
||||
recipeId <- io(context.client.addRecipe(recipe))
|
||||
_ <- io(context.client.bake(recipeId, recipeInstanceId))
|
||||
state <- io(context.client.getRecipeInstanceState(recipeInstanceId))
|
||||
_ <- context.remoteEventListener.verifyNoEventsArrived
|
||||
} yield {
|
||||
state.recipeInstanceId shouldBe recipeInstanceId
|
||||
@@ -68,16 +80,14 @@ class StateNodeSpec extends BakeryFunSpec with Matchers {
|
||||
test("Baker.bake (fail with ProcessAlreadyExistsException)") { context =>
|
||||
val recipeInstanceId: String = UUID.randomUUID().toString
|
||||
for {
|
||||
_ <- context.remoteComponents.registerToTheCluster
|
||||
client <- context.bakeryBoots()
|
||||
_ <- context.remoteInteraction.processesSuccessfullyAndFires(ItemsReservedEvent)
|
||||
recipeId <- client.addRecipe(recipe)
|
||||
_ <- client.bake(recipeId, recipeInstanceId)
|
||||
e <- client
|
||||
recipeId <- io(context.client.addRecipe(recipe))
|
||||
_ <- io(context.client.bake(recipeId, recipeInstanceId))
|
||||
e <- io(context.client
|
||||
.bake(recipeId, recipeInstanceId)
|
||||
.map(_ => None)
|
||||
.recover { case e: BakerException => Some(e) }
|
||||
state <- client.getRecipeInstanceState(recipeInstanceId)
|
||||
.recover { case e: BakerException => Some(e) })
|
||||
state <- io(context.client.getRecipeInstanceState(recipeInstanceId))
|
||||
} yield {
|
||||
e shouldBe Some(BakerException.ProcessAlreadyExistsException(recipeInstanceId))
|
||||
state.recipeInstanceId shouldBe recipeInstanceId
|
||||
@@ -87,49 +97,41 @@ class StateNodeSpec extends BakeryFunSpec with Matchers {
|
||||
test("Baker.bake (fail with NoSuchRecipeException)") { context =>
|
||||
val recipeInstanceId: String = UUID.randomUUID().toString
|
||||
for {
|
||||
_ <- context.remoteComponents.registerToTheCluster
|
||||
client <- context.bakeryBoots()
|
||||
e <- client
|
||||
e <- io(context.client
|
||||
.bake("non-existent", recipeInstanceId)
|
||||
.map(_ => None)
|
||||
.recover { case e: BakerException => Some(e) }
|
||||
.recover { case e: BakerException => Some(e) })
|
||||
} yield e shouldBe Some(BakerException.NoSuchRecipeException("non-existent"))
|
||||
}
|
||||
|
||||
test("Baker.getRecipeInstanceState (fails with NoSuchProcessException)") { context =>
|
||||
for {
|
||||
_ <- context.remoteComponents.registerToTheCluster
|
||||
client <- context.bakeryBoots()
|
||||
e <- client
|
||||
e <- io(context.client
|
||||
.getRecipeInstanceState("non-existent")
|
||||
.map(_ => None)
|
||||
.recover { case e: BakerException => Some(e) }
|
||||
.recover { case e: BakerException => Some(e) })
|
||||
} yield e shouldBe Some(BakerException.NoSuchProcessException("non-existent"))
|
||||
}
|
||||
|
||||
test("Baker.fireEventAndResolveWhenReceived") { context =>
|
||||
val recipeInstanceId: String = UUID.randomUUID().toString
|
||||
for {
|
||||
_ <- context.remoteComponents.registerToTheCluster
|
||||
client <- context.bakeryBoots()
|
||||
recipeId <- client.addRecipe(recipe)
|
||||
_ <- client.bake(recipeId, recipeInstanceId)
|
||||
recipeId <- io(context.client.addRecipe(recipe))
|
||||
_ <- io(context.client.bake(recipeId, recipeInstanceId))
|
||||
_ <- context.remoteInteraction.processesSuccessfullyAndFires(ItemsReservedEvent)
|
||||
status <- client.fireEventAndResolveWhenReceived(recipeInstanceId, OrderPlacedEvent)
|
||||
status <- io(context.client.fireEventAndResolveWhenReceived(recipeInstanceId, OrderPlacedEvent))
|
||||
} yield status shouldBe SensoryEventStatus.Received
|
||||
}
|
||||
|
||||
test("Baker.fireEventAndResolveWhenCompleted") { context =>
|
||||
val recipeInstanceId: String = UUID.randomUUID().toString
|
||||
for {
|
||||
_ <- context.remoteComponents.registerToTheCluster
|
||||
client <- context.bakeryBoots()
|
||||
_ <- context.remoteInteraction.processesSuccessfullyAndFires(ItemsReservedEvent)
|
||||
recipeId <- client.addRecipe(recipe)
|
||||
_ <- client.bake(recipeId, recipeInstanceId)
|
||||
result <- client.fireEventAndResolveWhenCompleted(recipeInstanceId, OrderPlacedEvent)
|
||||
serverState <- client.getRecipeInstanceState(recipeInstanceId)
|
||||
_ <- context.remoteEventListener.verifyEventsReceived(2)
|
||||
recipeId <- io(context.client.addRecipe(recipe))
|
||||
_ <- io(context.client.bake(recipeId, recipeInstanceId))
|
||||
result <- io(context.client.fireEventAndResolveWhenCompleted(recipeInstanceId, OrderPlacedEvent))
|
||||
serverState <- io(context.client.getRecipeInstanceState(recipeInstanceId))
|
||||
_ <- eventually(eventually(context.remoteEventListener.verifyEventsReceived(2)))
|
||||
} yield {
|
||||
result.eventNames shouldBe Seq("OrderPlaced", "ItemsReserved")
|
||||
serverState.events.map(_.name) shouldBe Seq("OrderPlaced", "ItemsReserved")
|
||||
@@ -140,15 +142,13 @@ class StateNodeSpec extends BakeryFunSpec with Matchers {
|
||||
val recipeInstanceId: String = UUID.randomUUID().toString
|
||||
val event = EventInstance("non-existent", Map.empty)
|
||||
for {
|
||||
_ <- context.remoteComponents.registerToTheCluster
|
||||
client <- context.bakeryBoots()
|
||||
recipeId <- client.addRecipe(recipe)
|
||||
_ <- client.bake(recipeId, recipeInstanceId)
|
||||
result <- client
|
||||
recipeId <- io(context.client.addRecipe(recipe))
|
||||
_ <- io(context.client.bake(recipeId, recipeInstanceId))
|
||||
result <- io(context.client
|
||||
.fireEventAndResolveWhenCompleted(recipeInstanceId, event)
|
||||
.map(_ => None)
|
||||
.recover { case e: BakerException => Some(e) }
|
||||
serverState <- client.getRecipeInstanceState(recipeInstanceId)
|
||||
.recover { case e: BakerException => Some(e) })
|
||||
serverState <- io(context.client.getRecipeInstanceState(recipeInstanceId))
|
||||
_ <- context.remoteInteraction.didNothing
|
||||
} yield {
|
||||
result shouldBe Some(BakerException.IllegalEventException("No event with name 'non-existent' found in recipe 'ItemReservation'"))
|
||||
@@ -159,16 +159,14 @@ class StateNodeSpec extends BakeryFunSpec with Matchers {
|
||||
test("Baker.fireEventAndResolveOnEvent") { context =>
|
||||
val recipeInstanceId: String = UUID.randomUUID().toString
|
||||
for {
|
||||
_ <- context.remoteComponents.registerToTheCluster
|
||||
client <- context.bakeryBoots()
|
||||
_ <- context.remoteInteraction.processesSuccessfullyAndFires(ItemsReservedEvent)
|
||||
recipeId <- client.addRecipe(recipe)
|
||||
_ <- client.bake(recipeId, recipeInstanceId)
|
||||
result <- client.fireEventAndResolveOnEvent(recipeInstanceId, OrderPlacedEvent, "OrderPlaced")
|
||||
recipeId <- io(context.client.addRecipe(recipe))
|
||||
_ <- io(context.client.bake(recipeId, recipeInstanceId))
|
||||
result <- io(context.client.fireEventAndResolveOnEvent(recipeInstanceId, OrderPlacedEvent, "OrderPlaced"))
|
||||
_ <- eventually {
|
||||
for {
|
||||
serverState <- client.getRecipeInstanceState(recipeInstanceId)
|
||||
_ <- context.remoteEventListener.verifyEventsReceived(2)
|
||||
serverState <- io(context.client.getRecipeInstanceState(recipeInstanceId))
|
||||
_ <- eventually(context.remoteEventListener.verifyEventsReceived(2))
|
||||
} yield serverState.events.map(_.name) shouldBe Seq("OrderPlaced", "ItemsReserved")
|
||||
}
|
||||
} yield result.eventNames shouldBe Seq("OrderPlaced")
|
||||
@@ -177,42 +175,36 @@ class StateNodeSpec extends BakeryFunSpec with Matchers {
|
||||
test("Baker.getAllRecipeInstancesMetadata") { context =>
|
||||
val recipeInstanceId: String = UUID.randomUUID().toString
|
||||
for {
|
||||
_ <- context.remoteComponents.registerToTheCluster
|
||||
client <- context.bakeryBoots()
|
||||
_ <- context.remoteInteraction.processesSuccessfullyAndFires(ItemsReservedEvent)
|
||||
recipeId <- client.addRecipe(recipe)
|
||||
_ <- client.bake(recipeId, recipeInstanceId)
|
||||
clientMetadata <- client.getAllRecipeInstancesMetadata
|
||||
serverMetadata <- client.getAllRecipeInstancesMetadata
|
||||
recipeId <- io(context.client.addRecipe(recipe))
|
||||
_ <- io(context.client.bake(recipeId, recipeInstanceId))
|
||||
clientMetadata <- io(context.client.getAllRecipeInstancesMetadata)
|
||||
serverMetadata <- io(context.client.getAllRecipeInstancesMetadata)
|
||||
} yield clientMetadata shouldBe serverMetadata
|
||||
}
|
||||
|
||||
test("Baker.getVisualState") { context =>
|
||||
val recipeInstanceId: String = UUID.randomUUID().toString
|
||||
for {
|
||||
_ <- context.remoteComponents.registerToTheCluster
|
||||
client <- context.bakeryBoots()
|
||||
_ <- context.remoteInteraction.processesSuccessfullyAndFires(ItemsReservedEvent)
|
||||
recipeId <- client.addRecipe(recipe)
|
||||
_ <- client.bake(recipeId, recipeInstanceId)
|
||||
_ <- client.getVisualState(recipeInstanceId)
|
||||
recipeId <- io(context.client.addRecipe(recipe))
|
||||
_ <- io(context.client.bake(recipeId, recipeInstanceId))
|
||||
_ <- io(context.client.getVisualState(recipeInstanceId))
|
||||
} yield succeed
|
||||
}
|
||||
|
||||
test("Baker.retryInteraction") { context =>
|
||||
val recipeInstanceId: String = UUID.randomUUID().toString
|
||||
for {
|
||||
_ <- context.remoteComponents.registerToTheCluster
|
||||
client <- context.bakeryBoots()
|
||||
recipeId <- client.addRecipe(recipeWithBlockingStrategy)
|
||||
_ <- client.bake(recipeId, recipeInstanceId)
|
||||
recipeId <- io(context.client.addRecipe(recipeWithBlockingStrategy))
|
||||
_ <- io(context.client.bake(recipeId, recipeInstanceId))
|
||||
_ <- context.remoteInteraction.processesWithFailure(new RuntimeException("functional failure"))
|
||||
_ <- client.fireEventAndResolveWhenCompleted(recipeInstanceId, OrderPlacedEvent)
|
||||
state1 <- client.getRecipeInstanceState(recipeInstanceId).map(_.events.map(_.name))
|
||||
_ <- io(context.client.fireEventAndResolveWhenCompleted(recipeInstanceId, OrderPlacedEvent))
|
||||
state1 <- io(context.client.getRecipeInstanceState(recipeInstanceId).map(_.events.map(_.name)))
|
||||
_ <- context.remoteInteraction.processesSuccessfullyAndFires(ItemsReservedEvent)
|
||||
_ <- client.retryInteraction(recipeInstanceId, "ReserveItems")
|
||||
state2 <- client.getRecipeInstanceState(recipeInstanceId).map(_.events.map(_.name))
|
||||
_ <- context.remoteEventListener.verifyEventsReceived(2)
|
||||
_ <- io(context.client.retryInteraction(recipeInstanceId, "ReserveItems"))
|
||||
state2 <- io(context.client.getRecipeInstanceState(recipeInstanceId).map(_.events.map(_.name)))
|
||||
_ <- eventually(context.remoteEventListener.verifyEventsReceived(2))
|
||||
} yield {
|
||||
state1 should contain("OrderPlaced")
|
||||
state1 should not contain("ItemsReserved")
|
||||
@@ -227,19 +219,17 @@ class StateNodeSpec extends BakeryFunSpec with Matchers {
|
||||
ItemsReserved(reservedItems = ReservedItems(items = List(Item("resolution-item")), data = Array.empty))
|
||||
)
|
||||
for {
|
||||
_ <- context.remoteComponents.registerToTheCluster
|
||||
client <- context.bakeryBoots()
|
||||
recipeId <- client.addRecipe(recipeWithBlockingStrategy)
|
||||
_ <- client.bake(recipeId, recipeInstanceId)
|
||||
recipeId <- io(context.client.addRecipe(recipeWithBlockingStrategy))
|
||||
_ <- io(context.client.bake(recipeId, recipeInstanceId))
|
||||
_ <- context.remoteInteraction.processesWithFailure(new RuntimeException("functional failure"))
|
||||
_ <- client.fireEventAndResolveWhenCompleted(recipeInstanceId, OrderPlacedEvent)
|
||||
state1 <- client.getRecipeInstanceState(recipeInstanceId).map(_.events.map(_.name))
|
||||
_ <- client.resolveInteraction(recipeInstanceId, "ReserveItems", resolutionEvent)
|
||||
state2data <- client.getRecipeInstanceState(recipeInstanceId)
|
||||
_ <- io(context.client.fireEventAndResolveWhenCompleted(recipeInstanceId, OrderPlacedEvent))
|
||||
state1 <- io(context.client.getRecipeInstanceState(recipeInstanceId).map(_.events.map(_.name)))
|
||||
_ <- io(context.client.resolveInteraction(recipeInstanceId, "ReserveItems", resolutionEvent))
|
||||
state2data <- io(context.client.getRecipeInstanceState(recipeInstanceId))
|
||||
state2 = state2data.events.map(_.name)
|
||||
eventState = state2data.ingredients.get("reservedItems").map(_.as[ReservedItems].items.head.itemId)
|
||||
// TODO Currently the event listener receives the OrderPlaced... shouldn't also receive the resolved event?
|
||||
_ <- context.remoteEventListener.verifyEventsReceived(1)
|
||||
_ <- eventually(context.remoteEventListener.verifyEventsReceived(1))
|
||||
} yield {
|
||||
state1 should contain("OrderPlaced")
|
||||
state1 should not contain("ItemsReserved")
|
||||
@@ -252,17 +242,15 @@ class StateNodeSpec extends BakeryFunSpec with Matchers {
|
||||
test("Baker.stopRetryingInteraction") { context =>
|
||||
val recipeInstanceId: String = UUID.randomUUID().toString
|
||||
for {
|
||||
_ <- context.remoteComponents.registerToTheCluster
|
||||
client <- context.bakeryBoots()
|
||||
recipeId <- client.addRecipe(recipe)
|
||||
_ <- client.bake(recipeId, recipeInstanceId)
|
||||
recipeId <- io(context.client.addRecipe(recipe))
|
||||
_ <- io(context.client.bake(recipeId, recipeInstanceId))
|
||||
_ <- context.remoteInteraction.processesWithFailure(new RuntimeException("functional failure"))
|
||||
_ <- client.fireEventAndResolveWhenReceived(recipeInstanceId, OrderPlacedEvent)
|
||||
state1 <- client.getRecipeInstanceState(recipeInstanceId).map(_.events.map(_.name))
|
||||
_ <- client.stopRetryingInteraction(recipeInstanceId, "ReserveItems")
|
||||
state2data <- client.getRecipeInstanceState(recipeInstanceId)
|
||||
_ <- io(context.client.fireEventAndResolveWhenReceived(recipeInstanceId, OrderPlacedEvent))
|
||||
state1 <- io(context.client.getRecipeInstanceState(recipeInstanceId).map(_.events.map(_.name)))
|
||||
_ <- io(context.client.stopRetryingInteraction(recipeInstanceId, "ReserveItems"))
|
||||
state2data <- io(context.client.getRecipeInstanceState(recipeInstanceId))
|
||||
state2 = state2data.events.map(_.name)
|
||||
_ <- context.remoteEventListener.verifyEventsReceived(1)
|
||||
_ <- eventually(eventually(context.remoteEventListener.verifyEventsReceived(1)))
|
||||
} yield {
|
||||
state1 should contain("OrderPlaced")
|
||||
state1 should not contain("ItemsReserved")
|
||||
@@ -271,5 +259,77 @@ class StateNodeSpec extends BakeryFunSpec with Matchers {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case class Context(
|
||||
client: Baker,
|
||||
remoteComponents: RemoteComponents,
|
||||
remoteInteraction: RemoteInteraction,
|
||||
remoteEventListener: RemoteEventListener,
|
||||
kubeApiServer: KubeApiServer
|
||||
)
|
||||
|
||||
/** Represents the "sealed resources context" that each test can use. */
|
||||
type TestContext = Context
|
||||
|
||||
/** Represents external arguments to the test context builder. */
|
||||
type TestArguments = Unit
|
||||
|
||||
/** Creates a `Resource` which allocates and liberates the expensive resources each test can use.
|
||||
* For example web servers, network connection, database mocks.
|
||||
*
|
||||
* The objective of this function is to provide "sealed resources context" to each test, that means context
|
||||
* that other tests simply cannot touch.
|
||||
*
|
||||
* @param testArguments arguments built by the `argumentsBuilder` function.
|
||||
* @return the resources each test can use
|
||||
*/
|
||||
def contextBuilder(testArguments: TestArguments): Resource[IO, TestContext] = {
|
||||
for {
|
||||
// Mock server
|
||||
mockServer <- Resource.make(IO(ClientAndServer.startClientAndServer(0)))(s => IO(s.stop()))
|
||||
remoteInteraction = new RemoteInteraction(mockServer, Interactions.ReserveItemsInteraction)
|
||||
remoteEventListener = new RemoteEventListener(mockServer)
|
||||
kubeApiServer = new KubeApiServer(mockServer)
|
||||
remoteComponents = new RemoteComponents(kubeApiServer, remoteInteraction, remoteEventListener)
|
||||
_ <- Resource.liftF(remoteComponents.registerToTheCluster)
|
||||
|
||||
makeActorSystem = IO {
|
||||
ActorSystem(UUID.randomUUID().toString, ConfigFactory.parseString(
|
||||
"""
|
||||
|akka {
|
||||
| stdout-loglevel = "OFF"
|
||||
| loglevel = "OFF"
|
||||
|}
|
||||
|""".stripMargin)) }
|
||||
stopActorSystem = (system: ActorSystem) => IO.fromFuture(IO {
|
||||
system.terminate().flatMap(_ => system.whenTerminated) }).void
|
||||
system <- Resource.make(makeActorSystem)(stopActorSystem)
|
||||
|
||||
kubernetesApi = new ApiClient().setBasePath(s"http://localhost:${mockServer.getLocalPort}")
|
||||
serviceDiscovery <- ServiceDiscovery.resource(executionContext, "default", kubernetesApi)
|
||||
baker = AkkaBaker.withConfig(
|
||||
AkkaBakerConfig.localDefault(system).copy(
|
||||
interactionManager = serviceDiscovery.buildInteractionManager,
|
||||
bakerValidationSettings = AkkaBakerConfig.BakerValidationSettings(
|
||||
allowAddingRecipeWithoutRequiringInstances = true))(system))
|
||||
_ <- Resource.liftF(serviceDiscovery.plugBakerEventListeners(baker))
|
||||
server <- StateNodeService.resource(baker, InetSocketAddress.createUnresolved("0.0.0.0", 0))
|
||||
client <- BakerClient.resource(server.baseUri, executionContext)
|
||||
} yield Context(
|
||||
client,
|
||||
remoteComponents,
|
||||
remoteInteraction,
|
||||
remoteEventListener,
|
||||
kubeApiServer
|
||||
)
|
||||
}
|
||||
|
||||
/** Refines the `ConfigMap` populated with the -Dkey=value arguments coming from the "sbt testOnly" command.
|
||||
*
|
||||
* @param config map populated with the -Dkey=value arguments.
|
||||
* @return the data structure used by the `contextBuilder` function.
|
||||
*/
|
||||
def argumentsBuilder(config: ConfigMap): TestArguments = ()
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.ing.baker.baas.testing
|
||||
|
||||
import cats.effect.{ContextShift, IO, Resource, Timer}
|
||||
import cats.syntax.apply._
|
||||
import org.scalactic.source
|
||||
import org.scalatest.compatible.Assertion
|
||||
import org.scalatest.{ConfigMap, FutureOutcome, Tag, fixture}
|
||||
|
||||
import scala.concurrent.duration._
|
||||
|
||||
/** Abstracts the common test practices across the Bakery project. */
|
||||
abstract class BakeryFunSpec extends fixture.AsyncFunSpecLike {
|
||||
|
||||
implicit val contextShift: ContextShift[IO] =
|
||||
IO.contextShift(executionContext)
|
||||
|
||||
implicit val timer: Timer[IO] =
|
||||
IO.timer(executionContext)
|
||||
|
||||
/** Represents the "sealed resources context" that each test can use. */
|
||||
type TestContext
|
||||
|
||||
/** Represents external arguments to the test context builder. */
|
||||
type TestArguments
|
||||
|
||||
/** Creates a `Resource` which allocates and liberates the expensive resources each test can use.
|
||||
* For example web servers, network connection, database mocks.
|
||||
*
|
||||
* The objective of this function is to provide "sealed resources context" to each test, that means context
|
||||
* that other tests simply cannot touch.
|
||||
*
|
||||
* @param testArguments arguments built by the `argumentsBuilder` function.
|
||||
* @return the resources each test can use
|
||||
*/
|
||||
def contextBuilder(testArguments: TestArguments): Resource[IO, TestContext]
|
||||
|
||||
/** Refines the `ConfigMap` populated with the -Dkey=value arguments coming from the "sbt testOnly" command.
|
||||
*
|
||||
* @param config map populated with the -Dkey=value arguments.
|
||||
* @return the data structure used by the `contextBuilder` function.
|
||||
*/
|
||||
def argumentsBuilder(config: ConfigMap): TestArguments
|
||||
|
||||
/** Runs a single test with a clean sealed context. */
|
||||
def test(specText: String, testTags: Tag*)(runTest: TestContext => IO[Assertion])(implicit pos: source.Position): Unit =
|
||||
it(specText, testTags: _*)(args =>
|
||||
contextBuilder(args).use(runTest).unsafeToFuture())
|
||||
|
||||
/** Tries every second f until it succeeds or until 20 attempts have been made. */
|
||||
def eventually[A](f: IO[A]): IO[A] =
|
||||
within(20.seconds, 20)(f)
|
||||
|
||||
/** Retries the argument f until it succeeds or time/split attempts have been made,
|
||||
* there exists a delay of time for each retry.
|
||||
*/
|
||||
def within[A](time: FiniteDuration, split: Int)(f: IO[A]): IO[A] = {
|
||||
def inner(count: Int, times: FiniteDuration): IO[A] = {
|
||||
if (count < 1) f else f.attempt.flatMap {
|
||||
case Left(_) => IO.sleep(times) *> inner(count - 1, times)
|
||||
case Right(a) => IO(a)
|
||||
}
|
||||
}
|
||||
|
||||
inner(split, time / split)
|
||||
}
|
||||
|
||||
override type FixtureParam = TestArguments
|
||||
|
||||
override def withFixture(test: OneArgAsyncTest): FutureOutcome =
|
||||
test.apply(argumentsBuilder(test.configMap))
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package com.ing.baker.baas.akka
|
||||
|
||||
import akka.actor.ActorSystem
|
||||
import akka.http.scaladsl.Http
|
||||
import akka.http.scaladsl.marshalling.{Marshal, Marshaller, ToEntityMarshaller}
|
||||
import akka.http.scaladsl.model.Uri.Path
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.scaladsl.unmarshalling.{FromEntityUnmarshaller, Unmarshaller}
|
||||
import akka.stream.Materializer
|
||||
import com.ing.baker.runtime.scaladsl.BakerEvent
|
||||
import com.ing.baker.runtime.serialization.{Encryption, ProtoMap}
|
||||
|
||||
import scala.concurrent.Future
|
||||
|
||||
object RemoteBakerEventListenerClient {
|
||||
|
||||
def apply(hostname: String)(implicit system: ActorSystem, mat: Materializer, encryption: Encryption) =
|
||||
new RemoteBakerEventListenerClient(Uri(hostname))
|
||||
}
|
||||
|
||||
class RemoteBakerEventListenerClient(hostname: Uri)(implicit system: ActorSystem, mat: Materializer, encryption: Encryption) {
|
||||
|
||||
import system.dispatcher
|
||||
|
||||
private type ProtoMessage[A] = scalapb.GeneratedMessage with scalapb.Message[A]
|
||||
|
||||
private implicit def protoMarshaller[A, P <: ProtoMessage[P]](implicit mapping: ProtoMap[A, P]): ToEntityMarshaller[A] =
|
||||
Marshaller.ByteArrayMarshaller.wrap(MediaTypes.`application/octet-stream`)(mapping.toByteArray)
|
||||
|
||||
private implicit def protoUnmarshaller[A, P <: ProtoMessage[P]](implicit mapping: ProtoMap[A, P]): FromEntityUnmarshaller[A] =
|
||||
Unmarshaller.byteArrayUnmarshaller.map(mapping.fromByteArray(_).get)
|
||||
|
||||
|
||||
private val root: Path = Path./("api")./("v3")
|
||||
|
||||
private def withPath(path: Path): Uri = hostname.withPath(path)
|
||||
|
||||
def apply(event: BakerEvent): Future[Unit] =
|
||||
for {
|
||||
encoded <- Marshal(event).to[MessageEntity]
|
||||
request = HttpRequest(method = HttpMethods.POST, uri = withPath(root./("apply")), entity = encoded)
|
||||
_ <- Http().singleRequest(request)
|
||||
} yield ()
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.ing.baker.baas.bakerlistener
|
||||
|
||||
import cats.data.EitherT
|
||||
import cats.effect.IO
|
||||
import com.ing.baker.runtime.serialization.ProtoMap
|
||||
import org.http4s.EntityDecoder.collectBinary
|
||||
import org.http4s.util.CaseInsensitiveString
|
||||
import org.http4s.{EntityDecoder, EntityEncoder, Header, MalformedMessageBodyFailure, MediaType, Uri}
|
||||
|
||||
import scala.util.{Failure, Success}
|
||||
|
||||
object BakeryHttp {
|
||||
|
||||
object ProtoEntityEncoders {
|
||||
|
||||
type ProtoMessage[A] = scalapb.GeneratedMessage with scalapb.Message[A]
|
||||
|
||||
implicit def protoDecoder[A, P <: ProtoMessage[P]](implicit protoMap: ProtoMap[A, P]): EntityDecoder[IO, A] =
|
||||
EntityDecoder.decodeBy(MediaType.application.`octet-stream`)(collectBinary[IO]).map(_.toArray)
|
||||
.flatMapR { bytes =>
|
||||
protoMap.fromByteArray(bytes) match {
|
||||
case Success(a) =>
|
||||
EitherT.fromEither[IO](Right(a))
|
||||
case Failure(exception) =>
|
||||
EitherT.fromEither[IO](Left(MalformedMessageBodyFailure(exception.getMessage, Some(exception))))
|
||||
}
|
||||
}
|
||||
|
||||
implicit def protoEncoder[A, P <: ProtoMessage[P]](implicit protoMap: ProtoMap[A, P]): EntityEncoder[IO, A] =
|
||||
EntityEncoder.byteArrayEncoder[IO].contramap(protoMap.toByteArray)
|
||||
}
|
||||
|
||||
object Headers {
|
||||
|
||||
def `X-Bakery-Intent`(intent: Intent, hostname: Uri): Header = {
|
||||
Header.Raw(CaseInsensitiveString("X-Bakery-Intent"), intent.render(hostname))
|
||||
}
|
||||
|
||||
sealed abstract class Intent(rawIntent: String) {
|
||||
|
||||
def render(hostname: Uri): String = {
|
||||
val intendedHost = hostname.authority.map(_.host.value).getOrElse("unknown")
|
||||
s"$rawIntent:$intendedHost"
|
||||
}
|
||||
}
|
||||
|
||||
object Intent {
|
||||
|
||||
case object `Remote-Event-Listener` extends Intent("Remote-Event-Listener")
|
||||
|
||||
case object `Remote-Baker-Event-Listener` extends Intent("Remote-Baker-Event-Listener")
|
||||
|
||||
case object `Remote-Interaction` extends Intent("Remote-Interaction")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.ing.baker.baas.bakerlistener
|
||||
|
||||
import cats.effect.{ContextShift, IO, Resource, Timer}
|
||||
import com.ing.baker.baas.bakerlistener.BakeryHttp.Headers.{Intent, `X-Bakery-Intent`}
|
||||
import com.ing.baker.baas.bakerlistener.BakeryHttp.ProtoEntityEncoders._
|
||||
import com.ing.baker.runtime.scaladsl.BakerEvent
|
||||
import org.http4s.Method._
|
||||
import org.http4s.client.Client
|
||||
import org.http4s.client.blaze.BlazeClientBuilder
|
||||
import org.http4s.client.dsl.io._
|
||||
import org.http4s.{Status, Uri}
|
||||
|
||||
import scala.concurrent.ExecutionContext
|
||||
|
||||
object RemoteBakerEventListenerClient {
|
||||
|
||||
/** use method `use` of the Resource, the client will be acquired and shut down automatically each time
|
||||
* the resulting `IO` is run, each time using the common connection pool.
|
||||
*/
|
||||
def resource(hostname: Uri, pool: ExecutionContext)(implicit cs: ContextShift[IO], timer: Timer[IO]): Resource[IO, RemoteBakerEventListenerClient] =
|
||||
BlazeClientBuilder[IO](pool)
|
||||
.resource
|
||||
.map(new RemoteBakerEventListenerClient(_, hostname))
|
||||
}
|
||||
|
||||
final class RemoteBakerEventListenerClient(client: Client[IO], hostname: Uri) {
|
||||
|
||||
def fireEvent(event: BakerEvent): IO[Status] = {
|
||||
val request = POST(
|
||||
event,
|
||||
hostname / "api" / "v3" / "baker-event",
|
||||
`X-Bakery-Intent`(Intent.`Remote-Event-Listener`, hostname)
|
||||
)
|
||||
client.status(request)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.ing.baker.baas.protocol
|
||||
|
||||
import cats.data.EitherT
|
||||
import cats.effect.IO
|
||||
import com.ing.baker.runtime.serialization.ProtoMap
|
||||
import org.http4s.EntityDecoder.collectBinary
|
||||
import org.http4s.util.CaseInsensitiveString
|
||||
import org.http4s._
|
||||
|
||||
import scala.util.{Failure, Success}
|
||||
|
||||
object BakeryHttp {
|
||||
|
||||
object ProtoEntityEncoders {
|
||||
|
||||
type ProtoMessage[A] = scalapb.GeneratedMessage with scalapb.Message[A]
|
||||
|
||||
implicit def protoDecoder[A, P <: ProtoMessage[P]](implicit protoMap: ProtoMap[A, P]): EntityDecoder[IO, A] =
|
||||
EntityDecoder.decodeBy(MediaType.application.`octet-stream`)(collectBinary[IO]).map(_.toArray)
|
||||
.flatMapR { bytes =>
|
||||
protoMap.fromByteArray(bytes) match {
|
||||
case Success(a) =>
|
||||
EitherT.fromEither[IO](Right(a))
|
||||
case Failure(exception) =>
|
||||
EitherT.fromEither[IO](Left(MalformedMessageBodyFailure(exception.getMessage, Some(exception))))
|
||||
}
|
||||
}
|
||||
|
||||
implicit def protoEncoder[A, P <: ProtoMessage[P]](implicit protoMap: ProtoMap[A, P]): EntityEncoder[IO, A] =
|
||||
EntityEncoder.byteArrayEncoder[IO].contramap(protoMap.toByteArray)
|
||||
}
|
||||
|
||||
object Headers {
|
||||
|
||||
def `X-Bakery-Intent`(intent: Intent, hostname: Uri): Header = {
|
||||
Header.Raw(CaseInsensitiveString("X-Bakery-Intent"), intent.render(hostname))
|
||||
}
|
||||
|
||||
sealed abstract class Intent(rawIntent: String) {
|
||||
|
||||
def render(hostname: Uri): String = {
|
||||
val intendedHost = hostname.authority.map(_.host.value).getOrElse("unknown")
|
||||
s"$rawIntent:$intendedHost"
|
||||
}
|
||||
}
|
||||
|
||||
object Intent {
|
||||
|
||||
case object `Remote-Event-Listener` extends Intent("Remote-Event-Listener")
|
||||
|
||||
case object `Remote-Baker-Event-Listener` extends Intent("Remote-Baker-Event-Listener")
|
||||
|
||||
case object `Remote-Interaction` extends Intent("Remote-Interaction")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
package com.ing.baker.baas.protocol
|
||||
|
||||
import akka.http.scaladsl.marshalling.{Marshaller, ToEntityMarshaller}
|
||||
import akka.http.scaladsl.model.{ContentTypes, HttpResponse, MediaTypes, StatusCodes}
|
||||
import akka.http.scaladsl.server.Directives.{complete, onSuccess}
|
||||
import akka.http.scaladsl.server.Route
|
||||
import akka.http.scaladsl.unmarshalling.{FromEntityUnmarshaller, Unmarshal, Unmarshaller}
|
||||
import akka.stream.Materializer
|
||||
import com.ing.baker.runtime.common.BakerException
|
||||
import com.ing.baker.runtime.serialization.ProtoMap
|
||||
import BaaSProto._
|
||||
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
object MarshallingUtils {
|
||||
|
||||
type ProtoMessage[A] = scalapb.GeneratedMessage with scalapb.Message[A]
|
||||
|
||||
def completeWithBakerFailures[A, P <: ProtoMessage[P]](f: Future[A])(implicit ec: ExecutionContext, m1: ProtoMap[A, P]): Route =
|
||||
complete(f.map(Right(_)).recover { case e: BakerException => Left(BaaSProtocol.BaaSRemoteFailure(e)) })
|
||||
|
||||
def completeWithBakerFailures(f: Future[Unit])(implicit ec: ExecutionContext): Route =
|
||||
onSuccess(f.map(_ => None).recover { case e: BakerException => Some(e) }) {
|
||||
case Some(e) => complete(BaaSProtocol.BaaSRemoteFailure(e))
|
||||
case None => complete(StatusCodes.OK)
|
||||
}
|
||||
|
||||
case class UnmarshalWithBakerExceptions[A](response: HttpResponse) {
|
||||
|
||||
def withBakerExceptions[P <: ProtoMessage[P]](implicit ec: ExecutionContext, mat: Materializer, m1: ProtoMap[A, P]): Future[A] = {
|
||||
for {
|
||||
decoded <- Unmarshal(response).to[Either[BaaSProtocol.BaaSRemoteFailure, A]]
|
||||
response <- decoded match {
|
||||
case Left(e) => Future.failed(e.error)
|
||||
case Right(a) => Future.successful(a)
|
||||
}
|
||||
} yield response
|
||||
}
|
||||
}
|
||||
|
||||
def unmarshal[A](response: HttpResponse): UnmarshalWithBakerExceptions[A] =
|
||||
UnmarshalWithBakerExceptions[A](response)
|
||||
|
||||
def unmarshalBakerExceptions(response: HttpResponse)(implicit ec: ExecutionContext, mat: Materializer): Future[Unit] =
|
||||
response.entity.httpEntity.contentType match {
|
||||
case ContentTypes.`application/octet-stream` =>
|
||||
Unmarshal(response)
|
||||
.to[BaaSProtocol.BaaSRemoteFailure]
|
||||
.flatMap(e => Future.failed(e.error))
|
||||
case _ =>
|
||||
Future.successful(())
|
||||
}
|
||||
|
||||
implicit def protoMarshaller[A, P <: ProtoMessage[P]](implicit mapping: ProtoMap[A, P]): ToEntityMarshaller[A] =
|
||||
Marshaller.ByteArrayMarshaller.wrap(MediaTypes.`application/octet-stream`)(mapping.toByteArray)
|
||||
|
||||
implicit def protoUnmarshaller[A, P <: ProtoMessage[P]](implicit mapping: ProtoMap[A, P]): FromEntityUnmarshaller[A] =
|
||||
Unmarshaller.byteArrayUnmarshaller.map(mapping.fromByteArray(_).get)
|
||||
|
||||
implicit def protoEitherMarshaller[A, P0 <: ProtoMessage[P0], B, P1 <: ProtoMessage[P1]](implicit m1: ProtoMap[A, P0], m2: ProtoMap[B, P1]): ToEntityMarshaller[Either[A, B]] =
|
||||
Marshaller.ByteArrayMarshaller.wrap(MediaTypes.`application/octet-stream`) {
|
||||
case Left(a) => m1.toByteArray(a)
|
||||
case Right(b) => m2.toByteArray(b)
|
||||
}
|
||||
|
||||
implicit def protoEitherUnmarshaller[A, P0 <: ProtoMessage[P0], B, P1 <: ProtoMessage[P1]](implicit m1: ProtoMap[A, P0], m2: ProtoMap[B, P1]): FromEntityUnmarshaller[Either[A, B]] =
|
||||
Unmarshaller.byteArrayUnmarshaller.map { byteArray =>
|
||||
m1.fromByteArray(byteArray).map(Left(_)).orElse(m2.fromByteArray(byteArray).map(Right(_))).get
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
package com.ing.baker.baas.akka
|
||||
|
||||
import akka.actor.ActorSystem
|
||||
import akka.http.scaladsl.Http
|
||||
import akka.http.scaladsl.marshalling.{Marshal, Marshaller, ToEntityMarshaller}
|
||||
import akka.http.scaladsl.model.Uri.Path
|
||||
import akka.http.scaladsl.model.headers.RawHeader
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.scaladsl.unmarshalling.{FromEntityUnmarshaller, Unmarshal, Unmarshaller}
|
||||
import akka.stream.Materializer
|
||||
import com.ing.baker.baas.protocol.InteractionSchedulingProto._
|
||||
import com.ing.baker.baas.protocol.ProtocolInteractionExecution
|
||||
import com.ing.baker.runtime.scaladsl.{EventInstance, IngredientInstance}
|
||||
import com.ing.baker.runtime.serialization.{Encryption, ProtoMap}
|
||||
import com.ing.baker.types.Type
|
||||
|
||||
import scala.concurrent.Future
|
||||
|
||||
object RemoteInteractionClient {
|
||||
|
||||
def apply(hostname: String)(implicit system: ActorSystem, mat: Materializer, encryption: Encryption) =
|
||||
new RemoteInteractionClient(Uri(hostname))
|
||||
}
|
||||
|
||||
class RemoteInteractionClient(hostname: Uri)(implicit system: ActorSystem, mat: Materializer, encryption: Encryption) {
|
||||
|
||||
import system.dispatcher
|
||||
|
||||
val intendedHost: String = hostname.authority.host.toString()
|
||||
|
||||
private type ProtoMessage[A] = scalapb.GeneratedMessage with scalapb.Message[A]
|
||||
|
||||
private implicit def protoMarshaller[A, P <: ProtoMessage[P]](implicit mapping: ProtoMap[A, P]): ToEntityMarshaller[A] =
|
||||
Marshaller.ByteArrayMarshaller.wrap(MediaTypes.`application/octet-stream`)(mapping.toByteArray)
|
||||
|
||||
private implicit def protoUnmarshaller[A, P <: ProtoMessage[P]](implicit mapping: ProtoMap[A, P]): FromEntityUnmarshaller[A] =
|
||||
Unmarshaller.byteArrayUnmarshaller.map(mapping.fromByteArray(_).get)
|
||||
|
||||
private implicit def protoEitherMarshaller[A, P0 <: ProtoMessage[P0], B, P1 <: ProtoMessage[P1]](implicit m1: ProtoMap[A, P0], m2: ProtoMap[B, P1]): ToEntityMarshaller[Either[A, B]] =
|
||||
Marshaller.ByteArrayMarshaller.wrap(MediaTypes.`application/octet-stream`) {
|
||||
case Left(a) => m1.toByteArray(a)
|
||||
case Right(b) => m2.toByteArray(b)
|
||||
}
|
||||
|
||||
private implicit def protoEitherUnmarshaller[A, P0 <: ProtoMessage[P0], B, P1 <: ProtoMessage[P1]](implicit m1: ProtoMap[A, P0], m2: ProtoMap[B, P1]): FromEntityUnmarshaller[Either[A, B]] =
|
||||
Unmarshaller.byteArrayUnmarshaller.map { byteArray =>
|
||||
m1.fromByteArray(byteArray).map(Left(_)).orElse(m2.fromByteArray(byteArray).map(Right(_))).get
|
||||
}
|
||||
|
||||
private val root: Path = Path./("api")./("v3")
|
||||
|
||||
private def withPath(path: Path): Uri = hostname.withPath(path)
|
||||
|
||||
def interface: Future[(String, Seq[Type])] = {
|
||||
val request = HttpRequest(method = HttpMethods.GET, uri = withPath(root./("interface")))
|
||||
.withHeaders(RawHeader("X-Bakery-Intent", s"Remote-Interaction:$intendedHost"))
|
||||
for {
|
||||
response <- Http().singleRequest(request)
|
||||
decoded <- Unmarshal(response).to[ProtocolInteractionExecution.InstanceInterface]
|
||||
} yield (decoded.name, decoded.input)
|
||||
}
|
||||
|
||||
def apply(input: Seq[IngredientInstance]): Future[Option[EventInstance]] =
|
||||
for {
|
||||
encoded <- Marshal(ProtocolInteractionExecution.ExecuteInstance(input)).to[MessageEntity]
|
||||
request = HttpRequest(method = HttpMethods.POST, uri = withPath(root./("apply")), entity = encoded)
|
||||
.withHeaders(RawHeader("X-Bakery-Intent", s"Remote-Interaction:$intendedHost"))
|
||||
response <- Http().singleRequest(request)
|
||||
decoded <- Unmarshal(response).to[Either[
|
||||
ProtocolInteractionExecution.InstanceExecutedSuccessfully,
|
||||
ProtocolInteractionExecution.InstanceExecutionFailed]]
|
||||
result <- decoded match {
|
||||
case Left(result) =>
|
||||
Future.successful(result.result)
|
||||
case Right(e) =>
|
||||
Future.failed(new RuntimeException(s"Remote interaction failed with message: ${e.message}"))
|
||||
}
|
||||
} yield result
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.ing.baker.baas.interaction
|
||||
|
||||
import cats.data.EitherT
|
||||
import cats.effect.IO
|
||||
import com.ing.baker.runtime.serialization.ProtoMap
|
||||
import org.http4s.EntityDecoder.collectBinary
|
||||
import org.http4s.util.CaseInsensitiveString
|
||||
import org.http4s._
|
||||
|
||||
import scala.util.{Failure, Success, Try}
|
||||
|
||||
object BakeryHttp {
|
||||
|
||||
object ProtoEntityEncoders {
|
||||
|
||||
type ProtoMessage[A] = scalapb.GeneratedMessage with scalapb.Message[A]
|
||||
|
||||
implicit def protoDecoder[A, P <: ProtoMessage[P]](implicit protoMap: ProtoMap[A, P]): EntityDecoder[IO, A] =
|
||||
EntityDecoder.decodeBy(MediaType.application.`octet-stream`)(collectBinary[IO]).map(_.toArray)
|
||||
.flatMapR { bytes =>
|
||||
protoMap.fromByteArray(bytes) match {
|
||||
case Success(a) =>
|
||||
EitherT.fromEither[IO](Right(a))
|
||||
case Failure(exception) =>
|
||||
EitherT.fromEither[IO](Left(MalformedMessageBodyFailure(exception.getMessage, Some(exception))))
|
||||
}
|
||||
}
|
||||
|
||||
implicit def protoEncoder[A, P <: ProtoMessage[P]](implicit protoMap: ProtoMap[A, P]): EntityEncoder[IO, A] =
|
||||
EntityEncoder.byteArrayEncoder[IO].contramap(protoMap.toByteArray)
|
||||
|
||||
implicit def protoEitherDecoder[A, P0 <: ProtoMessage[P0], B, P1 <: ProtoMessage[P1]](implicit p1: ProtoMap[A, P0], p2: ProtoMap[B, P1]): EntityDecoder[IO, Either[A, B]] =
|
||||
EntityDecoder.decodeBy(MediaType.application.`octet-stream`)(collectBinary[IO]).map(_.toArray)
|
||||
.flatMapR { bytes =>
|
||||
val eitherTry: Try[Either[A, B]] =
|
||||
p1.fromByteArray(bytes).map[Either[A, B]](Left(_))
|
||||
.orElse(p2.fromByteArray(bytes).map[Either[A, B]](Right(_)))
|
||||
eitherTry match {
|
||||
case Success(a) =>
|
||||
EitherT.fromEither[IO](Right(a))
|
||||
case Failure(exception) =>
|
||||
EitherT.fromEither[IO](Left(MalformedMessageBodyFailure(exception.getMessage, Some(exception))))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object Headers {
|
||||
|
||||
def `X-Bakery-Intent`(intent: Intent, hostname: Uri): Header = {
|
||||
Header.Raw(CaseInsensitiveString("X-Bakery-Intent"), intent.render(hostname))
|
||||
}
|
||||
|
||||
sealed abstract class Intent(rawIntent: String) {
|
||||
|
||||
def render(hostname: Uri): String = {
|
||||
val intendedHost = hostname.authority.map(_.host.value).getOrElse("unknown")
|
||||
s"$rawIntent:$intendedHost"
|
||||
}
|
||||
}
|
||||
|
||||
object Intent {
|
||||
|
||||
case object `Remote-Event-Listener` extends Intent("Remote-Event-Listener")
|
||||
|
||||
case object `Remote-Baker-Event-Listener` extends Intent("Remote-Baker-Event-Listener")
|
||||
|
||||
case object `Remote-Interaction` extends Intent("Remote-Interaction")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.ing.baker.baas.interaction
|
||||
|
||||
import cats.effect.{ContextShift, IO, Resource, Timer}
|
||||
import com.ing.baker.baas.interaction.BakeryHttp.Headers.{Intent, `X-Bakery-Intent`}
|
||||
import com.ing.baker.baas.interaction.BakeryHttp.ProtoEntityEncoders._
|
||||
import com.ing.baker.baas.protocol.InteractionSchedulingProto._
|
||||
import com.ing.baker.baas.protocol.ProtocolInteractionExecution
|
||||
import com.ing.baker.runtime.scaladsl.{EventInstance, IngredientInstance}
|
||||
import com.ing.baker.types.Type
|
||||
import org.http4s.Method._
|
||||
import org.http4s.Uri
|
||||
import org.http4s.client.Client
|
||||
import org.http4s.client.blaze.BlazeClientBuilder
|
||||
import org.http4s.client.dsl.io._
|
||||
|
||||
import scala.concurrent.ExecutionContext
|
||||
|
||||
object RemoteInteractionClient {
|
||||
|
||||
/** use method `use` of the Resource, the client will be acquired and shut down automatically each time
|
||||
* the resulting `IO` is run, each time using the common connection pool.
|
||||
*/
|
||||
def resource(hostname: Uri, pool: ExecutionContext)(implicit cs: ContextShift[IO], timer: Timer[IO]): Resource[IO, RemoteInteractionClient] =
|
||||
BlazeClientBuilder[IO](pool)
|
||||
.resource
|
||||
.map(new RemoteInteractionClient(_, hostname))
|
||||
}
|
||||
|
||||
final class RemoteInteractionClient(client: Client[IO], hostname: Uri)(implicit cs: ContextShift[IO], timer: Timer[IO]) {
|
||||
|
||||
def interface: IO[(String, Seq[Type])] = {
|
||||
val request = GET(
|
||||
hostname / "api" / "v3" / "interface",
|
||||
`X-Bakery-Intent`(Intent.`Remote-Interaction`, hostname)
|
||||
)
|
||||
client.expect[ProtocolInteractionExecution.InstanceInterface](request)
|
||||
.map(message => (message.name, message.input))
|
||||
}
|
||||
|
||||
def runInteraction(input: Seq[IngredientInstance]): IO[Option[EventInstance]] = {
|
||||
val request = POST(
|
||||
ProtocolInteractionExecution.ExecuteInstance(input),
|
||||
hostname / "api" / "v3" / "run-interaction",
|
||||
`X-Bakery-Intent`(Intent.`Remote-Interaction`, hostname)
|
||||
)
|
||||
client.expect[Either[
|
||||
ProtocolInteractionExecution.InstanceExecutedSuccessfully,
|
||||
ProtocolInteractionExecution.InstanceExecutionFailed]](request)
|
||||
.flatMap {
|
||||
case Left(result) =>
|
||||
IO.pure(result.result)
|
||||
case Right(e) =>
|
||||
IO.raiseError(new RuntimeException(s"Remote interaction failed with message: ${e.message}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
package com.ing.baker.baas.akka
|
||||
|
||||
import akka.actor.ActorSystem
|
||||
import akka.http.scaladsl.Http
|
||||
import akka.http.scaladsl.marshalling.{Marshal, Marshaller, ToEntityMarshaller}
|
||||
import akka.http.scaladsl.model.Uri.Path
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.scaladsl.model.headers.RawHeader
|
||||
import akka.http.scaladsl.unmarshalling.{FromEntityUnmarshaller, Unmarshaller}
|
||||
import com.ing.baker.baas.protocol.DistributedEventPublishingProto._
|
||||
import com.ing.baker.baas.protocol.ProtocolDistributedEventPublishing
|
||||
import com.ing.baker.runtime.scaladsl.{EventInstance, RecipeEventMetadata}
|
||||
import com.ing.baker.runtime.serialization.{Encryption, ProtoMap}
|
||||
|
||||
import scala.concurrent.Future
|
||||
|
||||
object RemoteEventListenerClient {
|
||||
|
||||
def apply(hostname: String)(implicit system: ActorSystem, encryption: Encryption) =
|
||||
new RemoteEventListenerClient(Uri(hostname))
|
||||
}
|
||||
|
||||
class RemoteEventListenerClient(hostname: Uri)(implicit system: ActorSystem, encryption: Encryption) {
|
||||
|
||||
import system.dispatcher
|
||||
|
||||
val intendedHost: String = hostname.authority.host.toString()
|
||||
|
||||
private type ProtoMessage[A] = scalapb.GeneratedMessage with scalapb.Message[A]
|
||||
|
||||
private implicit def protoMarshaller[A, P <: ProtoMessage[P]](implicit mapping: ProtoMap[A, P]): ToEntityMarshaller[A] =
|
||||
Marshaller.ByteArrayMarshaller.wrap(MediaTypes.`application/octet-stream`)(mapping.toByteArray)
|
||||
|
||||
private implicit def protoUnmarshaller[A, P <: ProtoMessage[P]](implicit mapping: ProtoMap[A, P]): FromEntityUnmarshaller[A] =
|
||||
Unmarshaller.byteArrayUnmarshaller.map(mapping.fromByteArray(_).get)
|
||||
|
||||
private implicit def protoEitherMarshaller[A, P0 <: ProtoMessage[P0], B, P1 <: ProtoMessage[P1]](implicit m1: ProtoMap[A, P0], m2: ProtoMap[B, P1]): ToEntityMarshaller[Either[A, B]] =
|
||||
Marshaller.ByteArrayMarshaller.wrap(MediaTypes.`application/octet-stream`) {
|
||||
case Left(a) => m1.toByteArray(a)
|
||||
case Right(b) => m2.toByteArray(b)
|
||||
}
|
||||
|
||||
private val root: Path = Path./("api")./("v3")
|
||||
|
||||
private def withPath(path: Path): Uri = hostname.withPath(path)
|
||||
|
||||
def apply(recipeEventMetadata: RecipeEventMetadata, event: EventInstance): Future[Unit] =
|
||||
for {
|
||||
encoded <- Marshal(ProtocolDistributedEventPublishing.Event(recipeEventMetadata, event)).to[MessageEntity]
|
||||
request = HttpRequest(method = HttpMethods.POST, uri = withPath(root./("apply")), entity = encoded)
|
||||
.withHeaders(RawHeader("X-Bakery-Intent", s"Remote-Event-Listener:$intendedHost"))
|
||||
response <- Http().singleRequest(request)
|
||||
} yield ()
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.ing.baker.baas.recipelistener
|
||||
|
||||
import cats.data.EitherT
|
||||
import cats.effect.IO
|
||||
import com.ing.baker.runtime.serialization.ProtoMap
|
||||
import org.http4s.EntityDecoder.collectBinary
|
||||
import org.http4s.util.CaseInsensitiveString
|
||||
import org.http4s.{EntityDecoder, EntityEncoder, Header, MalformedMessageBodyFailure, MediaType, Uri}
|
||||
|
||||
import scala.util.{Failure, Success}
|
||||
|
||||
object BakeryHttp {
|
||||
|
||||
object ProtoEntityEncoders {
|
||||
|
||||
type ProtoMessage[A] = scalapb.GeneratedMessage with scalapb.Message[A]
|
||||
|
||||
implicit def protoDecoder[A, P <: ProtoMessage[P]](implicit protoMap: ProtoMap[A, P]): EntityDecoder[IO, A] =
|
||||
EntityDecoder.decodeBy(MediaType.application.`octet-stream`)(collectBinary[IO]).map(_.toArray)
|
||||
.flatMapR { bytes =>
|
||||
protoMap.fromByteArray(bytes) match {
|
||||
case Success(a) =>
|
||||
EitherT.fromEither[IO](Right(a))
|
||||
case Failure(exception) =>
|
||||
EitherT.fromEither[IO](Left(MalformedMessageBodyFailure(exception.getMessage, Some(exception))))
|
||||
}
|
||||
}
|
||||
|
||||
implicit def protoEncoder[A, P <: ProtoMessage[P]](implicit protoMap: ProtoMap[A, P]): EntityEncoder[IO, A] =
|
||||
EntityEncoder.byteArrayEncoder[IO].contramap(protoMap.toByteArray)
|
||||
}
|
||||
|
||||
object Headers {
|
||||
|
||||
def `X-Bakery-Intent`(intent: Intent, hostname: Uri): Header = {
|
||||
Header.Raw(CaseInsensitiveString("X-Bakery-Intent"), intent.render(hostname))
|
||||
}
|
||||
|
||||
sealed abstract class Intent(rawIntent: String) {
|
||||
|
||||
def render(hostname: Uri): String = {
|
||||
val intendedHost = hostname.authority.map(_.host.value).getOrElse("unknown")
|
||||
s"$rawIntent:$intendedHost"
|
||||
}
|
||||
}
|
||||
|
||||
object Intent {
|
||||
|
||||
case object `Remote-Event-Listener` extends Intent("Remote-Event-Listener")
|
||||
|
||||
case object `Remote-Baker-Event-Listener` extends Intent("Remote-Baker-Event-Listener")
|
||||
|
||||
case object `Remote-Interaction` extends Intent("Remote-Interaction")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.ing.baker.baas.recipelistener
|
||||
|
||||
import cats.effect.{ContextShift, IO, Resource, Timer}
|
||||
import com.ing.baker.baas.protocol.DistributedEventPublishingProto._
|
||||
import com.ing.baker.baas.protocol.ProtocolDistributedEventPublishing
|
||||
import com.ing.baker.baas.recipelistener.BakeryHttp.Headers.`X-Bakery-Intent`
|
||||
import com.ing.baker.baas.recipelistener.BakeryHttp.Headers.Intent
|
||||
import com.ing.baker.baas.recipelistener.BakeryHttp.ProtoEntityEncoders._
|
||||
import com.ing.baker.runtime.scaladsl.{EventInstance, RecipeEventMetadata}
|
||||
import org.http4s.Method._
|
||||
import org.http4s.client.Client
|
||||
import org.http4s.client.blaze.BlazeClientBuilder
|
||||
import org.http4s.client.dsl.io._
|
||||
import org.http4s.{Status, Uri}
|
||||
|
||||
import scala.concurrent.ExecutionContext
|
||||
|
||||
object RemoteEventListenerClient {
|
||||
|
||||
/** use method `use` of the Resource, the client will be acquired and shut down automatically each time
|
||||
* the resulting `IO` is run, each time using the common connection pool.
|
||||
*/
|
||||
def resource(hostname: Uri, pool: ExecutionContext)(implicit cs: ContextShift[IO], timer: Timer[IO]): Resource[IO, RemoteEventListenerClient] =
|
||||
BlazeClientBuilder[IO](pool)
|
||||
.resource
|
||||
.map(new RemoteEventListenerClient(_, hostname))
|
||||
}
|
||||
|
||||
final class RemoteEventListenerClient(client: Client[IO], hostname: Uri)(implicit cs: ContextShift[IO], timer: Timer[IO]) {
|
||||
|
||||
def fireEvent(recipeEventMetadata: RecipeEventMetadata, event: EventInstance): IO[Status] = {
|
||||
val request = POST(
|
||||
ProtocolDistributedEventPublishing.Event(recipeEventMetadata, event),
|
||||
hostname / "api" / "v3" / "recipe-event",
|
||||
`X-Bakery-Intent`(Intent.`Remote-Event-Listener`, hostname)
|
||||
)
|
||||
client.status(request)
|
||||
}
|
||||
}
|
||||
55
build.sbt
55
build.sbt
@@ -102,7 +102,6 @@ lazy val `baker-interface` = project.in(file("baker-interface"))
|
||||
.settings(
|
||||
moduleName := "baker-interface",
|
||||
libraryDependencies ++= Seq(
|
||||
akkaActor,
|
||||
catsEffect,
|
||||
scalaJava8Compat
|
||||
) ++ providedDeps(findbugs) ++ testDeps(
|
||||
@@ -219,8 +218,8 @@ lazy val `baas-protocol-baker` = project.in(file("baas-protocol-baker"))
|
||||
.settings(
|
||||
moduleName := "baas-protocol-baker",
|
||||
libraryDependencies ++= Seq(
|
||||
akkaStream,
|
||||
akkaHttp
|
||||
http4s,
|
||||
http4sDsl
|
||||
)
|
||||
)
|
||||
.dependsOn(`baker-interface`)
|
||||
@@ -231,8 +230,9 @@ lazy val `baas-protocol-interaction-scheduling` = project.in(file("baas-protocol
|
||||
.settings(
|
||||
moduleName := "baas-protocol-interaction-scheduling",
|
||||
libraryDependencies ++= Seq(
|
||||
akkaStream,
|
||||
akkaHttp
|
||||
http4s,
|
||||
http4sDsl,
|
||||
http4sClient
|
||||
)
|
||||
)
|
||||
.dependsOn(`baker-interface`)
|
||||
@@ -243,10 +243,6 @@ lazy val `baas-protocol-recipe-event-publishing` = project.in(file("baas-protoco
|
||||
.settings(
|
||||
moduleName := "baas-protocol-recipe-event-publishing",
|
||||
libraryDependencies ++= Seq(
|
||||
akkaHttp,
|
||||
akkaStream,
|
||||
slf4jApi,
|
||||
slf4jSimple,
|
||||
http4s,
|
||||
http4sDsl,
|
||||
http4sClient
|
||||
@@ -260,8 +256,9 @@ lazy val `baas-protocol-baker-event-publishing` = project.in(file("baas-protocol
|
||||
.settings(
|
||||
moduleName := "baas-protocol-baker-event-publishing",
|
||||
libraryDependencies ++= Seq(
|
||||
akkaStream,
|
||||
akkaHttp
|
||||
http4s,
|
||||
http4sDsl,
|
||||
http4sClient
|
||||
)
|
||||
)
|
||||
.dependsOn(`baker-interface`)
|
||||
@@ -271,8 +268,9 @@ lazy val `baas-node-client` = project.in(file("baas-node-client"))
|
||||
.settings(
|
||||
moduleName := "baas-node-client",
|
||||
libraryDependencies ++= Seq(
|
||||
akkaStream,
|
||||
akkaHttp
|
||||
http4s,
|
||||
http4sDsl,
|
||||
http4sClient
|
||||
)
|
||||
)
|
||||
.dependsOn(`baker-interface`, `baas-protocol-baker`)
|
||||
@@ -288,18 +286,19 @@ lazy val `baas-node-state` = project.in(file("baas-node-state"))
|
||||
libraryDependencies ++= Seq(
|
||||
slf4jApi,
|
||||
slf4jSimple,
|
||||
akkaHttp,
|
||||
akkaPersistenceCassandra,
|
||||
akkaManagementHttp,
|
||||
akkaClusterBoostrap,
|
||||
akkaDiscoveryKube,
|
||||
kubernetesJavaClient
|
||||
kubernetesJavaClient,
|
||||
http4s,
|
||||
http4sDsl,
|
||||
http4sServer
|
||||
) ++ testDeps(
|
||||
slf4jApi,
|
||||
slf4jSimple,
|
||||
scalaTest,
|
||||
mockServer,
|
||||
akkaHttpCirce,
|
||||
circe,
|
||||
circeGeneric
|
||||
)
|
||||
@@ -325,15 +324,13 @@ lazy val `baas-node-interaction` = project.in(file("baas-node-interaction"))
|
||||
.settings(
|
||||
moduleName := "baas-node-interaction",
|
||||
libraryDependencies ++= Seq(
|
||||
akkaCluster,
|
||||
akkaClusterTools,
|
||||
akkaHttp,
|
||||
slf4jApi
|
||||
slf4jApi,
|
||||
slf4jSimple,
|
||||
http4s,
|
||||
http4sDsl,
|
||||
http4sServer
|
||||
) ++ testDeps(
|
||||
akkaSlf4j,
|
||||
scalaTest,
|
||||
junitInterface,
|
||||
scalaCheck
|
||||
scalaTest
|
||||
)
|
||||
)
|
||||
.dependsOn(`baas-protocol-interaction-scheduling`, `baker-interface`)
|
||||
@@ -358,13 +355,13 @@ lazy val `baas-node-baker-event-listener` = project.in(file("baas-node-baker-eve
|
||||
.settings(
|
||||
moduleName := "baas-node-baker-event-listener",
|
||||
libraryDependencies ++= Seq(
|
||||
akkaHttp,
|
||||
slf4jApi,
|
||||
slf4jSimple
|
||||
slf4jSimple,
|
||||
http4s,
|
||||
http4sDsl,
|
||||
http4sServer
|
||||
) ++ testDeps(
|
||||
scalaTest,
|
||||
junitInterface,
|
||||
scalaCheck
|
||||
scalaTest
|
||||
)
|
||||
)
|
||||
.dependsOn(`baas-protocol-baker-event-publishing`, `baker-interface`)
|
||||
|
||||
@@ -1,54 +1,37 @@
|
||||
package webshop.webservice
|
||||
|
||||
import akka.actor.ActorSystem
|
||||
import akka.http.scaladsl.model.Uri
|
||||
import akka.stream.ActorMaterializer
|
||||
import cats.effect.{ExitCode, IO, IOApp}
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
import cats.effect.{ExitCode, IO, IOApp, Resource}
|
||||
import cats.implicits._
|
||||
import com.ing.baker.baas.scaladsl.BakerClient
|
||||
import com.ing.baker.runtime.scaladsl._
|
||||
import com.ing.baker.runtime.serialization.Encryption
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import org.http4s.Uri
|
||||
import org.http4s.server.blaze.BlazeServerBuilder
|
||||
|
||||
import scala.concurrent.Await
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.ExecutionContext
|
||||
|
||||
object Main extends IOApp {
|
||||
|
||||
case class AppDependencies(actorSystem: ActorSystem, baker: Baker, app: WebShopService, port: Int)
|
||||
|
||||
def dependencies: IO[AppDependencies] = {
|
||||
val config = ConfigFactory.load()
|
||||
val baasHostname = config.getString("baas.state-node-hostname")
|
||||
val httpPort = config.getInt("baas-component.http-api-port")
|
||||
|
||||
implicit val system = ActorSystem("CheckoutService")
|
||||
implicit val materializer = ActorMaterializer()
|
||||
implicit val encryption = Encryption.from(config)
|
||||
|
||||
val baker = BakerClient(Uri.parseAbsolute(baasHostname))
|
||||
sys.addShutdownHook(Await.result(baker.gracefulShutdown(), 20.seconds))
|
||||
|
||||
import system.dispatcher
|
||||
|
||||
for {
|
||||
checkoutRecipeId <- WebShopBaker.initRecipes(baker)
|
||||
webShopBaker = new WebShopBaker(baker, checkoutRecipeId)
|
||||
app = new WebShopService(webShopBaker)
|
||||
resources = AppDependencies(system, baker, app, httpPort)
|
||||
} yield resources
|
||||
}
|
||||
|
||||
override def run(args: List[String]): IO[ExitCode] =
|
||||
for {
|
||||
deps <- dependencies
|
||||
exitCode <- BlazeServerBuilder[IO]
|
||||
.bindHttp(deps.port, "0.0.0.0")
|
||||
.withHttpApp(deps.app.build)
|
||||
override def run(args: List[String]): IO[ExitCode] = {
|
||||
val config =
|
||||
ConfigFactory.load()
|
||||
val baasHostname =
|
||||
config.getString("baas.state-node-hostname")
|
||||
val httpPort =
|
||||
config.getInt("baas-component.http-api-port")
|
||||
val connectionPool =
|
||||
ExecutionContext.fromExecutor(Executors.newCachedThreadPool())
|
||||
val mainResource = for {
|
||||
baker <- BakerClient.resource(Uri.unsafeFromString(baasHostname), connectionPool)
|
||||
checkoutRecipeId <- Resource.liftF(WebShopBaker.initRecipes(baker))
|
||||
_ <- BlazeServerBuilder[IO]
|
||||
.bindHttp(httpPort, "0.0.0.0")
|
||||
.withHttpApp(new WebShopService(new WebShopBaker(baker, checkoutRecipeId)).build)
|
||||
.resource
|
||||
.use(_ => IO.never)
|
||||
.as(ExitCode.Success)
|
||||
} yield exitCode
|
||||
|
||||
} yield ()
|
||||
mainResource
|
||||
.use(_ => IO.never)
|
||||
.as(ExitCode.Success)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,14 +3,13 @@ package webshop.webservice
|
||||
import java.util.UUID
|
||||
|
||||
import cats.effect.{ContextShift, IO, Timer}
|
||||
import cats.implicits._
|
||||
import com.ing.baker.compiler.RecipeCompiler
|
||||
import com.ing.baker.il.CompiledRecipe
|
||||
import com.ing.baker.runtime.scaladsl.{Baker, EventInstance}
|
||||
import com.typesafe.scalalogging.LazyLogging
|
||||
import webshop.webservice.CheckoutFlowIngredients.{Item, OrderId, PaymentInformation, ShippingAddress}
|
||||
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
object WebShopBaker {
|
||||
|
||||
val checkoutFlowCompiledRecipe: CompiledRecipe =
|
||||
@@ -20,62 +19,54 @@ object WebShopBaker {
|
||||
IO.fromFuture(IO(baker.addRecipe(checkoutFlowCompiledRecipe)))
|
||||
}
|
||||
|
||||
class WebShopBaker(baker: Baker, checkoutRecipeId: String)(implicit cs: ContextShift[IO], ec: ExecutionContext) extends WebShop with LazyLogging {
|
||||
class WebShopBaker(baker: Baker, checkoutRecipeId: String)(implicit cs: ContextShift[IO]) extends WebShop with LazyLogging {
|
||||
|
||||
override def createCheckoutOrder(items: List[String]): IO[String] =
|
||||
IO.fromFuture(IO {
|
||||
val orderId: String = UUID.randomUUID().toString
|
||||
val event = EventInstance.unsafeFrom(
|
||||
CheckoutFlowEvents.OrderPlaced(OrderId(orderId), items.map(Item)))
|
||||
for {
|
||||
_ <- baker.bake(checkoutRecipeId, orderId)
|
||||
status <- baker.fireEventAndResolveWhenReceived(orderId, event)
|
||||
_ = logger.info(s"${event.name}[$orderId]: $status")
|
||||
} yield orderId
|
||||
})
|
||||
override def createCheckoutOrder(items: List[String]): IO[String] = {
|
||||
val orderId: String = UUID.randomUUID().toString
|
||||
val event = EventInstance.unsafeFrom(
|
||||
CheckoutFlowEvents.OrderPlaced(OrderId(orderId), items.map(Item)))
|
||||
for {
|
||||
_ <- IO.fromFuture(IO(baker.bake(checkoutRecipeId, orderId)))
|
||||
status <- IO.fromFuture(IO(baker.fireEventAndResolveWhenReceived(orderId, event)))
|
||||
_ = logger.info(s"${event.name}[$orderId]: $status")
|
||||
} yield orderId
|
||||
}
|
||||
|
||||
override def addCheckoutAddressInfo(orderId: String, address: String): IO[Unit] =
|
||||
IO.fromFuture(IO {
|
||||
fireAndInformEvent(orderId, EventInstance.unsafeFrom(
|
||||
CheckoutFlowEvents.ShippingAddressReceived(ShippingAddress(address))))
|
||||
})
|
||||
fireAndInformEvent(orderId, EventInstance.unsafeFrom(
|
||||
CheckoutFlowEvents.ShippingAddressReceived(ShippingAddress(address))))
|
||||
|
||||
override def addCheckoutPaymentInfo(orderId: String, paymentInfo: String): IO[Unit] =
|
||||
IO.fromFuture(IO {
|
||||
fireAndInformEvent(orderId, EventInstance.unsafeFrom(
|
||||
CheckoutFlowEvents.PaymentInformationReceived(PaymentInformation(paymentInfo))))
|
||||
})
|
||||
fireAndInformEvent(orderId, EventInstance.unsafeFrom(
|
||||
CheckoutFlowEvents.PaymentInformationReceived(PaymentInformation(paymentInfo))))
|
||||
|
||||
private def fireAndInformEvent(orderId: String, event: EventInstance): Future[Unit] = {
|
||||
for {
|
||||
status <- baker.fireEventAndResolveWhenReceived(orderId, event)
|
||||
_ = logger.info(s"${event.name}[$orderId]: $status")
|
||||
} yield ()
|
||||
private def fireAndInformEvent(orderId: String, event: EventInstance): IO[Unit] = {
|
||||
IO.fromFuture(IO {
|
||||
baker.fireEventAndResolveWhenReceived(orderId, event)
|
||||
}).void
|
||||
}
|
||||
|
||||
override def pollOrderStatus(orderId: String): IO[OrderStatus] =
|
||||
IO.fromFuture(IO {
|
||||
for {
|
||||
state <- baker.getRecipeInstanceState(orderId)
|
||||
eventNames = state.events.map(_.name)
|
||||
status = {
|
||||
if (eventNames.contains("ShippingConfirmed"))
|
||||
OrderStatus.Complete
|
||||
else if (eventNames.contains("PaymentFailed"))
|
||||
OrderStatus.PaymentFailed
|
||||
else if (eventNames.contains("OrderHadUnavailableItems"))
|
||||
OrderStatus.UnavailableItems(state.ingredients("unavailableItems").as[List[Item]].map(_.itemId))
|
||||
else if (eventNames.containsSlice(List("ShippingAddressReceived", "PaymentInformationReceived")))
|
||||
OrderStatus.ProcessingPayment
|
||||
else if (eventNames.contains("PaymentSuccessful"))
|
||||
OrderStatus.ShippingItems
|
||||
else
|
||||
OrderStatus.InfoPending(List("ShippingAddressReceived", "PaymentInformationReceived")
|
||||
.filterNot(eventNames.contains)
|
||||
.map(_.replace("Received", "")))
|
||||
}
|
||||
} yield status
|
||||
})
|
||||
for {
|
||||
state <- IO.fromFuture(IO(baker.getRecipeInstanceState(orderId)))
|
||||
eventNames = state.events.map(_.name)
|
||||
status = {
|
||||
if (eventNames.contains("ShippingConfirmed"))
|
||||
OrderStatus.Complete
|
||||
else if (eventNames.contains("PaymentFailed"))
|
||||
OrderStatus.PaymentFailed
|
||||
else if (eventNames.contains("OrderHadUnavailableItems"))
|
||||
OrderStatus.UnavailableItems(state.ingredients("unavailableItems").as[List[Item]].map(_.itemId))
|
||||
else if (eventNames.containsSlice(List("ShippingAddressReceived", "PaymentInformationReceived")))
|
||||
OrderStatus.ProcessingPayment
|
||||
else if (eventNames.contains("PaymentSuccessful"))
|
||||
OrderStatus.ShippingItems
|
||||
else
|
||||
OrderStatus.InfoPending(List("ShippingAddressReceived", "PaymentInformationReceived")
|
||||
.filterNot(eventNames.contains)
|
||||
.map(_.replace("Received", "")))
|
||||
}
|
||||
} yield status
|
||||
|
||||
override def gracefulShutdown: IO[Unit] =
|
||||
IO {
|
||||
|
||||
@@ -40,8 +40,6 @@ object Dependencies {
|
||||
val akkaManagementHttp = "com.lightbend.akka.management" %% "akka-management-cluster-http" % "1.0.5"
|
||||
val akkaClusterBoostrap = "com.lightbend.akka.management" %% "akka-management-cluster-bootstrap" % "1.0.5"
|
||||
val akkaDiscoveryKube = "com.lightbend.akka.discovery" %% "akka-discovery-kubernetes-api" % "1.0.5"
|
||||
val akkaHttp = "com.typesafe.akka" %% "akka-http" % "10.1.11"
|
||||
val akkaHttpCirce = "de.heikoseeberger" %% "akka-http-circe" % "1.28.0"
|
||||
val akkaBoostrap = "com.lightbend.akka.management" %% "akka-management-cluster-bootstrap" % "1.0.5"
|
||||
|
||||
val levelDB = "org.iq80.leveldb" % "leveldb" % "0.12"
|
||||
|
||||
@@ -12,6 +12,6 @@ addSbtPlugin("com.jsuereth" % "sbt-pgp" % "2.0.1")
|
||||
|
||||
addSbtPlugin("com.typesafe.sbt" % "sbt-multi-jvm" % "0.4.0")
|
||||
|
||||
addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.6.0")
|
||||
addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.6.1")
|
||||
|
||||
libraryDependencies += "org.slf4j" % "slf4j-nop" % "1.7.29"
|
||||
@@ -3,12 +3,57 @@ package com.ing.baker.runtime.akka.internal
|
||||
import com.ing.baker.il.petrinet.InteractionTransition
|
||||
import com.ing.baker.runtime.scaladsl.{EventInstance, IngredientInstance, InteractionInstance}
|
||||
|
||||
import scala.concurrent.Future
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
/** The InteractionManager is responsible for all implementation of interactions.
|
||||
* It knows all available implementations and gives the correct implementation for an Interaction
|
||||
*/
|
||||
trait InteractionManager {
|
||||
def hasImplementation(interaction: InteractionTransition): Future[Boolean]
|
||||
|
||||
def executeImplementation(interaction: InteractionTransition, input: Seq[IngredientInstance]): Future[Option[EventInstance]]
|
||||
|
||||
/** Adds an interaction instance to the manager.
|
||||
*
|
||||
* @param interaction to be added
|
||||
* @return async writing process
|
||||
*/
|
||||
def addImplementation(interaction: InteractionInstance): Future[Unit]
|
||||
|
||||
/** Gets an implementation is available for the given interaction.
|
||||
* It checks:
|
||||
* 1. Name
|
||||
* 2. Input variable sizes
|
||||
* 3. Input variable types
|
||||
*
|
||||
* @param interaction The interaction to check
|
||||
* @return An option containing the implementation if available
|
||||
*/
|
||||
def getImplementation(interaction: InteractionTransition): Future[Option[InteractionInstance]]
|
||||
|
||||
/** Checks if an InteractionInstance in the manager matches the transition. */
|
||||
def hasImplementation(interaction: InteractionTransition)(implicit ec: ExecutionContext): Future[Boolean] =
|
||||
getImplementation(interaction).map(_.isDefined)
|
||||
|
||||
/** Tries to find InteractionInstance that can run the given transition with provided input. */
|
||||
def executeImplementation(interaction: InteractionTransition, input: Seq[IngredientInstance])(implicit ec: ExecutionContext): Future[Option[EventInstance]] = {
|
||||
getImplementation(interaction).flatMap {
|
||||
case Some(implementation) => implementation.run(input)
|
||||
case None => Future.failed(new FatalInteractionException("No implementation available for interaction"))
|
||||
}
|
||||
}
|
||||
|
||||
/** Helper function for implementations of the InteractionManager, this is the core logic for the binding of
|
||||
* InteractionInstances.
|
||||
*/
|
||||
protected def isCompatibleImplementation(interaction: InteractionTransition, implementation: InteractionInstance): Boolean = {
|
||||
val interactionNameMatches =
|
||||
interaction.originalInteractionName == implementation.name
|
||||
val inputSizeMatches =
|
||||
implementation.input.size == interaction.requiredIngredients.size
|
||||
val inputNamesAndTypesMatches =
|
||||
interaction
|
||||
.requiredIngredients
|
||||
.forall { descriptor =>
|
||||
implementation.input.exists(_.isAssignableFrom(descriptor.`type`))
|
||||
}
|
||||
interactionNameMatches && inputSizeMatches && inputNamesAndTypesMatches
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,10 @@ package com.ing.baker.runtime.akka.internal
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
import com.ing.baker.il.petrinet.InteractionTransition
|
||||
import com.ing.baker.runtime.scaladsl.{EventInstance, IngredientInstance, InteractionInstance}
|
||||
import com.ing.baker.runtime.scaladsl.InteractionInstance
|
||||
|
||||
import scala.concurrent.Future
|
||||
import scala.compat.java8.FunctionConverters._
|
||||
import scala.concurrent.Future
|
||||
|
||||
/**
|
||||
* The InteractionManager is responsible for all implementation of interactions.
|
||||
@@ -19,51 +19,12 @@ class InteractionManagerLocal(private var interactionImplementations: Seq[Intera
|
||||
private val implementationCache: ConcurrentHashMap[InteractionTransition, InteractionInstance] =
|
||||
new ConcurrentHashMap[InteractionTransition, InteractionInstance]
|
||||
|
||||
private def isCompatibleImplementation(interaction: InteractionTransition, implementation: InteractionInstance): Boolean = {
|
||||
val interactionNameMatches =
|
||||
interaction.originalInteractionName == implementation.name
|
||||
val inputSizeMatches =
|
||||
implementation.input.size == interaction.requiredIngredients.size
|
||||
val inputNamesAndTypesMatches =
|
||||
interaction
|
||||
.requiredIngredients
|
||||
.forall { descriptor =>
|
||||
implementation.input.exists(_.isAssignableFrom(descriptor.`type`))
|
||||
}
|
||||
interactionNameMatches && inputSizeMatches && inputNamesAndTypesMatches
|
||||
}
|
||||
|
||||
private def findInteractionImplementation(interaction: InteractionTransition): InteractionInstance =
|
||||
interactionImplementations.find(implementation => isCompatibleImplementation(interaction, implementation)).orNull
|
||||
|
||||
/**
|
||||
* Add an implementation to the InteractionManager
|
||||
*
|
||||
* @param implementation
|
||||
*/
|
||||
def addImplementation(implementation: InteractionInstance): Future[Unit] =
|
||||
Future.successful(interactionImplementations :+= implementation)
|
||||
|
||||
/**
|
||||
* Gets an implementation is available for the given interaction.
|
||||
* It checks:
|
||||
* 1. Name
|
||||
* 2. Input variable sizes
|
||||
* 3. Input variable types
|
||||
*
|
||||
* @param interaction The interaction to check
|
||||
* @return An option containing the implementation if available
|
||||
*/
|
||||
private[internal] def getImplementation(interaction: InteractionTransition): Option[InteractionInstance] =
|
||||
Option(implementationCache.computeIfAbsent(interaction, (findInteractionImplementation _).asJava))
|
||||
|
||||
def hasImplementation(interaction: InteractionTransition): Future[Boolean] =
|
||||
Future.successful(getImplementation(interaction).isDefined)
|
||||
|
||||
override def executeImplementation(interaction: InteractionTransition, input: Seq[IngredientInstance]): Future[Option[EventInstance]] = {
|
||||
this.getImplementation(interaction) match {
|
||||
case Some(implementation) => implementation.run(input)
|
||||
case None => Future.failed(new FatalInteractionException("No implementation available for interaction"))
|
||||
}
|
||||
override def getImplementation(interaction: InteractionTransition): Future[Option[InteractionInstance]] = {
|
||||
def findInteractionImplementation(interaction: InteractionTransition): InteractionInstance =
|
||||
interactionImplementations.find(implementation => isCompatibleImplementation(interaction, implementation)).orNull
|
||||
Future.successful(Option(implementationCache.computeIfAbsent(interaction, (findInteractionImplementation _).asJava)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,10 +6,10 @@ import com.ing.baker.runtime.scaladsl.InteractionInstance
|
||||
import com.ing.baker.types
|
||||
import com.ing.baker.types.Type
|
||||
import org.mockito.Mockito.when
|
||||
import org.scalatest.{Matchers, WordSpecLike}
|
||||
import org.scalatest.{AsyncWordSpecLike, Matchers}
|
||||
import org.scalatestplus.mockito.MockitoSugar
|
||||
|
||||
class InteractionManagerLocalSpec extends WordSpecLike with Matchers with MockitoSugar {
|
||||
class InteractionManagerLocalSpec extends AsyncWordSpecLike with Matchers with MockitoSugar {
|
||||
"getImplementation" should {
|
||||
"return Some" when {
|
||||
"an interaction implementation is available" in {
|
||||
@@ -23,7 +23,7 @@ class InteractionManagerLocalSpec extends WordSpecLike with Matchers with Mockit
|
||||
val ingredientDescriptor: IngredientDescriptor = IngredientDescriptor("ingredientName", types.Int32)
|
||||
when(interactionTransition.requiredIngredients).thenReturn(Seq(ingredientDescriptor))
|
||||
|
||||
interactionManager.getImplementation(interactionTransition) should equal(Some(interactionImplementation))
|
||||
interactionManager.getImplementation(interactionTransition).map(_ should equal(Some(interactionImplementation)))
|
||||
}
|
||||
|
||||
"multiple interaction implementations are available" in {
|
||||
@@ -41,7 +41,7 @@ class InteractionManagerLocalSpec extends WordSpecLike with Matchers with Mockit
|
||||
val ingredientDescriptor: IngredientDescriptor = IngredientDescriptor("ingredientName", types.Int32)
|
||||
when(interactionTransition.requiredIngredients).thenReturn(Seq(ingredientDescriptor))
|
||||
|
||||
interactionManager.getImplementation(interactionTransition) should equal(Some(interactionImplementation1))
|
||||
interactionManager.getImplementation(interactionTransition).map(_ should equal(Some(interactionImplementation1)))
|
||||
}
|
||||
|
||||
"two implementations with the same correct name but only one has the correct input types" in {
|
||||
@@ -59,7 +59,7 @@ class InteractionManagerLocalSpec extends WordSpecLike with Matchers with Mockit
|
||||
val ingredientDescriptor: IngredientDescriptor = IngredientDescriptor("ingredientName", types.Int32)
|
||||
when(interactionTransition.requiredIngredients).thenReturn(Seq(ingredientDescriptor))
|
||||
|
||||
interactionManager.getImplementation(interactionTransition) should equal(Some(interactionImplementation2))
|
||||
interactionManager.getImplementation(interactionTransition).map(_ should equal(Some(interactionImplementation2)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ class InteractionManagerLocalSpec extends WordSpecLike with Matchers with Mockit
|
||||
val ingredientDescriptor: IngredientDescriptor = IngredientDescriptor("ingredientName", types.Int32)
|
||||
when(interactionTransition.requiredIngredients).thenReturn(Seq(ingredientDescriptor))
|
||||
|
||||
interactionManager.getImplementation(interactionTransition) should equal(None)
|
||||
interactionManager.getImplementation(interactionTransition).map(_ should equal(None))
|
||||
}
|
||||
|
||||
"an interaction implementation has a wrong ingredient input type" in {
|
||||
@@ -89,7 +89,7 @@ class InteractionManagerLocalSpec extends WordSpecLike with Matchers with Mockit
|
||||
val ingredientDescriptor: IngredientDescriptor = IngredientDescriptor("ingredientName", types.CharArray)
|
||||
when(interactionTransition.requiredIngredients).thenReturn(Seq(ingredientDescriptor))
|
||||
|
||||
interactionManager.getImplementation(interactionTransition) should equal(None)
|
||||
interactionManager.getImplementation(interactionTransition).map(_ should equal(None))
|
||||
}
|
||||
|
||||
"an interaction implementation has extra ingredient input types" in {
|
||||
@@ -103,7 +103,7 @@ class InteractionManagerLocalSpec extends WordSpecLike with Matchers with Mockit
|
||||
val ingredientDescriptor: IngredientDescriptor = IngredientDescriptor("ingredientName", types.Int32)
|
||||
when(interactionTransition.requiredIngredients).thenReturn(Seq(ingredientDescriptor))
|
||||
|
||||
interactionManager.getImplementation(interactionTransition) should equal(None)
|
||||
interactionManager.getImplementation(interactionTransition).map(_ should equal(None))
|
||||
}
|
||||
|
||||
"an interaction implementation has not enough ingredient input types" in {
|
||||
@@ -118,14 +118,14 @@ class InteractionManagerLocalSpec extends WordSpecLike with Matchers with Mockit
|
||||
val ingredientDescriptor2: IngredientDescriptor = IngredientDescriptor("ingredientName2", types.CharArray)
|
||||
when(interactionTransition.requiredIngredients).thenReturn(Seq(ingredientDescriptor, ingredientDescriptor2))
|
||||
|
||||
interactionManager.getImplementation(interactionTransition) should equal(None)
|
||||
interactionManager.getImplementation(interactionTransition).map(_ should equal(None))
|
||||
}
|
||||
|
||||
"empty interaction seq" in {
|
||||
val interactionManager: InteractionManagerLocal = new InteractionManagerLocal(Seq.empty)
|
||||
|
||||
val interactionTransition: InteractionTransition = mock[InteractionTransition]
|
||||
interactionManager.getImplementation(interactionTransition) should equal(None)
|
||||
interactionManager.getImplementation(interactionTransition).map(_ should equal(None))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user