From 86be387aaba3353f2aeaba11ba1ae7a4e686ef41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Ferreira?= Date: Wed, 4 Oct 2023 11:30:15 +0100 Subject: [PATCH 1/7] allow protocol selection via AttributeMap and SdkHttpConfigurationOption.PROTOCOL --- .../matsluni/akkahttpspi/AkkaHttpClient.scala | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/main/scala/com/github/matsluni/akkahttpspi/AkkaHttpClient.scala b/src/main/scala/com/github/matsluni/akkahttpspi/AkkaHttpClient.scala index 8275892..b3c171f 100644 --- a/src/main/scala/com/github/matsluni/akkahttpspi/AkkaHttpClient.scala +++ b/src/main/scala/com/github/matsluni/akkahttpspi/AkkaHttpClient.scala @@ -32,7 +32,7 @@ import akka.stream.{ActorMaterializer, Materializer, SystemMaterializer} import akka.util.ByteString import org.slf4j.LoggerFactory import software.amazon.awssdk.http.async._ -import software.amazon.awssdk.http.SdkHttpRequest +import software.amazon.awssdk.http.{Protocol, SdkHttpConfigurationOption, SdkHttpRequest} import software.amazon.awssdk.utils.AttributeMap import scala.collection.immutable @@ -41,13 +41,13 @@ import scala.compat.java8.OptionConverters._ import scala.concurrent.duration.Duration import scala.concurrent.{Await, ExecutionContext} -class AkkaHttpClient(shutdownHandle: () => Unit, connectionSettings: ConnectionPoolSettings)(implicit actorSystem: ActorSystem, ec: ExecutionContext, mat: Materializer) extends SdkAsyncHttpClient { +class AkkaHttpClient(shutdownHandle: () => Unit, connectionSettings: ConnectionPoolSettings, protocol: HttpProtocol)(implicit actorSystem: ActorSystem, ec: ExecutionContext, mat: Materializer) extends SdkAsyncHttpClient { import AkkaHttpClient._ lazy val runner = new RequestRunner() override def execute(request: AsyncExecuteRequest): CompletableFuture[Void] = { - val akkaHttpRequest = toAkkaRequest(request.request(), request.requestContentPublisher()) + val akkaHttpRequest = toAkkaRequest(protocol, request.request(), request.requestContentPublisher()) runner.run( () => Http().singleRequest(akkaHttpRequest, settings = connectionSettings), request.responseHandler() @@ -65,7 +65,7 @@ object AkkaHttpClient { val logger = LoggerFactory.getLogger(this.getClass) - private[akkahttpspi] def toAkkaRequest(request: SdkHttpRequest, contentPublisher: SdkHttpContentPublisher): HttpRequest = { + private[akkahttpspi] def toAkkaRequest(protocol: HttpProtocol, request: SdkHttpRequest, contentPublisher: SdkHttpContentPublisher): HttpRequest = { val (contentTypeHeader, reqheaders) = convertHeaders(request.headers()) val method = convertMethod(request.method().name()) HttpRequest( @@ -73,7 +73,7 @@ object AkkaHttpClient { uri = Uri(request.getUri.toString), headers = reqheaders, entity = entityForMethodAndContentType(method, contentTypeHeaderToContentType(contentTypeHeader), contentPublisher), - protocol = HttpProtocols.`HTTP/1.1` + protocol = protocol ) } @@ -145,14 +145,17 @@ object AkkaHttpClient { implicit val ec = executionContext.getOrElse(as.dispatcher) val mat: Materializer = SystemMaterializer(as).materializer + val mergedAttributeMap = attributeMap.merge(SdkHttpConfigurationOption.GLOBAL_HTTP_DEFAULTS) + val cps = connectionPoolSettings.getOrElse(ConnectionPoolSettings(as)) + val protocol = convertProtocol(mergedAttributeMap.get(SdkHttpConfigurationOption.PROTOCOL)) val shutdownhandleF = () => { if (actorSystem.isEmpty) { Await.result(Http().shutdownAllConnectionPools().flatMap(_ => as.terminate()), Duration.apply(10, TimeUnit.SECONDS)) } () } - new AkkaHttpClient(shutdownhandleF, cps)(as, ec, mat) + new AkkaHttpClient(shutdownhandleF, cps, protocol)(as, ec, mat) } def withActorSystem(actorSystem: ActorSystem): AkkaHttpClientBuilder = copy(actorSystem = Some(actorSystem)) def withActorSystem(actorSystem: ClassicActorSystemProvider): AkkaHttpClientBuilder = copy(actorSystem = Some(actorSystem.classicSystem)) @@ -160,6 +163,13 @@ object AkkaHttpClient { def withConnectionPoolSettings(connectionPoolSettings: ConnectionPoolSettings): AkkaHttpClientBuilder = copy(connectionPoolSettings = Some(connectionPoolSettings)) } + private def convertProtocol(protocol: Protocol) = { + protocol match { + case Protocol.HTTP1_1 => HttpProtocols.`HTTP/1.1` + case Protocol.HTTP2 => HttpProtocols.`HTTP/2.0` + } + } + lazy val xAmzJson = ContentType(MediaType.customBinary("application", "x-amz-json-1.0", Compressible)) lazy val xAmzJson11 = ContentType(MediaType.customBinary("application", "x-amz-json-1.1", Compressible)) lazy val xAmzCbor11 = ContentType(MediaType.customBinary("application", "x-amz-cbor-1.1", Compressible)) From 1b3d454b5b2859242e9773eebeee4378a1d2c896 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Ferreira?= Date: Thu, 19 Oct 2023 12:01:48 +0100 Subject: [PATCH 2/7] add tests --- .../akkahttpspi/dynamodb/TestDynamoDB.scala | 13 +++++++++--- .../matsluni/akkahttpspi/s3/TestS3.scala | 21 +++++++++++++------ .../matsluni/akkahttpspi/sns/TestSNS.scala | 14 ++++++++++--- .../matsluni/akkahttpspi/sqs/TestSQS.scala | 17 +++++++++++---- 4 files changed, 49 insertions(+), 16 deletions(-) diff --git a/src/test/scala/com/github/matsluni/akkahttpspi/dynamodb/TestDynamoDB.scala b/src/test/scala/com/github/matsluni/akkahttpspi/dynamodb/TestDynamoDB.scala index 93d3324..0974290 100644 --- a/src/test/scala/com/github/matsluni/akkahttpspi/dynamodb/TestDynamoDB.scala +++ b/src/test/scala/com/github/matsluni/akkahttpspi/dynamodb/TestDynamoDB.scala @@ -18,14 +18,16 @@ package com.github.matsluni.akkahttpspi.dynamodb import com.github.matsluni.akkahttpspi.{AkkaHttpAsyncHttpService, LocalstackBaseAwsClientTest} import software.amazon.awssdk.auth.credentials.{AwsBasicCredentials, StaticCredentialsProvider} +import software.amazon.awssdk.http.{Protocol, SdkHttpConfigurationOption} import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient import software.amazon.awssdk.services.dynamodb.model._ +import software.amazon.awssdk.utils.AttributeMap import scala.jdk.CollectionConverters._ class TestDynamoDB extends LocalstackBaseAwsClientTest[DynamoDbAsyncClient] { "DynamoDB" should { - "create a table" in withClient { implicit client => + "create a table" in withClient() { implicit client => val attributes = AttributeDefinition.builder.attributeName("film_id").attributeType(ScalarAttributeType.S).build() val keySchema = KeySchemaElement.builder.attributeName("film_id").keyType(KeyType.HASH).build() @@ -47,11 +49,16 @@ class TestDynamoDB extends LocalstackBaseAwsClientTest[DynamoDbAsyncClient] { tableResult.tableNames().asScala should have size (1) } + val http2AttributeMap = AttributeMap.builder().put(SdkHttpConfigurationOption.PROTOCOL, Protocol.HTTP2).build() + "work with HTTP/2" in withClient(http2AttributeMap) { implicit client => + val result = client.listTables().join() + result.tableNames() should not be null + } } - def withClient(testCode: DynamoDbAsyncClient => Any): Any = { + private def withClient(attributeMap: AttributeMap = AttributeMap.empty())(testCode: DynamoDbAsyncClient => Any): Any = { - val akkaClient = new AkkaHttpAsyncHttpService().createAsyncHttpClientFactory().build() + val akkaClient = new AkkaHttpAsyncHttpService().createAsyncHttpClientFactory().buildWithDefaults(attributeMap) val client = DynamoDbAsyncClient .builder() diff --git a/src/test/scala/com/github/matsluni/akkahttpspi/s3/TestS3.scala b/src/test/scala/com/github/matsluni/akkahttpspi/s3/TestS3.scala index 26df999..91f4b8d 100644 --- a/src/test/scala/com/github/matsluni/akkahttpspi/s3/TestS3.scala +++ b/src/test/scala/com/github/matsluni/akkahttpspi/s3/TestS3.scala @@ -17,14 +17,16 @@ package com.github.matsluni.akkahttpspi.s3 import java.io.{File, FileWriter} - import com.dimafeng.testcontainers.GenericContainer import com.github.matsluni.akkahttpspi.testcontainers.TimeoutWaitStrategy import com.github.matsluni.akkahttpspi.{AkkaHttpAsyncHttpService, BaseAwsClientTest} import software.amazon.awssdk.auth.credentials.AnonymousCredentialsProvider import software.amazon.awssdk.core.async.{AsyncRequestBody, AsyncResponseTransformer} +import software.amazon.awssdk.http.Protocol +import software.amazon.awssdk.http.SdkHttpConfigurationOption import software.amazon.awssdk.services.s3.{S3AsyncClient, S3Configuration} import software.amazon.awssdk.services.s3.model._ +import software.amazon.awssdk.utils.AttributeMap import scala.jdk.CollectionConverters._ import scala.concurrent.duration._ @@ -34,14 +36,14 @@ import scala.util.Random class TestS3 extends BaseAwsClientTest[S3AsyncClient] { "Async S3 client" should { - "create bucket" in withClient { implicit client => + "create bucket" in withClient() { implicit client => val bucketName = createBucket() val buckets = client.listBuckets().join buckets.buckets() should have size (1) buckets.buckets().asScala.toList.head.name() should be(bucketName) } - "upload and download a file to a bucket" in withClient { implicit client => + "upload and download a file to a bucket" in withClient() { implicit client => val bucketName = createBucket() val fileContent = 0 to 1000 mkString @@ -55,7 +57,7 @@ class TestS3 extends BaseAwsClientTest[S3AsyncClient] { result.response().contentLength() shouldEqual fileContent.getBytes().length } - "multipart upload" in withClient { implicit client => + "multipart upload" in withClient() { implicit client => val bucketName = createBucket() val randomFile = File.createTempFile("aws1", Random.alphanumeric.take(5).mkString) val fileContent = (0 to 1000000).mkString @@ -85,6 +87,13 @@ class TestS3 extends BaseAwsClientTest[S3AsyncClient] { result.asUtf8String() should be(fileContent + fileContent) } + val http2AttributeMap = AttributeMap.builder().put(SdkHttpConfigurationOption.PROTOCOL, Protocol.HTTP2).build() + //adobe/s3mock does not support HTTP/2 + "work with HTTP/2" ignore withClient(http2AttributeMap) { implicit client => + val result = client.listBuckets().join() + result.buckets() should not be null + } + } def createBucket()(implicit client: S3AsyncClient): String = { @@ -93,9 +102,9 @@ class TestS3 extends BaseAwsClientTest[S3AsyncClient] { bucketName } - private def withClient(testCode: S3AsyncClient => Any): Any = { + private def withClient(attributeMap: AttributeMap = AttributeMap.empty())(testCode: S3AsyncClient => Any): Any = { - val akkaClient = new AkkaHttpAsyncHttpService().createAsyncHttpClientFactory().build() + val akkaClient = new AkkaHttpAsyncHttpService().createAsyncHttpClientFactory().buildWithDefaults(attributeMap) val client = S3AsyncClient .builder() diff --git a/src/test/scala/com/github/matsluni/akkahttpspi/sns/TestSNS.scala b/src/test/scala/com/github/matsluni/akkahttpspi/sns/TestSNS.scala index f45fb36..7b0e0b1 100644 --- a/src/test/scala/com/github/matsluni/akkahttpspi/sns/TestSNS.scala +++ b/src/test/scala/com/github/matsluni/akkahttpspi/sns/TestSNS.scala @@ -18,23 +18,31 @@ package com.github.matsluni.akkahttpspi.sns import com.github.matsluni.akkahttpspi.{AkkaHttpAsyncHttpService, LocalstackBaseAwsClientTest} import software.amazon.awssdk.auth.credentials.{AwsBasicCredentials, StaticCredentialsProvider} +import software.amazon.awssdk.http.{Protocol, SdkHttpConfigurationOption} import software.amazon.awssdk.services.sns.SnsAsyncClient import software.amazon.awssdk.services.sns.model.{CreateTopicRequest, PublishRequest} +import software.amazon.awssdk.utils.AttributeMap class TestSNS extends LocalstackBaseAwsClientTest[SnsAsyncClient] { "Async SNS client" should { - "publish a message to a topic" in withClient { implicit client => + "publish a message to a topic" in withClient() { implicit client => val arn = client.createTopic(CreateTopicRequest.builder().name("topic-example").build()).join().topicArn() val result = client.publish(PublishRequest.builder().message("a message").topicArn(arn).build()).join() result.messageId() should not be null } + + val http2AttributeMap = AttributeMap.builder().put(SdkHttpConfigurationOption.PROTOCOL, Protocol.HTTP2).build() + "work with HTTP/2" in withClient(http2AttributeMap) { implicit client => + val result = client.listTopics().join() + result.topics() should not be null + } } - def withClient(testCode: SnsAsyncClient => Any): Any = { + private def withClient(attributeMap: AttributeMap = AttributeMap.empty())(testCode: SnsAsyncClient => Any): Any = { - val akkaClient = new AkkaHttpAsyncHttpService().createAsyncHttpClientFactory().build() + val akkaClient = new AkkaHttpAsyncHttpService().createAsyncHttpClientFactory().buildWithDefaults(attributeMap) val client = SnsAsyncClient .builder() diff --git a/src/test/scala/com/github/matsluni/akkahttpspi/sqs/TestSQS.scala b/src/test/scala/com/github/matsluni/akkahttpspi/sqs/TestSQS.scala index 8a01faa..0ff6320 100644 --- a/src/test/scala/com/github/matsluni/akkahttpspi/sqs/TestSQS.scala +++ b/src/test/scala/com/github/matsluni/akkahttpspi/sqs/TestSQS.scala @@ -18,21 +18,23 @@ package com.github.matsluni.akkahttpspi.sqs import com.github.matsluni.akkahttpspi.{AkkaHttpAsyncHttpService, ElasticMQSQSBaseAwsClientTest} import software.amazon.awssdk.auth.credentials.{AwsBasicCredentials, StaticCredentialsProvider} +import software.amazon.awssdk.http.{Protocol, SdkHttpConfigurationOption} import software.amazon.awssdk.services.sqs.SqsAsyncClient import software.amazon.awssdk.services.sqs.model._ +import software.amazon.awssdk.utils.AttributeMap // switched to use ElasticMQ container instead of Localstack due to https://github.com/localstack/localstack/issues/8545 class TestSQS extends ElasticMQSQSBaseAwsClientTest[SqsAsyncClient] { "Async SQS client" should { - "publish a message to a queue" in withClient { implicit client => + "publish a message to a queue" in withClient() { implicit client => client.createQueue(CreateQueueRequest.builder().queueName("foo").build()).join() client.sendMessage(SendMessageRequest.builder().queueUrl(s"$endpoint/queue/foo").messageBody("123").build()).join() val receivedMessage = client.receiveMessage(ReceiveMessageRequest.builder().queueUrl(s"$endpoint/queue/foo").maxNumberOfMessages(1).build()).join() receivedMessage.messages().get(0).body() should be("123") } - "delete a message" in withClient { implicit client => + "delete a message" in withClient() { implicit client => client.createQueue(CreateQueueRequest.builder().queueName("foo").build()).join() client.sendMessage(SendMessageRequest.builder().queueUrl(s"$endpoint/queue/foo").messageBody("123").build()).join() @@ -44,11 +46,18 @@ class TestSQS extends ElasticMQSQSBaseAwsClientTest[SqsAsyncClient] { receivedMessage.messages() shouldBe java.util.Collections.EMPTY_LIST } + val http2AttributeMap = AttributeMap.builder().put(SdkHttpConfigurationOption.PROTOCOL, Protocol.HTTP2).build() + //softwaremill/elasticmq-native does not support HTTP/2 + "work with HTTP/2" ignore withClient(http2AttributeMap) { implicit client => + val result = client.listQueues().join() + result.queueUrls() should not be null + } + } - def withClient(testCode: SqsAsyncClient => Any): Any = { + private def withClient(attributeMap: AttributeMap = AttributeMap.empty())(testCode: SqsAsyncClient => Any): Any = { - val akkaClient = new AkkaHttpAsyncHttpService().createAsyncHttpClientFactory().build() + val akkaClient = new AkkaHttpAsyncHttpService().createAsyncHttpClientFactory().buildWithDefaults(attributeMap) val client = SqsAsyncClient .builder() From dc2c5dac26baa1b4a4a43c3084c8209c39ad7187 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Ferreira?= Date: Thu, 19 Oct 2023 13:20:47 +0100 Subject: [PATCH 3/7] improve readme --- README.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f8555a9..22c11ba 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,21 @@ val client = S3AsyncClient val eventualResponse = client.listBuckets() ``` +You can also set `software.amazon.awssdk.http.SdkHttpConfigurationOption` options: + +```scala +val attributeMap = AttributeMap.builder() + .put(SdkHttpConfigurationOption.PROTOCOL, Protocol.HTTP2) + .build() +val akkaHttpClient = + AkkaHttpClient + .builder() + .buildWithDefaults(attributeMap) +``` + +The following options are supported: +* `SdkHttpConfigurationOption.PROTOCOL` + When you use this library and a specific AWS service (e.g. S3, SQS, etc...) you may want to exclude the transitive Netty dependency `netty-nio-client` like this: @@ -85,7 +100,7 @@ If you not exclude the transitive dependency like shown above you have to explic in the service client instantiation. If no client is explicitly set and multiple implementations for the `SdkAsyncHttpClient` are found on the classpath, an exception like the following is thrown at runtime: -```java +``` software.amazon.awssdk.core.exception.SdkClientException: Multiple HTTP implementations were found on the classpath. To avoid non-deterministic loading implementations, please explicitly provide an HTTP client via the client builders, set the software.amazon.awssdk.http.async.service.impl system property with the FQCN of the HTTP service to use as the From 8ac221215410c09fa0c6cd19bba9160a84e32036 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Ferreira?= Date: Thu, 19 Oct 2023 13:32:03 +0100 Subject: [PATCH 4/7] increase TimeoutWaitStrategy, as per the logs s3mock is taking more than 10s to boot 11:04:01.130 [pool-1-thread-1] INFO t.a.1.0 - Container adobe/s3mock:2.13.0 is starting: 85d5881648cb02a95ecc3d406cf0b70ca7a6638c42a0d5e1f800c783479ee14e 11:04:11.530 [pool-1-thread-1] INFO t.a.1.0 - Container adobe/s3mock:2.13.0 started in PT10.436S --- src/test/scala/com/github/matsluni/akkahttpspi/s3/TestS3.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/scala/com/github/matsluni/akkahttpspi/s3/TestS3.scala b/src/test/scala/com/github/matsluni/akkahttpspi/s3/TestS3.scala index 91f4b8d..e6d40fb 100644 --- a/src/test/scala/com/github/matsluni/akkahttpspi/s3/TestS3.scala +++ b/src/test/scala/com/github/matsluni/akkahttpspi/s3/TestS3.scala @@ -135,7 +135,7 @@ class TestS3 extends BaseAwsClientTest[S3AsyncClient] { private lazy val containerInstance = new GenericContainer( dockerImage = "adobe/s3mock:2.13.0", exposedPorts = Seq(exposedServicePort), - waitStrategy = Some(TimeoutWaitStrategy(10 seconds)) + waitStrategy = Some(TimeoutWaitStrategy(15 seconds)) ) override val container: GenericContainer = containerInstance } From 35732cd1aa0022ab976bd1358ad8307a0d9ce9bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Ferreira?= Date: Thu, 19 Oct 2023 14:47:44 +0100 Subject: [PATCH 5/7] do not use SdkHttpConfigurationOption --- .../matsluni/akkahttpspi/AkkaHttpClient.scala | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/main/scala/com/github/matsluni/akkahttpspi/AkkaHttpClient.scala b/src/main/scala/com/github/matsluni/akkahttpspi/AkkaHttpClient.scala index b3c171f..a92982a 100644 --- a/src/main/scala/com/github/matsluni/akkahttpspi/AkkaHttpClient.scala +++ b/src/main/scala/com/github/matsluni/akkahttpspi/AkkaHttpClient.scala @@ -32,7 +32,7 @@ import akka.stream.{ActorMaterializer, Materializer, SystemMaterializer} import akka.util.ByteString import org.slf4j.LoggerFactory import software.amazon.awssdk.http.async._ -import software.amazon.awssdk.http.{Protocol, SdkHttpConfigurationOption, SdkHttpRequest} +import software.amazon.awssdk.http.SdkHttpRequest import software.amazon.awssdk.utils.AttributeMap import scala.collection.immutable @@ -139,16 +139,15 @@ object AkkaHttpClient { case class AkkaHttpClientBuilder(private val actorSystem: Option[ActorSystem] = None, private val executionContext: Option[ExecutionContext] = None, - private val connectionPoolSettings: Option[ConnectionPoolSettings] = None) extends SdkAsyncHttpClient.Builder[AkkaHttpClientBuilder] { + private val connectionPoolSettings: Option[ConnectionPoolSettings] = None, + private val protocol: HttpProtocol = HttpProtocols.`HTTP/1.1` + ) extends SdkAsyncHttpClient.Builder[AkkaHttpClientBuilder] { def buildWithDefaults(attributeMap: AttributeMap): SdkAsyncHttpClient = { implicit val as = actorSystem.getOrElse(ActorSystem("aws-akka-http")) implicit val ec = executionContext.getOrElse(as.dispatcher) val mat: Materializer = SystemMaterializer(as).materializer - val mergedAttributeMap = attributeMap.merge(SdkHttpConfigurationOption.GLOBAL_HTTP_DEFAULTS) - val cps = connectionPoolSettings.getOrElse(ConnectionPoolSettings(as)) - val protocol = convertProtocol(mergedAttributeMap.get(SdkHttpConfigurationOption.PROTOCOL)) val shutdownhandleF = () => { if (actorSystem.isEmpty) { Await.result(Http().shutdownAllConnectionPools().flatMap(_ => as.terminate()), Duration.apply(10, TimeUnit.SECONDS)) @@ -163,13 +162,6 @@ object AkkaHttpClient { def withConnectionPoolSettings(connectionPoolSettings: ConnectionPoolSettings): AkkaHttpClientBuilder = copy(connectionPoolSettings = Some(connectionPoolSettings)) } - private def convertProtocol(protocol: Protocol) = { - protocol match { - case Protocol.HTTP1_1 => HttpProtocols.`HTTP/1.1` - case Protocol.HTTP2 => HttpProtocols.`HTTP/2.0` - } - } - lazy val xAmzJson = ContentType(MediaType.customBinary("application", "x-amz-json-1.0", Compressible)) lazy val xAmzJson11 = ContentType(MediaType.customBinary("application", "x-amz-json-1.1", Compressible)) lazy val xAmzCbor11 = ContentType(MediaType.customBinary("application", "x-amz-cbor-1.1", Compressible)) From 0060c7fc33a97335586d72bb48790432c1a10ba5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Ferreira?= Date: Thu, 26 Oct 2023 14:16:15 +0100 Subject: [PATCH 6/7] add withProtocol method fix tests to use withProtocol instead of attributeMap --- .../matsluni/akkahttpspi/AkkaHttpClient.scala | 1 + .../akkahttpspi/dynamodb/TestDynamoDB.scala | 11 +++++----- .../matsluni/akkahttpspi/s3/TestS3.scala | 20 ++++++++++--------- .../matsluni/akkahttpspi/sns/TestSNS.scala | 11 +++++----- .../matsluni/akkahttpspi/sqs/TestSQS.scala | 19 ++++++++++-------- 5 files changed, 33 insertions(+), 29 deletions(-) diff --git a/src/main/scala/com/github/matsluni/akkahttpspi/AkkaHttpClient.scala b/src/main/scala/com/github/matsluni/akkahttpspi/AkkaHttpClient.scala index a92982a..1e189e2 100644 --- a/src/main/scala/com/github/matsluni/akkahttpspi/AkkaHttpClient.scala +++ b/src/main/scala/com/github/matsluni/akkahttpspi/AkkaHttpClient.scala @@ -160,6 +160,7 @@ object AkkaHttpClient { def withActorSystem(actorSystem: ClassicActorSystemProvider): AkkaHttpClientBuilder = copy(actorSystem = Some(actorSystem.classicSystem)) def withExecutionContext(executionContext: ExecutionContext): AkkaHttpClientBuilder = copy(executionContext = Some(executionContext)) def withConnectionPoolSettings(connectionPoolSettings: ConnectionPoolSettings): AkkaHttpClientBuilder = copy(connectionPoolSettings = Some(connectionPoolSettings)) + def withProtocol(protocol: HttpProtocol): AkkaHttpClientBuilder = copy(protocol = protocol) } lazy val xAmzJson = ContentType(MediaType.customBinary("application", "x-amz-json-1.0", Compressible)) diff --git a/src/test/scala/com/github/matsluni/akkahttpspi/dynamodb/TestDynamoDB.scala b/src/test/scala/com/github/matsluni/akkahttpspi/dynamodb/TestDynamoDB.scala index 0974290..95d4a03 100644 --- a/src/test/scala/com/github/matsluni/akkahttpspi/dynamodb/TestDynamoDB.scala +++ b/src/test/scala/com/github/matsluni/akkahttpspi/dynamodb/TestDynamoDB.scala @@ -16,12 +16,12 @@ package com.github.matsluni.akkahttpspi.dynamodb +import akka.http.scaladsl.model.HttpProtocols +import com.github.matsluni.akkahttpspi.AkkaHttpClient.AkkaHttpClientBuilder import com.github.matsluni.akkahttpspi.{AkkaHttpAsyncHttpService, LocalstackBaseAwsClientTest} import software.amazon.awssdk.auth.credentials.{AwsBasicCredentials, StaticCredentialsProvider} -import software.amazon.awssdk.http.{Protocol, SdkHttpConfigurationOption} import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient import software.amazon.awssdk.services.dynamodb.model._ -import software.amazon.awssdk.utils.AttributeMap import scala.jdk.CollectionConverters._ @@ -49,16 +49,15 @@ class TestDynamoDB extends LocalstackBaseAwsClientTest[DynamoDbAsyncClient] { tableResult.tableNames().asScala should have size (1) } - val http2AttributeMap = AttributeMap.builder().put(SdkHttpConfigurationOption.PROTOCOL, Protocol.HTTP2).build() - "work with HTTP/2" in withClient(http2AttributeMap) { implicit client => + "work with HTTP/2" in withClient(_.withProtocol(HttpProtocols.`HTTP/2.0`)) { implicit client => val result = client.listTables().join() result.tableNames() should not be null } } - private def withClient(attributeMap: AttributeMap = AttributeMap.empty())(testCode: DynamoDbAsyncClient => Any): Any = { + private def withClient(builderFn: AkkaHttpClientBuilder => AkkaHttpClientBuilder = identity)(testCode: DynamoDbAsyncClient => Any): Any = { - val akkaClient = new AkkaHttpAsyncHttpService().createAsyncHttpClientFactory().buildWithDefaults(attributeMap) + val akkaClient = builderFn(new AkkaHttpAsyncHttpService().createAsyncHttpClientFactory()).build() val client = DynamoDbAsyncClient .builder() diff --git a/src/test/scala/com/github/matsluni/akkahttpspi/s3/TestS3.scala b/src/test/scala/com/github/matsluni/akkahttpspi/s3/TestS3.scala index e6d40fb..86858d5 100644 --- a/src/test/scala/com/github/matsluni/akkahttpspi/s3/TestS3.scala +++ b/src/test/scala/com/github/matsluni/akkahttpspi/s3/TestS3.scala @@ -17,16 +17,17 @@ package com.github.matsluni.akkahttpspi.s3 import java.io.{File, FileWriter} +import java.util.concurrent.CompletionException + +import akka.http.scaladsl.model.HttpProtocols import com.dimafeng.testcontainers.GenericContainer +import com.github.matsluni.akkahttpspi.AkkaHttpClient.AkkaHttpClientBuilder import com.github.matsluni.akkahttpspi.testcontainers.TimeoutWaitStrategy import com.github.matsluni.akkahttpspi.{AkkaHttpAsyncHttpService, BaseAwsClientTest} import software.amazon.awssdk.auth.credentials.AnonymousCredentialsProvider import software.amazon.awssdk.core.async.{AsyncRequestBody, AsyncResponseTransformer} -import software.amazon.awssdk.http.Protocol -import software.amazon.awssdk.http.SdkHttpConfigurationOption import software.amazon.awssdk.services.s3.{S3AsyncClient, S3Configuration} import software.amazon.awssdk.services.s3.model._ -import software.amazon.awssdk.utils.AttributeMap import scala.jdk.CollectionConverters._ import scala.concurrent.duration._ @@ -87,11 +88,12 @@ class TestS3 extends BaseAwsClientTest[S3AsyncClient] { result.asUtf8String() should be(fileContent + fileContent) } - val http2AttributeMap = AttributeMap.builder().put(SdkHttpConfigurationOption.PROTOCOL, Protocol.HTTP2).build() //adobe/s3mock does not support HTTP/2 - "work with HTTP/2" ignore withClient(http2AttributeMap) { implicit client => - val result = client.listBuckets().join() - result.buckets() should not be null + "work with HTTP/2" in withClient(_.withProtocol(HttpProtocols.`HTTP/2.0`)) { implicit client => + the[CompletionException] thrownBy { + val result = client.listBuckets().join() + result.buckets() should not be null + } should have message "software.amazon.awssdk.services.s3.model.S3Exception: null (Service: S3, Status Code: 426, Request ID: null)" } } @@ -102,9 +104,9 @@ class TestS3 extends BaseAwsClientTest[S3AsyncClient] { bucketName } - private def withClient(attributeMap: AttributeMap = AttributeMap.empty())(testCode: S3AsyncClient => Any): Any = { + private def withClient(builderFn: AkkaHttpClientBuilder => AkkaHttpClientBuilder = identity)(testCode: S3AsyncClient => Any): Any = { - val akkaClient = new AkkaHttpAsyncHttpService().createAsyncHttpClientFactory().buildWithDefaults(attributeMap) + val akkaClient = builderFn(new AkkaHttpAsyncHttpService().createAsyncHttpClientFactory()).build() val client = S3AsyncClient .builder() diff --git a/src/test/scala/com/github/matsluni/akkahttpspi/sns/TestSNS.scala b/src/test/scala/com/github/matsluni/akkahttpspi/sns/TestSNS.scala index 7b0e0b1..68d561b 100644 --- a/src/test/scala/com/github/matsluni/akkahttpspi/sns/TestSNS.scala +++ b/src/test/scala/com/github/matsluni/akkahttpspi/sns/TestSNS.scala @@ -16,12 +16,12 @@ package com.github.matsluni.akkahttpspi.sns +import akka.http.scaladsl.model.HttpProtocols +import com.github.matsluni.akkahttpspi.AkkaHttpClient.AkkaHttpClientBuilder import com.github.matsluni.akkahttpspi.{AkkaHttpAsyncHttpService, LocalstackBaseAwsClientTest} import software.amazon.awssdk.auth.credentials.{AwsBasicCredentials, StaticCredentialsProvider} -import software.amazon.awssdk.http.{Protocol, SdkHttpConfigurationOption} import software.amazon.awssdk.services.sns.SnsAsyncClient import software.amazon.awssdk.services.sns.model.{CreateTopicRequest, PublishRequest} -import software.amazon.awssdk.utils.AttributeMap class TestSNS extends LocalstackBaseAwsClientTest[SnsAsyncClient] { @@ -33,16 +33,15 @@ class TestSNS extends LocalstackBaseAwsClientTest[SnsAsyncClient] { result.messageId() should not be null } - val http2AttributeMap = AttributeMap.builder().put(SdkHttpConfigurationOption.PROTOCOL, Protocol.HTTP2).build() - "work with HTTP/2" in withClient(http2AttributeMap) { implicit client => + "work with HTTP/2" in withClient(_.withProtocol(HttpProtocols.`HTTP/2.0`)) { implicit client => val result = client.listTopics().join() result.topics() should not be null } } - private def withClient(attributeMap: AttributeMap = AttributeMap.empty())(testCode: SnsAsyncClient => Any): Any = { + private def withClient(builderFn: AkkaHttpClientBuilder => AkkaHttpClientBuilder = identity)(testCode: SnsAsyncClient => Any): Any = { - val akkaClient = new AkkaHttpAsyncHttpService().createAsyncHttpClientFactory().buildWithDefaults(attributeMap) + val akkaClient = builderFn(new AkkaHttpAsyncHttpService().createAsyncHttpClientFactory()).build() val client = SnsAsyncClient .builder() diff --git a/src/test/scala/com/github/matsluni/akkahttpspi/sqs/TestSQS.scala b/src/test/scala/com/github/matsluni/akkahttpspi/sqs/TestSQS.scala index 0ff6320..da1e128 100644 --- a/src/test/scala/com/github/matsluni/akkahttpspi/sqs/TestSQS.scala +++ b/src/test/scala/com/github/matsluni/akkahttpspi/sqs/TestSQS.scala @@ -16,12 +16,14 @@ package com.github.matsluni.akkahttpspi.sqs +import akka.http.scaladsl.model.HttpProtocols +import com.github.matsluni.akkahttpspi.AkkaHttpClient.AkkaHttpClientBuilder import com.github.matsluni.akkahttpspi.{AkkaHttpAsyncHttpService, ElasticMQSQSBaseAwsClientTest} import software.amazon.awssdk.auth.credentials.{AwsBasicCredentials, StaticCredentialsProvider} -import software.amazon.awssdk.http.{Protocol, SdkHttpConfigurationOption} import software.amazon.awssdk.services.sqs.SqsAsyncClient import software.amazon.awssdk.services.sqs.model._ -import software.amazon.awssdk.utils.AttributeMap + +import java.util.concurrent.CompletionException // switched to use ElasticMQ container instead of Localstack due to https://github.com/localstack/localstack/issues/8545 class TestSQS extends ElasticMQSQSBaseAwsClientTest[SqsAsyncClient] { @@ -46,18 +48,19 @@ class TestSQS extends ElasticMQSQSBaseAwsClientTest[SqsAsyncClient] { receivedMessage.messages() shouldBe java.util.Collections.EMPTY_LIST } - val http2AttributeMap = AttributeMap.builder().put(SdkHttpConfigurationOption.PROTOCOL, Protocol.HTTP2).build() //softwaremill/elasticmq-native does not support HTTP/2 - "work with HTTP/2" ignore withClient(http2AttributeMap) { implicit client => - val result = client.listQueues().join() - result.queueUrls() should not be null + "work with HTTP/2" in withClient(_.withProtocol(HttpProtocols.`HTTP/2.0`)) { implicit client => + the [CompletionException] thrownBy { + val result = client.listQueues().join() + result.queueUrls() should not be null + } should have message "software.amazon.awssdk.services.sqs.model.SqsException: null (Service: Sqs, Status Code: 505, Request ID: null)" } } - private def withClient(attributeMap: AttributeMap = AttributeMap.empty())(testCode: SqsAsyncClient => Any): Any = { + private def withClient(builderFn: AkkaHttpClientBuilder => AkkaHttpClientBuilder = identity)(testCode: SqsAsyncClient => Any): Any = { - val akkaClient = new AkkaHttpAsyncHttpService().createAsyncHttpClientFactory().buildWithDefaults(attributeMap) + val akkaClient = builderFn(new AkkaHttpAsyncHttpService().createAsyncHttpClientFactory()).build() val client = SqsAsyncClient .builder() From a006ad2951993336a1714c036958e05f16318c0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Ferreira?= Date: Thu, 26 Oct 2023 14:30:45 +0100 Subject: [PATCH 7/7] revert changes in README.md --- README.md | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/README.md b/README.md index 22c11ba..f8555a9 100644 --- a/README.md +++ b/README.md @@ -72,21 +72,6 @@ val client = S3AsyncClient val eventualResponse = client.listBuckets() ``` -You can also set `software.amazon.awssdk.http.SdkHttpConfigurationOption` options: - -```scala -val attributeMap = AttributeMap.builder() - .put(SdkHttpConfigurationOption.PROTOCOL, Protocol.HTTP2) - .build() -val akkaHttpClient = - AkkaHttpClient - .builder() - .buildWithDefaults(attributeMap) -``` - -The following options are supported: -* `SdkHttpConfigurationOption.PROTOCOL` - When you use this library and a specific AWS service (e.g. S3, SQS, etc...) you may want to exclude the transitive Netty dependency `netty-nio-client` like this: @@ -100,7 +85,7 @@ If you not exclude the transitive dependency like shown above you have to explic in the service client instantiation. If no client is explicitly set and multiple implementations for the `SdkAsyncHttpClient` are found on the classpath, an exception like the following is thrown at runtime: -``` +```java software.amazon.awssdk.core.exception.SdkClientException: Multiple HTTP implementations were found on the classpath. To avoid non-deterministic loading implementations, please explicitly provide an HTTP client via the client builders, set the software.amazon.awssdk.http.async.service.impl system property with the FQCN of the HTTP service to use as the