Merge branch 'master' into fix_buildExampleDocker

This commit is contained in:
Francisco Arámburo
2020-03-03 14:26:16 +01:00
committed by GitHub
58 changed files with 1937 additions and 1681 deletions

View File

@@ -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)
}
})
}

View File

@@ -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
})
}

View File

@@ -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()
}
}

View 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>

View File

@@ -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)
}
}

View File

@@ -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 = ()
}

View File

@@ -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))
}

View File

@@ -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())
}

View File

@@ -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()
}

View File

@@ -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)
}
})
}

View File

@@ -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

View File

@@ -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
})
}

View File

@@ -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()
}
}

View 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>

View File

@@ -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)
}
}

View File

@@ -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 = ()
}

View File

@@ -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))
}

View File

@@ -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)
}
})
}

View File

@@ -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
})
}

View File

@@ -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()
}
}

View 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>

View File

@@ -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)
}
}

View File

@@ -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)))
}
}
}
}

View File

@@ -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))
}

View File

@@ -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))
})
}
}

View File

@@ -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"))
}

View File

@@ -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)
}
}

View File

@@ -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()
}
}

View File

@@ -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"))
}
}

View File

@@ -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))
}
})
}

View File

@@ -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(_)(_ => ""))
})
}

View File

@@ -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>

View File

@@ -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")

View File

@@ -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 ()
}

View File

@@ -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))
}
}

View File

@@ -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")
}

View File

@@ -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)
}
}

View File

@@ -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 = ()
}

View File

@@ -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))
}

View File

@@ -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 ()
}

View File

@@ -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")
}
}
}

View File

@@ -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)
}
}

View File

@@ -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")
}
}
}

View File

@@ -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
}
}

View File

@@ -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
}

View File

@@ -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")
}
}
}

View File

@@ -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}"))
}
}
}

View File

@@ -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 ()
}

View File

@@ -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")
}
}
}

View File

@@ -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)
}
}

View File

@@ -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`)

View File

@@ -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)
}
}

View File

@@ -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 {

View File

@@ -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"

View File

@@ -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"

View File

@@ -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
}
}

View File

@@ -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)))
}
}

View File

@@ -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))
}
}
}