I'm running Akka Streams Typed 2.6.9 with Akka HTTP 10.1.12 in a service running in AWS ECS and I'm getting the error message "Exceeded configured max-open-requests value of 32" in my streaming application. I've looked for answers and must have read this about a dozen times now.
The application streams from an AWS SQS queue and uses this information to query a REST service.
The application is streaming "all the way through." According to the documentation here, the Http().superPool()
implementation should backpressure. If so, why am I filling up max-open-requests? Shouldn't it backpressure the connection to the queue and not overload the pool?
I've tried throttling the requests, changing all mapAsync()
calls to one thread, and even throttling from the queue, but the pool eventually fills up, however slowly. What am I doing wrong?
Code is below...
object ExampleActor {
object Start extends Protocol
private val logger = LoggerFactory.getLogger(getClass)
def buildAwsSqsClient(actorSystem: ActorSystem[Nothing]): SqsAsyncClient = {
val awsCredentialsProvider = DefaultCredentialsProvider.create()
val awsSqsClientBuilder = SqsAsyncClient
.builder()
.credentialsProvider(awsCredentialsProvider)
.region(Region.US_EAST_1)
.httpClient(AkkaHttpClient.builder().withActorSystem(actorSystem.classicSystem).build())
// Possibility to configure the retry policy
// see https://doc.akka.io/docs/alpakka/current/aws-shared-configuration.html
// .overrideConfiguration(...)
if(System.getProperty("localstack", "false").equals("true")) {
println("****Enabling localstack resource")
awsSqsClientBuilder.endpointOverride(new URI("http://localhost:4566"))
}
implicit val awsSqsClient = awsSqsClientBuilder.build()
actorSystem.classicSystem.registerOnTermination(awsSqsClient.close())
awsSqsClient
}
def apply(): Behavior[Protocol] =
Behaviors.setup { ctx: ActorContext[Protocol] =>
def process(): Behavior[Protocol] =
Behaviors.receiveMessage {
case Start =>
println("Start up")
Behaviors.same[Protocol]
}
process()
}
def setupStream(context: ActorContext[Protocol])(implicit sqsClient: SqsAsyncClient, ex: ExecutionContext) = {
val deleteSink = Flow[Message]
.map(MessageAction.delete)
.log("Mapping to delete action for getting rid of message from queue")
.to(SqsAckSink("libraryPublish", SqsAckSettings.create()))
val fromWebService = SqsSource(
"libraryPublish",
SqsSourceSettings.create()
.withWaitTime(500.milliseconds)
)
.alsoTo(deleteSink)
.map(_.body())
val endingSink = Flow[BookCommand]
.map(group => SendMessageRequest.builder().messageBody(group.toJson.toString).build())
.log("Building message to SQS")
.to(SqsPublishSink.messageSink("libraryPublishPost"))
Flow[String]
.map(runGroupId => retrieveLibrary(runGroupId))
.via(queryBooks(context))
}
def queryBooks(context: ActorContext[Protocol]) = {
Flow[Library]
.map(library => {
LibraryQuery(
library.id,
library.books.map(book =>
AuthorIndex(
book.author.lastName,
library.tags match {
case Subject(tagName) =>
DeweySearch.createSearch(tagName)
case Character(tagName) =>
CharacterSearch(tagName)
case Publisher(tagName) =>
PublisherSearch("publisher:" + tagName)
}
)
)
)
})
.mapAsync(4)(libraryQuery => {
val seq = Source.single(libraryQuery)
.mapConcat(_.bookQueries)
.via(runBookQueries(context))
.runWith(Sink.seq)
LibraryAnswer(libraryQuery.id, seq)
}
)
.map(libraryAnswer => insertIntoDb(libraryAnswer))
.runWith(Sink.last)
}
def runBookQueries(context: ActorContext[Protocol]) = {
implicit val ec = context.executionContext
implicit val sys = context.system.classicSystem
val httpFlow: Flow[(HttpRequest, BookQuery), (Try[HttpResponse], BookQuery), NotUsed] =
Http().superPool[BookQuery]()
val retryFlow: Flow[(HttpRequest, BookQuery), (Try[HttpResponse], BookQuery), NotUsed] = {
RetryFlow.withBackoff(minBackoff = 10.millis, maxBackoff = 5.seconds, randomFactor = 0d, maxRetries = 3, httpFlow)(
decideRetry = { (request, response) =>
if(response._1.isFailure || response._1.get.status.isFailure) {
logger.debug("Retrying answer query from {} response", response._1.get.status)
response._1.get.discardEntityBytes()
Some(request)
} else {
logger.debug("Request went through...")
None
}
}
)
}
Flow[BookQuery]
.mapAsync(4)((query: BookQuery) => {
//Cached token getter is a function that gets an API token for the book service
CachedTokenGetter.getToken(context).map(token =>
(
Post("https://my.example.service")
.withEntity(HttpEntity(ContentTypes.`application/json`, query.toJson.toString))
.withHeaders(Authorization(OAuth2BearerToken(token.access_token))),
query
))
})
.via(retryFlow)
.flatMapConcat(pair => {
getBookResponseFromPair(pair).map(answer => (pair._2, answer))
})
.map(pair => BookQueryAndAnswer(pair._1, pair._2))
}
def getBookResponseFromPair(pair: (Try[HttpResponse], BookQuery)): Source[BookAnswer, Any] = {
pair._1.get.entity.dataBytes
.via(JsonFraming.objectScanner(Int.MaxValue))
.mapAsync(10)(bytes => Unmarshal(bytes).to[BookQueryResponse])
.mapConcat(_.titles)
}
}