My simplified Akka Camel application is set up as follows:
AppleProducer -> seda:appleRoute -> AppleConsumer
OrangeProducer -> seda:orangeRoute -> OrangeConsumer
What I am seeing though is that Apple events are intermittently being consumed by the OrangeConsumer, and vice versa.
Running this example (perhaps a few times) below recreates it.
I don't understand how this only happens intermittently. What am I doing wrong?
object TestApp extends App {
implicit val system = ActorSystem()
val camel = CamelExtension(system)
val appleProducer = system.actorOf(Props(classOf[MyProducer], "seda:appleRoute"), "AppleProducer")
system.actorOf(Props(classOf[MyAppleConsumer], "seda:appleRoute"), "AppleConsumer")
val orangeProducer = system.actorOf(Props(classOf[MyProducer], "seda:orangeRoute"), "OrangeProducer")
system.actorOf(Props(classOf[MyOrangeConsumer], "seda:orangeRoute"), "OrangeConsumer")
appleProducer ! new Apple("1")
orangeProducer ! new Orange("1")
appleProducer ! new Apple("2")
orangeProducer ! new Orange("2")
appleProducer ! new Apple("3")
orangeProducer ! new Orange("3")
appleProducer ! new Apple("4")
orangeProducer ! new Orange("4")
appleProducer ! new Apple("5")
orangeProducer ! new Orange("5")
appleProducer ! new Apple("6")
orangeProducer ! new Orange("6")
}
class MyProducer(route: String) extends Actor with ActorLogging {
def receive = {
case payload: Any =>
val template = CamelExtension(context.system).template
template.setDefaultEndpointUri(route)
template.sendBody(payload)
}
}
class MyAppleConsumer(route: String) extends Consumer with ActorLogging {
override def endpointUri: String = route
override def receive: Receive = {
case event: CamelMessage if event.body.isInstanceOf[Apple] =>
log.info("Received event {}", event.body)
case _ => throw new IllegalArgumentException("Invalid entity")
}
}
class MyOrangeConsumer(route: String) extends Consumer with ActorLogging {
override def endpointUri: String = route
override def receive: Receive = {
case event: CamelMessage if event.body.isInstanceOf[Orange] =>
log.info("Received event {}", event.body)
case _ => throw new IllegalArgumentException("Invalid entity")
}
}
class Apple(id: String)
class Orange(id: String)
I think I managed to figure this out eventually.
The issue has nothing to do with SEDA. Instead it seems that the same DefaultProducerTemplate is returned for multiple MyProducer instances.
Therefore, there is occasionally a race condition when setting the defaultEndpointUri
Solution, for me, was to create only one instance of the MyProducer actor to ensure that we do not encounter this race condition
I would recommend extending the trait Producer instead of using a template for your MyProducer the same way you are using Consumer for your MyAppleConsumer and MyOrangeConsumer.
class MyProducer(route: String) extends Producer with OneWay {
def endpointUri = route
}
More info can be found here: http://doc.akka.io/docs/akka/snapshot/scala/camel.html
I believe you should be able to simplify your code like this (disclaimer: not compiled or tested!):
case class Apple(id: String)
case class Orange(id: String)
object TestApp extends App {
implicit val system = ActorSystem()
val appleProducer = system.actorOf(Props(classOf[MyProducer], "seda:appleRoute"), "AppleProducer")
system.actorOf(Props(classOf[MyConsumer], "seda:appleRoute"), "AppleConsumer")
val orangeProducer = system.actorOf(Props(classOf[MyProducer], "seda:orangeRoute"), "OrangeProducer")
system.actorOf(Props(classOf[MyConsumer], "seda:orangeRoute"), "OrangeConsumer")
appleProducer ! Apple("1")
orangeProducer ! Orange("1")
appleProducer ! Apple("2")
orangeProducer ! Orange("2")
appleProducer ! Apple("3")
orangeProducer ! Orange("3")
appleProducer ! Apple("4")
orangeProducer ! Orange("4")
appleProducer ! Apple("5")
orangeProducer ! Orange("5")
appleProducer ! Apple("6")
orangeProducer ! Orange("6")
}
class MyProducer(route: String) extends Producer with OneWay with ActorLogging {
def endpointUri = route
}
class MyConsumer(route: String) extends Consumer with ActorLogging {
override def endpointUri: String = route
override def receive: Receive = {
case CamelMessage(body : Apple, headers) =>
log.info("Received event {}", body)
case CamelMessage(body : Orange, headers) =>
log.info("Received event {}", body)
case _ => throw new IllegalArgumentException("Invalid entity")
}
}
Related
I have created a RetryFlow like below,following this article https://doc.akka.io/docs/akka/current/stream/operators/RetryFlow/withBackoff.html
private val httpFlow: Flow[HttpRequest, HttpResponse, Any] =
Http().outgoingConnection(host, port)
val retryFlow: Flow[HttpRequest, HttpResponse, Any] =
RetryFlow.withBackoff(minBackoff = 10.millis, maxBackoff = 5.seconds, randomFactor = 0d, maxRetries = 20, httpFlow)(
decideRetry = {
case (request: HttpRequest, response) =>
response.status match {
case StatusCodes.OK => Some(request)
case _ => None
}
})
And then processing flow like
val request = Post(Uri(path), some_json_payload)(
stringMarshaller(MediaTypes.`application/json`),
system.executionContext
)
httpClient.processRequest(request, retryFlow) map (...)
My http client looks like below, added a RestartSettings and restarting source on onFailuresWithBackoff
class HttpClient(implicit val system: ActorSystem[_]) {
implicit val materializer: Materializer = Materializer(system)
private def checkSuccess(response: HttpResponse, path: String): Either[Throwable, HttpResponse] =
Either.cond(
response.status == StatusCodes.OK,
response,
new Throwable(s"Could not GET $path, Service returned ${response.status.toString()}"),
)
private def httpRequest(
request: HttpRequest,
flow: Flow[HttpRequest, HttpResponse, Any],
): Future[Either[Throwable, HttpResponse]] =
Source.single(request).via(flow)
.runWith(Sink.head) map { response =>
checkSuccess(response, request.uri.path.toString())
}
private def unmarshallString(response: Either[Throwable, HttpResponse]): Future[Either[Throwable, String]] =
response.fold(
err => Future(err.asLeft[String]),
r => Unmarshal(r.entity).to[String].map(_.asRight[Throwable]),
)
def processRequest(request: HttpRequest, flow: Flow[HttpRequest, HttpResponse, Any]): Future[Enclosed[Json]] =
httpRequest(request, flow) flatMap unmarshallString map (_.flatMap(parse))
}
I am pretty new especially in akka , what I am missing here .
I am getting JsResultException(errors:List((/additional-info,List(JsonValidationError(List(error.path.missing),WrappedArray()))) error though the json structure seems OK to me. What mistake am I making?
I am writing unit tests. The test case is that server should send error message if the client sends message without json.
The server side code which sends the error is Future { Ok(Json.toJson(JsonResultError(messagesApi("error.incorrectBodyType")(langs.availables(0))))) }
The test case is
"User signup request with no body" should {
"return 400 (Bad Request) and the validation text must be 'Incorrect body type. Body type must be JSON'" in {
println("testing with mocked User value",user);
val request = FakeRequest("POST","ws/users/signup")
println("sending request",request)
val result:Future[Result] = controller.signupUser(request)
val bodyAsString = contentAsString(result)
println("body as String: "+bodyAsString)
val bodyAsJson = Json.toJson(bodyAsString)
println("body as json:"+bodyAsJson)
val jsonResultError:JsonResult = bodyAsJson.as[JsonResult]
println("jsonResultError: "+jsonResultError)
(status(result) mustBe OK )
(jsonResultError.message mustBe ("some Error "))
}
}
}
The print messages on console are
body as String: {"result":"error","additional-info":"error.incorrectBodyType"}
body as json:"{\"result\":\"error\",\"additional-info\":\"error.incorrectBodyType\"}"
JsResultException(errors:List((/additional-info,List(JsonValidationError(List(error.path.missing),WrappedArray()))), (/result,List(JsonValidationError(List(error.path.missing),WrappedArray())))))
play.api.libs.json.JsResultException: JsResultException(errors:List((/additional-info,List(JsonValidationError(List(error.path.missing),WrappedArray()))), (/result,List(JsonValidationError(List(error.path.missing),WrappedArray())))))
The implicit conversions between the json and my model is
sealed abstract class JsonResult (val result:String, val message:String)
case class JsonResultError(override val message:String) extends JsonResult(result="error", message)
case class JsonResultSuccess(override val message:String) extends JsonResult(result="success",message)
object JsonResult {
def apply(result:String,additionalInfo:String) = {
if(result== "error") JsonResultError(additionalInfo) else JsonResultSuccess(additionalInfo)
}
def unapply(jsonResult:JsonResult):Option[(String,String)] = {
if(jsonResult == null) None else Some (jsonResult.result,jsonResult.message)
}
}
object JSONMessagesImplicits {
implicit val JsonResultWrites:Writes[JsonResult] = {
((JsPath \ "result").write[String] and
(JsPath \ "additional-info").write[String])(unlift(JsonResult.unapply)) //I suppose `unlift` would convert Option[T] return value of unapply method of JsonResult Extractor object to T.
}
//read from jsvalue i.e. create model from jsvalue
implicit val JsonResultReads:Reads[JsonResult] = {
((JsPath \ "result").read[String] and
(JsPath \ "additional-info").read[String])(JsonResult.apply _)
}
}
It worked when I changed val bodyAsJson = Json.toJson(bodyAsString) to val bodyAsJson = Json.parse(bodyAsString)
I am experimenting with how to propagate back-pressure correctly when I have ConnectedStreams as part of my computation graph. The problem is: I have two sources and one ingests data faster than the other, think we want to replay some data and one source has rare events that we use to enrich the other source. These two sources are then connected in a stream that expects them to be at least somewhat synchronized, merges them together somehow (making tuple, enriching, ...) and returns a result.
With single input streams its fairly easy to implement backpressure, you simply have to spend long time in the processElement function. With connectedstreams my initial idea was to have some logic in each of the processFunctions that waits for the other stream to catch up. For example I could have a buffer thats time-span limited (large enough span to fit a watermark) and the function would not accept events that would make this span pass a threshold. For example:
leftLock.aquire { nonEmptySignal =>
while (queueSpan() > capacity.toMillis && lastTs() < ctx.timestamp()) {
println("WAITING")
nonEmptySignal.await()
}
queueOp { queue =>
println(s"Left Event $value recieved ${Thread.currentThread()}")
queue.add(Left(value))
}
ctx.timerService().registerEventTimeTimer(value.ts)
}
Full code of my example is below (its written with two locks assuming access from two different threads, which is not the case - i think):
import java.util.concurrent.atomic.{AtomicBoolean, AtomicLong}
import java.util.concurrent.locks.{Condition, ReentrantLock}
import scala.collection.JavaConverters._
import com.google.common.collect.MinMaxPriorityQueue
import org.apache.flink.api.common.state.{ValueState, ValueStateDescriptor}
import org.apache.flink.api.common.typeinfo.{TypeHint, TypeInformation}
import org.apache.flink.api.java.utils.ParameterTool
import org.apache.flink.api.scala._
import org.apache.flink.configuration.Configuration
import org.apache.flink.streaming.api.TimeCharacteristic
import org.apache.flink.streaming.api.environment.LocalStreamEnvironment
import org.apache.flink.streaming.api.functions.co.CoProcessFunction
import org.apache.flink.streaming.api.functions.source.{RichSourceFunction, SourceFunction}
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.streaming.api.watermark.Watermark
import org.apache.flink.util.Collector
import scala.collection.mutable
import scala.concurrent.duration._
trait Timestamped {
val ts: Long
}
case class StateObject(ts: Long, state: String) extends Timestamped
case class DataObject(ts: Long, data: String) extends Timestamped
case class StatefulDataObject(ts: Long, state: Option[String], data: String) extends Timestamped
class DataSource[A](factory: Long => A, rate: Int, speedUpFactor: Long = 0) extends RichSourceFunction[A] {
private val max = new AtomicLong()
private val isRunning = new AtomicBoolean(false)
private val speedUp = new AtomicLong(0)
private val WatermarkDelay = 5 seconds
override def cancel(): Unit = {
isRunning.set(false)
}
override def run(ctx: SourceFunction.SourceContext[A]): Unit = {
isRunning.set(true)
while (isRunning.get()) {
val time = System.currentTimeMillis() + speedUp.addAndGet(speedUpFactor)
val event = factory(time)
ctx.collectWithTimestamp(event, time)
println(s"Event $event sourced $speedUpFactor")
val watermark = time - WatermarkDelay.toMillis
if (max.get() < watermark) {
ctx.emitWatermark(new Watermark(time - WatermarkDelay.toMillis))
max.set(watermark)
}
Thread.sleep(rate)
}
}
}
class ConditionalOperator {
private val lock = new ReentrantLock()
private val signal: Condition = lock.newCondition()
def aquire[B](func: Condition => B): B = {
lock.lock()
try {
func(signal)
} finally {
lock.unlock()
}
}
}
class BlockingCoProcessFunction(capacity: FiniteDuration = 20 seconds)
extends CoProcessFunction[StateObject, DataObject, StatefulDataObject] {
private type MergedType = Either[StateObject, DataObject]
private lazy val leftLock = new ConditionalOperator()
private lazy val rightLock = new ConditionalOperator()
private var queueState: ValueState[MinMaxPriorityQueue[MergedType]] = _
private var dataState: ValueState[StateObject] = _
override def open(parameters: Configuration): Unit = {
super.open(parameters)
queueState = getRuntimeContext.getState(new ValueStateDescriptor[MinMaxPriorityQueue[MergedType]](
"event-queue",
TypeInformation.of(new TypeHint[MinMaxPriorityQueue[MergedType]]() {})
))
dataState = getRuntimeContext.getState(new ValueStateDescriptor[StateObject](
"event-state",
TypeInformation.of(new TypeHint[StateObject]() {})
))
}
override def processElement1(value: StateObject,
ctx: CoProcessFunction[StateObject, DataObject, StatefulDataObject]#Context,
out: Collector[StatefulDataObject]): Unit = {
leftLock.aquire { nonEmptySignal =>
while (queueSpan() > capacity.toMillis && lastTs() < ctx.timestamp()) {
println("WAITING")
nonEmptySignal.await()
}
queueOp { queue =>
println(s"Left Event $value recieved ${Thread.currentThread()}")
queue.add(Left(value))
}
ctx.timerService().registerEventTimeTimer(value.ts)
}
}
override def processElement2(value: DataObject,
ctx: CoProcessFunction[StateObject, DataObject, StatefulDataObject]#Context,
out: Collector[StatefulDataObject]): Unit = {
rightLock.aquire { nonEmptySignal =>
while (queueSpan() > capacity.toMillis && lastTs() < ctx.timestamp()) {
println("WAITING")
nonEmptySignal.await()
}
queueOp { queue =>
println(s"Right Event $value recieved ${Thread.currentThread()}")
queue.add(Right(value))
}
ctx.timerService().registerEventTimeTimer(value.ts)
}
}
override def onTimer(timestamp: Long,
ctx: CoProcessFunction[StateObject, DataObject, StatefulDataObject]#OnTimerContext,
out: Collector[StatefulDataObject]): Unit = {
println(s"Watermarked $timestamp")
leftLock.aquire { leftSignal =>
rightLock.aquire { rightSignal =>
queueOp { queue =>
while (Option(queue.peekFirst()).exists(x => timestampOf(x) <= timestamp)) {
queue.poll() match {
case Left(state) =>
dataState.update(state)
leftSignal.signal()
case Right(event) =>
println(s"Event $event emitted ${Thread.currentThread()}")
out.collect(
StatefulDataObject(
event.ts,
Option(dataState.value()).map(_.state),
event.data
)
)
rightSignal.signal()
}
}
}
}
}
}
private def queueOp[B](func: MinMaxPriorityQueue[MergedType] => B): B = queueState.synchronized {
val queue = Option(queueState.value()).
getOrElse(
MinMaxPriorityQueue.
orderedBy(Ordering.by((x: MergedType) => timestampOf(x))).create[MergedType]()
)
val result = func(queue)
queueState.update(queue)
result
}
private def timestampOf(data: MergedType): Long = data match {
case Left(y) =>
y.ts
case Right(y) =>
y.ts
}
private def queueSpan(): Long = {
queueOp { queue =>
val firstTs = Option(queue.peekFirst()).map(timestampOf).getOrElse(Long.MaxValue)
val lastTs = Option(queue.peekLast()).map(timestampOf).getOrElse(Long.MinValue)
println(s"Span: $firstTs - $lastTs = ${lastTs - firstTs}")
lastTs - firstTs
}
}
private def lastTs(): Long = {
queueOp { queue =>
Option(queue.peekLast()).map(timestampOf).getOrElse(Long.MinValue)
}
}
}
object BackpressureTest {
var data = new mutable.ArrayBuffer[DataObject]()
def main(args: Array[String]): Unit = {
val streamConfig = new Configuration()
val env = new StreamExecutionEnvironment(new LocalStreamEnvironment(streamConfig))
env.getConfig.disableSysoutLogging()
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
env.setParallelism(1)
val stateSource = env.addSource(new DataSource(ts => StateObject(ts, ts.toString), 1000))
val dataSource = env.addSource(new DataSource(ts => DataObject(ts, ts.toString), 100, 100))
stateSource.
connect(dataSource).
keyBy(_ => "", _ => "").
process(new BlockingCoProcessFunction()).
print()
env.execute()
}
}
The problem with connected streams is it seems you cant simply block in one of the processFunctions when its stream is too far ahead, since that blocks the other processFunction aswell. On the other hand if i simply accepted all events in this job eventually the process function would run out of memory. Since it would buffer the whole stream that is ahead.
So my question is: Is it possible to propagate backpressure into each of the streams in ConnectedStreams separately and if so, how? Or alternatively, is there any other nice way to deal with this issue? Possibly all the sources communicating somehow to keep them mostly at the same event-time?
From my reading of the code in StreamTwoInputProcessor, it looks to me like the processInput() method is responsible for implementing the policy in question. Perhaps one could implement a variant that reads from whichever stream has the lower watermark, so long as it has unread input. Not sure what impact that would have overall, however.
I have one controller =>
#Singleton
class NewBillingMonthController #Inject() (implicit cacheService:
CacheService, repository: NewBillingRoleRepository, contexts: Contexts,
securityService: SecurityService) extends BaseController {
def getAll(implicit queryParams: QueryParams): Action[AnyContent] =
securityService.authenticate() { implicit request =>
withErrorRecovery { req =>
toJson {
repository.getAll(request.user.loginName)
repository.getUser()
}
// This controller calls the respository were i have a sql defined
This show the repository=> newBillingRepository.scala=>
class NewBillingRoleRepository #Inject() (val queryHandler:
QueryHandler, val contexts: Contexts, val configurationService:
ConfigurationService)
extends Repository[HierarchyEntryBillingRoleCheck] {
override val allQuery = s"""
select users from tble """
override def map2Object(implicit map: Map[String, Any]):
HierarchyEntryBillingRoleCheck = {
HierarchyEntryBillingRoleCheck(str("roleName"), oint("PersonID"))}
def getAll(implicit loginName: String):
Future[Seq[HierarchyEntryBillingRoleCheck]] = {
doQueryIgnoreRowErrors(allQuery, "loginName" -> loginName) }
another repo:
class UserNewBillingRepository #Inject() (val queryHandler: QueryHandler,
val contexts: Contexts, val configurationService: ConfigurationService)
extends Repository[HierarchyEntryBilling] {
override val allQuery= s"""select * from SiACPManageBillingMonth"""
def getrepo():Future[Seq[HierarchyEntryBilling]]= {
doQueryIgnoreRowErrors(allQuery)
}
override def map2Object(implicit map: Map[String, Any]):
HierarchyEntryBilling = {
HierarchyEntryBilling(str("Name"), str("Provider"), oint("Year"),
ostr("Month"), ostr("Status"), ostr("ProviderType"))
My issuse i want to know how to fetch the data from 1st sql and test the result for empty or not and execute the second query?
I use Source.queue to queue up HttpRequests and throttle it on the client side to download files from a remote server. I understand that Source.queue is not threadsafe and we need to use MergeHub to make it threadsafe. Following is the piece of code that uses Source.queue and uses cachedHostConnectionPool.
import java.io.File
import akka.actor.Actor
import akka.event.Logging
import akka.http.scaladsl.Http
import akka.http.scaladsl.client.RequestBuilding
import akka.http.scaladsl.model.{HttpResponse, HttpRequest, Uri}
import akka.stream._
import akka.stream.scaladsl._
import akka.util.ByteString
import com.typesafe.config.ConfigFactory
import scala.concurrent.{Promise, Future}
import scala.concurrent.duration._
import scala.util.{Failure, Success}
class HttpClient extends Actor with RequestBuilding {
implicit val system = context.system
val logger = Logging(system, this)
implicit lazy val materializer = ActorMaterializer()
val config = ConfigFactory.load()
val remoteHost = config.getString("pool.connection.host")
val remoteHostPort = config.getInt("pool.connection.port")
val queueSize = config.getInt("pool.queueSize")
val throttleSize = config.getInt("pool.throttle.numberOfRequests")
val throttleDuration = config.getInt("pool.throttle.duration")
import scala.concurrent.ExecutionContext.Implicits.global
val connectionPool = Http().cachedHostConnectionPool[Promise[HttpResponse]](host = remoteHost, port = remoteHostPort)
// Construct a Queue
val requestQueue =
Source.queue[(HttpRequest, Promise[HttpResponse])](queueSize, OverflowStrategy.backpressure)
.throttle(throttleSize, throttleDuration.seconds, 1, ThrottleMode.shaping)
.via(connectionPool)
.toMat(Sink.foreach({
case ((Success(resp), p)) => p.success(resp)
case ((Failure(error), p)) => p.failure(error)
}))(Keep.left)
.run()
// Convert Promise[HttpResponse] to Future[HttpResponse]
def queueRequest(request: HttpRequest): Future[HttpResponse] = {
val responsePromise = Promise[HttpResponse]()
requestQueue.offer(request -> responsePromise).flatMap {
case QueueOfferResult.Enqueued => responsePromise.future
case QueueOfferResult.Dropped => Future.failed(new RuntimeException("Queue overflowed. Try again later."))
case QueueOfferResult.Failure(ex) => Future.failed(ex)
case QueueOfferResult.QueueClosed => Future.failed(new RuntimeException("Queue was closed (pool shut down) while running the request. Try again later."))
}
}
def receive = {
case "download" =>
val uri = Uri(s"http://localhost:8080/file_csv.csv")
downloadFile(uri, new File("/tmp/compass_audience.csv"))
}
def downloadFile(uri: Uri, destinationFilePath: File) = {
def fileSink: Sink[ByteString, Future[IOResult]] =
Flow[ByteString].buffer(512, OverflowStrategy.backpressure)
.toMat(FileIO.toPath(destinationFilePath.toPath)) (Keep.right)
// Submit to queue and execute HttpRequest and write HttpResponse to file
Source.fromFuture(queueRequest(Get(uri)))
.flatMapConcat(_.entity.dataBytes)
.via(Framing.delimiter(ByteString("\n"), maximumFrameLength = 10000, allowTruncation = true))
.map(_.utf8String)
.map(d => s"$d\n")
.map(ByteString(_))
.runWith(fileSink)
}
}
However, when I use MergeHub, it returns Sink[(HttpRequest, Promise[HttpResponse]), NotUsed]. I need to extract the response.entity.dataBytes and write the response to a file using a filesink. I am not able figure out how to use MergeHub to achieve this. Any help will be appreciated.
val hub: Sink[(HttpRequest, Promise[HttpResponse]), NotUsed] =
MergeHub.source[(HttpRequest, Promise[HttpResponse])](perProducerBufferSize = queueSize)
.throttle(throttleSize, throttleDuration.seconds, 1, ThrottleMode.shaping)
.via(connectionPool)
.toMat(Sink.foreach({
case ((Success(resp), p)) => p.success(resp)
case ((Failure(error), p)) => p.failure(error)
}))(Keep.left)
.run()
Source.Queue is actually thread safe now. If you want to use MergeHub:
private lazy val poolFlow: Flow[(HttpRequest, Promise[HttpResponse]), (Try[HttpResponse], Promise[HttpResponse]), Http.HostConnectionPool] =
Http().cachedHostConnectionPool[Promise[HttpResponse]](host).tail.head, port, connectionPoolSettings)
val ServerSink =
poolFlow.toMat(Sink.foreach({
case ((Success(resp), p)) => p.success(resp)
case ((Failure(e), p)) => p.failure(e)
}))(Keep.left)
// Attach a MergeHub Source to the consumer. This will materialize to a
// corresponding Sink.
val runnableGraph: RunnableGraph[Sink[(HttpRequest, Promise[HttpResponse]), NotUsed]] =
MergeHub.source[(HttpRequest, Promise[HttpResponse])](perProducerBufferSize = 16).to(ServerSink)
val toConsumer: Sink[(HttpRequest, Promise[HttpResponse]), NotUsed] = runnableGraph.run()
protected[akkahttp] def executeRequest[T](httpRequest: HttpRequest, unmarshal: HttpResponse => Future[T]): Future[T] = {
val responsePromise = Promise[HttpResponse]()
Source.single((httpRequest -> responsePromise)).runWith(toConsumer)
responsePromise.future.flatMap(handleHttpResponse(_, unmarshal))
)
}
}