From 656a342a828b5f233667f195f82ab1f583976fa7 Mon Sep 17 00:00:00 2001 From: slve Date: Sun, 14 Feb 2021 09:21:41 +0000 Subject: [PATCH 1/8] Factor out common code to Server and package object --- src/main/scala/BlazeClientTest.scala | 51 +++++----------------- src/main/scala/EmberClientTest.scala | 44 +++---------------- src/main/scala/JdkHttpClientTest.scala | 59 ++++--------------------- src/main/scala/Server.scala | 32 ++++++++++++++ src/main/scala/SttpClientTest.scala | 60 ++++++-------------------- src/main/scala/package.scala | 26 +++++++++++ 6 files changed, 99 insertions(+), 173 deletions(-) create mode 100644 src/main/scala/Server.scala create mode 100644 src/main/scala/package.scala diff --git a/src/main/scala/BlazeClientTest.scala b/src/main/scala/BlazeClientTest.scala index 771ca43..4394cb4 100644 --- a/src/main/scala/BlazeClientTest.scala +++ b/src/main/scala/BlazeClientTest.scala @@ -1,18 +1,15 @@ -package io.bitrise.apm.symbolicator - -import cats.effect.{ExitCode, IO, IOApp} +import cats.effect.{ConcurrentEffect, ExitCode, IO, IOApp} import fs2.Stream -import org.http4s.{HttpRoutes, _} +import helpers.requestStream +import org.http4s._ import org.http4s.client.Client import org.http4s.client.blaze.BlazeClientBuilder import org.http4s.dsl.io._ import org.http4s.implicits._ -import org.http4s.server.blaze.BlazeServerBuilder import scala.concurrent.ExecutionContext import scala.concurrent.duration._ -object BlazeClientTest extends IOApp { // Numbers below vary on different computers // In my case, if the request payload size is 65398 or greater @@ -35,49 +32,23 @@ object BlazeClientTest extends IOApp { //val appTime = 30.seconds //val requestPayloadSize = 65398 //val responsePayloadSize = 81160 +class BlazeClientTest(appTime: FiniteDuration, requestPayloadSize: Int, responsePayloadSize: Int) extends IOApp { val uri = uri"http://localhost:8099" val body = "x" * requestPayloadSize val req = Request[IO](POST, uri).withEntity(body) val response = "x" * responsePayloadSize - var i = 0 override def run(args: List[String]): IO[ExitCode] = { - def requestStream(client: Client[IO]): Stream[IO, Unit] = Stream - .fixedRate(0.01.second) - .flatMap(_ => { - i = i + 1 + def request(client: Client[IO]): Stream[IO, String] = client.stream(req).flatMap(_.bodyText) - client.stream(req).flatMap(_.bodyText) - }) - .evalMap(c => IO.delay(println(s"$i ${c.size}"))) - .interruptAfter(appTime) + def simpleClient(implicit c: ConcurrentEffect[IO]): BlazeClientBuilder[IO] = + BlazeClientBuilder[IO](ExecutionContext.global) + .withRequestTimeout(45.seconds) + .withIdleTimeout(1.minute) + .withResponseHeaderTimeout(44.seconds) - server(simpleClient.stream.flatMap(requestStream)) + new Server(simpleClient.stream.flatMap(c => requestStream(request(c), appTime)), appTime, response).run(List()) } - val simpleClient: BlazeClientBuilder[IO] = - BlazeClientBuilder[IO](ExecutionContext.global) - .withRequestTimeout(45.seconds) - .withIdleTimeout(1.minute) - .withResponseHeaderTimeout(44.seconds) - - def server(app: Stream[IO, Unit]) = - BlazeServerBuilder[IO](ExecutionContext.global) - .withIdleTimeout(5.minutes) - .bindHttp(8099, "0.0.0.0") - .withHttpApp( - HttpRoutes - .of[IO] { - case POST -> Root => Ok(response) - } - .orNotFound - ) - .serve - .concurrently(app) - .interruptAfter(appTime + 2.seconds) - .compile - .drain - .as(ExitCode.Success) - } diff --git a/src/main/scala/EmberClientTest.scala b/src/main/scala/EmberClientTest.scala index cf3260a..ce4c154 100644 --- a/src/main/scala/EmberClientTest.scala +++ b/src/main/scala/EmberClientTest.scala @@ -1,18 +1,14 @@ -package io.bitrise.apm.symbolicator - import cats.effect.{ExitCode, IO, IOApp, Resource} import fs2.Stream +import helpers.requestStream +import org.http4s._ import org.http4s.client.Client import org.http4s.dsl.io._ import org.http4s.ember.client.EmberClientBuilder import org.http4s.implicits._ -import org.http4s.server.blaze.BlazeServerBuilder -import org.http4s.{HttpRoutes, _} -import scala.concurrent.ExecutionContext import scala.concurrent.duration._ -object EmberClientTest extends IOApp { // Numbers below vary on different computers // In my case, if the request payload size is 65337 or greater @@ -30,6 +26,7 @@ object EmberClientTest extends IOApp { //val appTime = 120.seconds //val requestPayloadSize = 65336 //val responsePayloadSize = 200 * 1000 * 1000 +class EmberClientTest(appTime: FiniteDuration, requestPayloadSize: Int, responsePayloadSize: Int) extends IOApp { val uri = uri"http://localhost:8099" val body = "x" * requestPayloadSize @@ -38,41 +35,14 @@ object EmberClientTest extends IOApp { var i = 0 override def run(args: List[String]): IO[ExitCode] = { - def requestStream(client: Client[IO]): Stream[IO, Unit] = - Stream - .fixedRate(0.01.second) - .flatMap(_ => { - i = i + 1 + def request(client: Client[IO]): Stream[IO, String] = client.stream(req).flatMap(_.bodyText) - client.stream(req).flatMap(_.bodyText) - }) - .evalMap(c => IO.delay(println(s"$i ${c.size}"))) - .interruptAfter(appTime) + val simpleClient: Resource[IO, Client[IO]] = + EmberClientBuilder.default[IO].build simpleClient.use { client => - server(requestStream(client)) + new Server(requestStream(request(client), appTime), appTime, response).run(List()) } } - val simpleClient: Resource[IO, Client[IO]] = - EmberClientBuilder.default[IO].build - - def server(app: Stream[IO, Unit]) = - BlazeServerBuilder[IO](ExecutionContext.global) - .withIdleTimeout(5.minutes) - .bindHttp(8099, "0.0.0.0") - .withHttpApp( - HttpRoutes - .of[IO] { - case POST -> Root => Ok(response) - } - .orNotFound - ) - .serve - .concurrently(app) - .interruptAfter(appTime + 2.seconds) - .compile - .drain - .as(ExitCode.Success) - } diff --git a/src/main/scala/JdkHttpClientTest.scala b/src/main/scala/JdkHttpClientTest.scala index 56b426a..199a3a8 100644 --- a/src/main/scala/JdkHttpClientTest.scala +++ b/src/main/scala/JdkHttpClientTest.scala @@ -1,20 +1,14 @@ -package io.bitrise.apm.symbolicator - import cats.effect.{ExitCode, IO, IOApp} import fs2.Stream +import helpers.requestStream +import org.http4s._ import org.http4s.client.Client import org.http4s.client.jdkhttpclient.JdkHttpClient import org.http4s.dsl.io._ import org.http4s.implicits._ -import org.http4s.server.blaze.BlazeServerBuilder -import org.http4s.{HttpRoutes, _} -import scala.concurrent.ExecutionContext import scala.concurrent.duration._ -object JdkHttpClientTest extends IOApp { - - val appTime = 300.seconds // Numbers below may vary on different computers // In my case, if the request payload size is 523329 or greater // OR response payload size is 65417 or greater @@ -35,56 +29,21 @@ object JdkHttpClientTest extends IOApp { // quite broken // val requestPayloadSize = 523328 // val responsePayloadSize = 65417 +class JdkHttpClientTest(appTime: FiniteDuration, requestPayloadSize: Int, responsePayloadSize: Int) extends IOApp { val uri = uri"http://localhost:8099" val body = "x" * requestPayloadSize val req = Request[IO](POST, uri).withEntity(body) val response = "x" * responsePayloadSize - var i = 0 override def run(args: List[String]): IO[ExitCode] = { - simpleClient.flatMap(client => { - val requestStream: Stream[IO, Unit] = Stream - .fixedRate(0.01.second) - .flatMap(_ => { - i = i + 1 - val y: Stream[IO, Response[IO]] = client.stream(req) - y.flatMap(r => r.bodyText) - }) - .evalMap(c => IO.delay(println(s"$i ${c.size}"))) - .interruptAfter(appTime) - - server(requestStream) - }) - } + def request(client: Client[IO]): Stream[IO, String] = client.stream(req).flatMap(_.bodyText) - import java.net.http.HttpClient - val client0: IO[Client[IO]] = IO { - HttpClient - .newBuilder() - .version(HttpClient.Version.HTTP_2) - .build() + val simpleClient: IO[Client[IO]] = JdkHttpClient.simple[IO] - }.map(JdkHttpClient(_)) - - val simpleClient: IO[Client[IO]] = JdkHttpClient.simple[IO] - - def server(app: Stream[IO, Unit]) = - BlazeServerBuilder[IO](ExecutionContext.global) - .withIdleTimeout(5.minutes) - .bindHttp(8099, "0.0.0.0") - .withHttpApp( - HttpRoutes - .of[IO] { - case POST -> Root => Ok(response) - } - .orNotFound - ) - .serve - .concurrently(app) - .interruptAfter(appTime + 2.seconds) - .compile - .drain - .as(ExitCode.Success) + simpleClient.flatMap { client => + new Server(requestStream(request(client), appTime), appTime, response).run(List()) + } + } } diff --git a/src/main/scala/Server.scala b/src/main/scala/Server.scala new file mode 100644 index 0000000..e41b08d --- /dev/null +++ b/src/main/scala/Server.scala @@ -0,0 +1,32 @@ +import cats.effect.{ExitCode, IO, IOApp} +import fs2.Stream +import org.http4s.HttpRoutes +import org.http4s.dsl.io._ +import org.http4s.implicits._ +import org.http4s.server.blaze.BlazeServerBuilder + +import scala.concurrent.ExecutionContext +import scala.concurrent.duration._ + +class Server(app: Stream[IO, Unit], appTime: FiniteDuration, response: String) extends IOApp { + + override def run(args: List[String]): IO[ExitCode] = { + BlazeServerBuilder[IO](ExecutionContext.global) + .withIdleTimeout(5.minutes) + .bindHttp(8099, "0.0.0.0") + .withHttpApp( + HttpRoutes + .of[IO] { + case POST -> Root => Ok(response) + } + .orNotFound + ) + .serve + .concurrently(app) + .interruptAfter(appTime + 0.2.seconds) + .compile + .drain + .as(ExitCode.Success) + + } +} diff --git a/src/main/scala/SttpClientTest.scala b/src/main/scala/SttpClientTest.scala index fba6564..6df7f36 100644 --- a/src/main/scala/SttpClientTest.scala +++ b/src/main/scala/SttpClientTest.scala @@ -1,12 +1,10 @@ -package io.bitrise.apm.symbolicator - import cats.effect.{ExitCode, IO, IOApp} import fs2.Stream +import helpers.requestStream +import sttp.client3._ -import scala.concurrent.ExecutionContext import scala.concurrent.duration._ -object SttpClientTest extends IOApp { // Numbers below vary on different computers // In my case, if the request payload size is 65289 or greater @@ -29,58 +27,28 @@ object SttpClientTest extends IOApp { //val appTime = 120.seconds //val requestPayloadSize = 65289 //val responsePayloadSize = 81160 +class SttpClientTest(appTime: FiniteDuration, requestPayloadSize: Int, responsePayloadSize: Int) extends IOApp { + val uri = uri"http://localhost:8099" val body = "x" * requestPayloadSize val response = "x" * responsePayloadSize + val backend = HttpURLConnectionBackend() - var i = 0 override def run(args: List[String]): IO[ExitCode] = { - import sttp.client3._ - - val uri = uri"http://localhost:8099" - - import sttp.client3._ - - val backend = HttpURLConnectionBackend() - - val requestStream: Stream[IO, Unit] = Stream - .fixedRate(0.01.second) - .map(_ => { - i = i + 1 + def request: Stream[IO, String] = Stream.eval { + IO( basicRequest .body(body) .post(uri) - .send(backend).body.toOption.get // let's just throw if fails to get - }) - .evalMap(c => IO.delay(println(s"$i ${c.size}"))) - .interruptAfter(appTime) - - server(requestStream) - } - - def server(app: Stream[IO, Unit]) = { - import org.http4s.HttpRoutes - import org.http4s.dsl.io._ - import org.http4s.implicits._ - import org.http4s.server.blaze.BlazeServerBuilder - - BlazeServerBuilder[IO](ExecutionContext.global) - .withIdleTimeout(5.minutes) - .bindHttp(8099, "0.0.0.0") - .withHttpApp( - HttpRoutes - .of[IO] { - case POST -> Root => Ok(response) - } - .orNotFound + .send(backend) + .body + .toOption + .get // let's just throw if fails to get the response body ) - .serve - .concurrently(app) - .interruptAfter(appTime + 2.seconds) - .compile - .drain - .as(ExitCode.Success) + } + + new Server(requestStream(request, appTime), appTime, response).run(List()) } } diff --git a/src/main/scala/package.scala b/src/main/scala/package.scala new file mode 100644 index 0000000..ff64f06 --- /dev/null +++ b/src/main/scala/package.scala @@ -0,0 +1,26 @@ +import cats.effect.{Concurrent, IO, Timer} +import fs2.Stream + +import scala.concurrent.duration._ + +package object helpers { + + def requestStream( + request: Stream[IO, String], + appTime: FiniteDuration + )(implicit c: Concurrent[IO], t: Timer[IO]): Stream[IO, Unit] = { + + var i = 0 + Stream + .fixedRate(0.01.second) + .flatMap(_ => { + i = i + 1 + + request + }) + //.evalMap(c => IO.delay(println(s"$i ${c.size}"))) + .evalMap(_ => IO.delay(())) + .interruptAfter(appTime) + } + +} From 86b71e455aaf500ed877cfa812b5e76b594cf78d Mon Sep 17 00:00:00 2001 From: slve Date: Sun, 14 Feb 2021 09:22:46 +0000 Subject: [PATCH 2/8] Upgrade scalatest, disable parallel run --- build.sbt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index cf1c3bf..9b8543b 100644 --- a/build.sbt +++ b/build.sbt @@ -1,4 +1,5 @@ name := "scala" +parallelExecution := false scalaVersion := "2.13.3" scalacOptions ++= Seq("-Wconf:any:warning-verbose") @@ -6,7 +7,7 @@ libraryDependencies ++= Seq( "co.fs2" %% "fs2-core" % "2.5.0", "com.softwaremill.sttp.client3" %% "core" % "3.1.1", "org.http4s" %% "http4s-jdk-http-client" % "0.3.5", - "org.scalatest" %% "scalatest" % "3.0.8", + "org.scalatest" %% "scalatest" % "3.2.2", "org.typelevel" %% "cats-core" % "2.4.1", "org.typelevel" %% "cats-effect" % "2.3.1" ) ++ Seq( From aecc885334b15b7a4a27d7e49f965e6779a65282 Mon Sep 17 00:00:00 2001 From: slve Date: Sun, 14 Feb 2021 09:48:59 +0000 Subject: [PATCH 3/8] Add tests, move existing manual debugging there too --- src/main/scala/BlazeClientTest.scala | 22 --------- src/main/scala/EmberClientTest.scala | 17 ------- src/main/scala/JdkHttpClientTest.scala | 20 -------- src/main/scala/SttpClientTest.scala | 22 --------- src/test/scala/Http4sClientsSuite.scala | 47 +++++++++++++++++++ src/test/scala/MagicNumbersBlaze.scala | 31 ++++++++++++ src/test/scala/MagicNumbersEmber.scala | 32 +++++++++++++ src/test/scala/MagicNumbersJdkHttp.scala | 30 ++++++++++++ src/test/scala/MagicNumbersSttp.scala | 31 ++++++++++++ src/test/scala/SoftwareMillClientsSuite.scala | 21 +++++++++ src/test/scala/package.scala | 29 ++++++++++++ 11 files changed, 221 insertions(+), 81 deletions(-) create mode 100644 src/test/scala/Http4sClientsSuite.scala create mode 100644 src/test/scala/MagicNumbersBlaze.scala create mode 100644 src/test/scala/MagicNumbersEmber.scala create mode 100644 src/test/scala/MagicNumbersJdkHttp.scala create mode 100644 src/test/scala/MagicNumbersSttp.scala create mode 100644 src/test/scala/SoftwareMillClientsSuite.scala create mode 100644 src/test/scala/package.scala diff --git a/src/main/scala/BlazeClientTest.scala b/src/main/scala/BlazeClientTest.scala index 4394cb4..2cdd561 100644 --- a/src/main/scala/BlazeClientTest.scala +++ b/src/main/scala/BlazeClientTest.scala @@ -10,28 +10,6 @@ import org.http4s.implicits._ import scala.concurrent.ExecutionContext import scala.concurrent.duration._ - - // Numbers below vary on different computers - // In my case, if the request payload size is 65398 or greater - // AND response payload size is 81161 or greater - // then I get an EOF exception in some but not all cases - // If however either of these payload sizes is lower then - // EOF exception doesn't occur, even if running for an extended period - - // broken on first request - val appTime = 5.seconds - val requestPayloadSize = 65398 - val responsePayloadSize = 81161 - - // can hold for 5 seconds, but broken on longer run, like 30 seconds appTime - //val appTime = 30.seconds - //val requestPayloadSize = 65397 - //val responsePayloadSize = 81161 - - // more stable, can hold up to 30 seconds appTime, seen broken on 120 seconds - //val appTime = 30.seconds - //val requestPayloadSize = 65398 - //val responsePayloadSize = 81160 class BlazeClientTest(appTime: FiniteDuration, requestPayloadSize: Int, responsePayloadSize: Int) extends IOApp { val uri = uri"http://localhost:8099" diff --git a/src/main/scala/EmberClientTest.scala b/src/main/scala/EmberClientTest.scala index ce4c154..e302731 100644 --- a/src/main/scala/EmberClientTest.scala +++ b/src/main/scala/EmberClientTest.scala @@ -9,23 +9,6 @@ import org.http4s.implicits._ import scala.concurrent.duration._ - - // Numbers below vary on different computers - // In my case, if the request payload size is 65337 or greater - // then I get an java.io.IOException on the first request - // If however the request payload size is 65336 or lower - // java.io.IOException doesn't occur, even if the response size is 200MB - // while running the test for an extended period - - // broken on first request - val appTime = 5.seconds - val requestPayloadSize = 65337 - val responsePayloadSize = 1 - - // stable for extended period - //val appTime = 120.seconds - //val requestPayloadSize = 65336 - //val responsePayloadSize = 200 * 1000 * 1000 class EmberClientTest(appTime: FiniteDuration, requestPayloadSize: Int, responsePayloadSize: Int) extends IOApp { val uri = uri"http://localhost:8099" diff --git a/src/main/scala/JdkHttpClientTest.scala b/src/main/scala/JdkHttpClientTest.scala index 199a3a8..abdcd18 100644 --- a/src/main/scala/JdkHttpClientTest.scala +++ b/src/main/scala/JdkHttpClientTest.scala @@ -9,26 +9,6 @@ import org.http4s.implicits._ import scala.concurrent.duration._ - // Numbers below may vary on different computers - // In my case, if the request payload size is 523329 or greater - // OR response payload size is 65417 or greater - // then I get a java.io.IOException: fixed content-length: 65416, bytes received: 49032 - // in some but not all cases. - // If however both of these payload sizes are lower then - // fixed content-length exception doesn't occur, even if running for an extended period. - // Yet in some rare cases I get a java.io.IOException: HTTP/1.1 header parser received no bytes - - // stable - at least for content-length - val requestPayloadSize = 523328 // it's 8 times 65416 !? - val responsePayloadSize = 65416 // it's 8 times 8177 !? - - // broken - yet fairly stable - // val requestPayloadSize = 523329 - // val responsePayloadSize = 65416 - - // quite broken - // val requestPayloadSize = 523328 - // val responsePayloadSize = 65417 class JdkHttpClientTest(appTime: FiniteDuration, requestPayloadSize: Int, responsePayloadSize: Int) extends IOApp { val uri = uri"http://localhost:8099" diff --git a/src/main/scala/SttpClientTest.scala b/src/main/scala/SttpClientTest.scala index 6df7f36..e7a03fc 100644 --- a/src/main/scala/SttpClientTest.scala +++ b/src/main/scala/SttpClientTest.scala @@ -5,28 +5,6 @@ import sttp.client3._ import scala.concurrent.duration._ - - // Numbers below vary on different computers - // In my case, if the request payload size is 65289 or greater - // AND response payload size is 81161 or greater - // then I get a sttp.client3.SttpClientException$ReadException on the first request - // If however either of these payload sizes is lower then - // ReadException doesn't occur, even if running for an extended period - - // broken at first request - val appTime = 5.seconds - val requestPayloadSize = 65289 - val responsePayloadSize = 81161 - - // can hold for 5 seconds, but broken on longer run, like 30 seconds appTime - //val appTime = 30.seconds - //val requestPayloadSize = 65288 - //val responsePayloadSize = 81161 - - // more stable, can hold up to 30 seconds appTime, seen broken on 120 seconds - //val appTime = 120.seconds - //val requestPayloadSize = 65289 - //val responsePayloadSize = 81160 class SttpClientTest(appTime: FiniteDuration, requestPayloadSize: Int, responsePayloadSize: Int) extends IOApp { val uri = uri"http://localhost:8099" diff --git a/src/test/scala/Http4sClientsSuite.scala b/src/test/scala/Http4sClientsSuite.scala new file mode 100644 index 0000000..39a57bd --- /dev/null +++ b/src/test/scala/Http4sClientsSuite.scala @@ -0,0 +1,47 @@ +import constants._ +import org.scalatest.featurespec.AnyFeatureSpec +import org.scalatest.matchers.must.Matchers + +class Http4sClientsSuite extends AnyFeatureSpec with Matchers { + + Feature("Blaze client") { + Scenario("Post request small payload size") { + val app = new BlazeClientTest(short, `1kB`, `1kB`) + app.run(List()).unsafeRunSync().code mustBe 0 + } + + Scenario("Post request large payload size") { + val app = new BlazeClientTest(short, `1MB`, `1MB`) + app.run(List()).unsafeRunSync().code mustBe 0 + } + } + + Feature("Ember client") { + Scenario("Post request small payload size") { + val app = new EmberClientTest(short, `1kB`, `1kB`) + app.run(List()).unsafeRunSync().code mustBe 0 + } + + Scenario("Post request large payload size") { + val app = new EmberClientTest(short, `1MB`, `1MB`) + app.run(List()).unsafeRunSync().code mustBe 0 + } + } + + Feature("JDK Http client") { + Scenario("Post request small payload size") { + val app = new JdkHttpClientTest(short, `1kB`, `1kB`) + app.run(List()).unsafeRunSync().code mustBe 0 + } + + Scenario("Post request large payload size") { + val app = new JdkHttpClientTest(short, `1MB`, `1MB`) + app.run(List()).unsafeRunSync().code mustBe 0 + } + + Scenario("Post request large payload size for an extended period") { + val app = new JdkHttpClientTest(long, `1MB`, `1MB`) + app.run(List()).unsafeRunSync().code mustBe 0 + } + } +} diff --git a/src/test/scala/MagicNumbersBlaze.scala b/src/test/scala/MagicNumbersBlaze.scala new file mode 100644 index 0000000..bb40db9 --- /dev/null +++ b/src/test/scala/MagicNumbersBlaze.scala @@ -0,0 +1,31 @@ +import constants._ +import constants.blaze._ +import org.scalatest.featurespec.AnyFeatureSpec +import org.scalatest.matchers.must.Matchers + +class MagicNumbersBlaze extends AnyFeatureSpec with Matchers { + + Feature("Blaze client") { + Scenario("Is always broken at first request, when req = magic AND res = magic") { + val app = new BlazeClientTest(short, magicRequestPayloadSize, magicResponsePayloadSize) + an[org.http4s.InvalidBodyException] should be thrownBy + app.run(List()).unsafeRunSync() + } + + Scenario("Tends to hold for a short period, when req = (magic - 1) AND res = magic") { + val app = new BlazeClientTest(short, magicRequestPayloadSize - 1, magicResponsePayloadSize) + app.run(List()).unsafeRunSync().code mustBe 0 + } + + Scenario("Tends to break on a longer run, when req = (magic - 1) AND res = magic") { + val app = new BlazeClientTest(long, magicRequestPayloadSize - 1, magicResponsePayloadSize) + an[org.http4s.InvalidBodyException] should be thrownBy + app.run(List()).unsafeRunSync() + } + + Scenario("Tends to hold on a longer run, when req = magic AND res = (magic - 1)") { + val app = new BlazeClientTest(long, magicRequestPayloadSize, magicResponsePayloadSize - 1) + app.run(List()).unsafeRunSync().code mustBe 0 + } + } +} diff --git a/src/test/scala/MagicNumbersEmber.scala b/src/test/scala/MagicNumbersEmber.scala new file mode 100644 index 0000000..df96cd5 --- /dev/null +++ b/src/test/scala/MagicNumbersEmber.scala @@ -0,0 +1,32 @@ +import constants._ +import constants.ember._ +import org.scalatest.featurespec.AnyFeatureSpec +import org.scalatest.matchers.must.Matchers + +class MagicNumbersEmber extends AnyFeatureSpec with Matchers { + + Feature("Ember client") { + Scenario("Is always broken at first request, when req = magic even if res is large") { + val app = new EmberClientTest(short, magicRequestPayloadSize, `1MB`) + an[java.io.IOException] should be thrownBy + app.run(List()).unsafeRunSync() + } + + Scenario("Is always broken at first request, when req = magic even if res is small") { + val app = new EmberClientTest(short, magicRequestPayloadSize, `1kB`) + an[java.io.IOException] should be thrownBy + app.run(List()).unsafeRunSync() + } + + Scenario("Tends to hold for short period, when req = (magic - 1) even if res is large") { + val app = new EmberClientTest(short, magicRequestPayloadSize - 1, `1MB`) + app.run(List()).unsafeRunSync().code mustBe 0 + } + + Scenario("Tends to break for longer period, when req = (magic - 1) and res is large") { + val app = new EmberClientTest(long, magicRequestPayloadSize - 1, `1MB`) + an[java.io.IOException] should be thrownBy + app.run(List()).unsafeRunSync() + } + } +} diff --git a/src/test/scala/MagicNumbersJdkHttp.scala b/src/test/scala/MagicNumbersJdkHttp.scala new file mode 100644 index 0000000..389e0b4 --- /dev/null +++ b/src/test/scala/MagicNumbersJdkHttp.scala @@ -0,0 +1,30 @@ +import constants._ +import constants.jdkhttp._ +import org.scalatest.featurespec.AnyFeatureSpec +import org.scalatest.matchers.must.Matchers + +class MagicNumbersJdkHttp extends AnyFeatureSpec with Matchers { + + Feature("JdkHttp client") { + Scenario("Is always broken at first request, when req = magic AND res = magic") { + val app = new JdkHttpClientTest(short, magicRequestPayloadSize, magicResponsePayloadSize) + an[java.io.IOException] should be thrownBy + app.run(List()).unsafeRunSync() + } + + Scenario("Tends to hold on a longer run, when req = magic while res is small") { + val app = new JdkHttpClientTest(long, magicRequestPayloadSize, `1kB`) + app.run(List()).unsafeRunSync().code mustBe 0 + } + + Scenario("Tends to hold on a longer run, when req is small while res = magic") { + val app = new JdkHttpClientTest(long, `1kB`, magicResponsePayloadSize) + app.run(List()).unsafeRunSync().code mustBe 0 + } + + Scenario("Tends to hold on a longer run, when req = (magic - 1) AND res = (magic - 1)") { + val app = new JdkHttpClientTest(long, magicRequestPayloadSize - 1, magicResponsePayloadSize - 1) + app.run(List()).unsafeRunSync().code mustBe 0 + } + } +} diff --git a/src/test/scala/MagicNumbersSttp.scala b/src/test/scala/MagicNumbersSttp.scala new file mode 100644 index 0000000..ff95679 --- /dev/null +++ b/src/test/scala/MagicNumbersSttp.scala @@ -0,0 +1,31 @@ +import constants.sttp._ +import constants.{long, short} +import org.scalatest.featurespec.AnyFeatureSpec +import org.scalatest.matchers.must.Matchers + +class MagicNumbersSttp extends AnyFeatureSpec with Matchers { + + Feature("Sttp client") { + Scenario("Is always broken at first request, when req = magic AND res = magic") { + val app = new SttpClientTest(short, magicRequestPayloadSize, magicResponsePayloadSize) + an[sttp.client3.SttpClientException.ReadException] should be thrownBy + app.run(List()).unsafeRunSync() + } + + Scenario("Tends to hold for a short period, when req = (magic - 1) AND res = magic") { + val app = new SttpClientTest(short, magicRequestPayloadSize - 1, magicResponsePayloadSize) + app.run(List()).unsafeRunSync().code mustBe 0 + } + + Scenario("Tends to break on a longer run, when req = (magic - 1) AND res = magic") { + val app = new SttpClientTest(long, magicRequestPayloadSize - 1, magicResponsePayloadSize) + an[sttp.client3.SttpClientException.ReadException] should be thrownBy + app.run(List()).unsafeRunSync() + } + + Scenario("Tends to hold on a longer run, when req = magic AND res = (magic - 1)") { + val app = new SttpClientTest(long, magicRequestPayloadSize, magicResponsePayloadSize - 1) + app.run(List()).unsafeRunSync().code mustBe 0 + } + } +} diff --git a/src/test/scala/SoftwareMillClientsSuite.scala b/src/test/scala/SoftwareMillClientsSuite.scala new file mode 100644 index 0000000..971458e --- /dev/null +++ b/src/test/scala/SoftwareMillClientsSuite.scala @@ -0,0 +1,21 @@ +import constants._ +import org.scalatest.featurespec.AnyFeatureSpec +import org.scalatest.matchers.must.Matchers + +class SoftwareMillClientsSuite extends AnyFeatureSpec with Matchers { + + Feature("Sttp client") { + Scenario("Post request small payload size") { + val app = new SttpClientTest(short, `1kB`, `1kB`) + app.run(List()).unsafeRunSync().code mustBe 0 + } + + Scenario("Post request large payload size") { + val app = new SttpClientTest(short, `1MB`, `1MB`) + app.run(List()).unsafeRunSync().code mustBe 0 + } + } + +} + + diff --git a/src/test/scala/package.scala b/src/test/scala/package.scala new file mode 100644 index 0000000..52df59b --- /dev/null +++ b/src/test/scala/package.scala @@ -0,0 +1,29 @@ +import scala.concurrent.duration._ + +package object constants { + val `1kB` = 1000 + val `1MB` = 1000 * 1000 + val `20MB` = 20 * 1000 * 1000 + val long = 30.seconds + val short = 2.seconds + + // Numbers below may vary on different computers + object blaze { + val magicRequestPayloadSize = 65398 + val magicResponsePayloadSize = 81161 + } + + object ember { + val magicRequestPayloadSize = 65337 + } + + object jdkhttp { + val magicRequestPayloadSize = 8 * 65416 + 1 // 523329 + val magicResponsePayloadSize = 8 * 8177 + 1 // 65417 + } + + object sttp { + val magicRequestPayloadSize = 65289 + val magicResponsePayloadSize = 81161 + } +} From 5397e478f495927b9ee211ff4f76b923859087df Mon Sep 17 00:00:00 2001 From: slve Date: Sun, 14 Feb 2021 09:50:47 +0000 Subject: [PATCH 4/8] Upgrade http4s to 0.21.19 --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 9b8543b..76addfa 100644 --- a/build.sbt +++ b/build.sbt @@ -16,4 +16,4 @@ libraryDependencies ++= Seq( "org.http4s" %% "http4s-blaze-server", "org.http4s" %% "http4s-dsl", "org.http4s" %% "http4s-ember-client" -).map(_ % "0.21.18") +).map(_ % "0.21.19") From 7bea3bc3d6d57fa31ab2d3274399f4ac51e81b13 Mon Sep 17 00:00:00 2001 From: slve Date: Sun, 14 Feb 2021 10:01:24 +0000 Subject: [PATCH 5/8] Clean up unused variable --- src/main/scala/EmberClientTest.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/scala/EmberClientTest.scala b/src/main/scala/EmberClientTest.scala index e302731..86c440a 100644 --- a/src/main/scala/EmberClientTest.scala +++ b/src/main/scala/EmberClientTest.scala @@ -16,7 +16,6 @@ class EmberClientTest(appTime: FiniteDuration, requestPayloadSize: Int, response val req = Request[IO](POST, uri).withEntity(body) val response = "x" * responsePayloadSize - var i = 0 override def run(args: List[String]): IO[ExitCode] = { def request(client: Client[IO]): Stream[IO, String] = client.stream(req).flatMap(_.bodyText) From a6be6a162f3a28143b81a7329306e449f79235ed Mon Sep 17 00:00:00 2001 From: slve Date: Sun, 14 Feb 2021 16:23:51 +0000 Subject: [PATCH 6/8] Add OkHttp client and acceptance test --- build.sbt | 3 ++- src/main/scala/OkHttpClientTest.scala | 35 +++++++++++++++++++++++++ src/test/scala/Http4sClientsSuite.scala | 12 +++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 src/main/scala/OkHttpClientTest.scala diff --git a/build.sbt b/build.sbt index 76addfa..e6fd049 100644 --- a/build.sbt +++ b/build.sbt @@ -15,5 +15,6 @@ libraryDependencies ++= Seq( "org.http4s" %% "http4s-blaze-client", "org.http4s" %% "http4s-blaze-server", "org.http4s" %% "http4s-dsl", - "org.http4s" %% "http4s-ember-client" + "org.http4s" %% "http4s-ember-client", + "org.http4s" %% "http4s-okhttp-client" ).map(_ % "0.21.19") diff --git a/src/main/scala/OkHttpClientTest.scala b/src/main/scala/OkHttpClientTest.scala new file mode 100644 index 0000000..43b6627 --- /dev/null +++ b/src/main/scala/OkHttpClientTest.scala @@ -0,0 +1,35 @@ +import cats.effect.{Blocker, ExitCode, IO, IOApp, Resource} +import fs2.Stream +import helpers.requestStream +import org.http4s._ +import org.http4s.client.Client +import org.http4s.dsl.io._ +import org.http4s.client.okhttp.OkHttpBuilder +import org.http4s.implicits._ + +import scala.concurrent.ExecutionContext +import scala.concurrent.duration._ + +class OkHttpClientTest(appTime: FiniteDuration, requestPayloadSize: Int, responsePayloadSize: Int) extends IOApp { + + val uri = uri"http://localhost:8099" + val body = "x" * requestPayloadSize + val req = Request[IO](POST, uri).withEntity(body) + val response = "x" * responsePayloadSize + + override def run(args: List[String]): IO[ExitCode] = { + def request(client: Client[IO]): Stream[IO, String] = client.stream(req).flatMap(_.bodyText) + + val simpleClient: Resource[IO, OkHttpBuilder[IO]] = { + val blocker = Blocker.liftExecutionContext(ExecutionContext.global) + OkHttpBuilder.withDefaultClient[IO](blocker) + } + + simpleClient.use { client => + client.resource.use { c => + new Server(requestStream(request(c), appTime), appTime, response).run(List()) + } + } + } + +} diff --git a/src/test/scala/Http4sClientsSuite.scala b/src/test/scala/Http4sClientsSuite.scala index 39a57bd..cd33dd4 100644 --- a/src/test/scala/Http4sClientsSuite.scala +++ b/src/test/scala/Http4sClientsSuite.scala @@ -44,4 +44,16 @@ class Http4sClientsSuite extends AnyFeatureSpec with Matchers { app.run(List()).unsafeRunSync().code mustBe 0 } } + + Feature("OkHttp client") { + Scenario("Post request small payload size") { + val app = new OkHttpClientTest(short, `1kB`, `1kB`) + app.run(List()).unsafeRunSync().code mustBe 0 + } + + Scenario("Post request large payload size") { + val app = new OkHttpClientTest(short, `1MB`, `1MB`) + app.run(List()).unsafeRunSync().code mustBe 0 + } + } } From f9b3a3a7a76d3613963b070e8145a420415f5fec Mon Sep 17 00:00:00 2001 From: slve Date: Sun, 14 Feb 2021 18:50:18 +0000 Subject: [PATCH 7/8] Add Jetty client and acceptance test --- build.sbt | 1 + src/main/scala/JettyClientTest.scala | 31 +++++++++++++++++++++++++ src/test/scala/Http4sClientsSuite.scala | 12 ++++++++++ 3 files changed, 44 insertions(+) create mode 100644 src/main/scala/JettyClientTest.scala diff --git a/build.sbt b/build.sbt index e6fd049..947c866 100644 --- a/build.sbt +++ b/build.sbt @@ -16,5 +16,6 @@ libraryDependencies ++= Seq( "org.http4s" %% "http4s-blaze-server", "org.http4s" %% "http4s-dsl", "org.http4s" %% "http4s-ember-client", + "org.http4s" %% "http4s-jetty-client", "org.http4s" %% "http4s-okhttp-client" ).map(_ % "0.21.19") diff --git a/src/main/scala/JettyClientTest.scala b/src/main/scala/JettyClientTest.scala new file mode 100644 index 0000000..aa706a0 --- /dev/null +++ b/src/main/scala/JettyClientTest.scala @@ -0,0 +1,31 @@ +import cats.effect.{ExitCode, IO, IOApp, Resource} +import fs2.Stream +import helpers.requestStream +import org.http4s._ +import org.http4s.client.Client +import org.http4s.client.jetty.JettyClient +import org.http4s.dsl.io._ +import org.http4s.implicits._ + +import scala.concurrent.duration._ + +class JettyClientTest(appTime: FiniteDuration, requestPayloadSize: Int, responsePayloadSize: Int) extends IOApp { + + val uri = uri"http://localhost:8099" + val body = "x" * requestPayloadSize + val req = Request[IO](POST, uri).withEntity(body) + val response = "x" * responsePayloadSize + + override def run(args: List[String]): IO[ExitCode] = { + def request(client: Client[IO]): Stream[IO, String] = client.stream(req).flatMap(_.bodyText) + + val simpleClient: Resource[IO, Client[IO]] = { + JettyClient.resource[IO]() + } + + simpleClient.use { client => + new Server(requestStream(request(client), appTime), appTime, response).run(List()) + } + } + +} diff --git a/src/test/scala/Http4sClientsSuite.scala b/src/test/scala/Http4sClientsSuite.scala index cd33dd4..09696bc 100644 --- a/src/test/scala/Http4sClientsSuite.scala +++ b/src/test/scala/Http4sClientsSuite.scala @@ -45,6 +45,18 @@ class Http4sClientsSuite extends AnyFeatureSpec with Matchers { } } + Feature("Jetty client") { + Scenario("Post request small payload size") { + val app = new JettyClientTest(short, `1kB`, `1kB`) + app.run(List()).unsafeRunSync().code mustBe 0 + } + + Scenario("Post request large payload size") { + val app = new JettyClientTest(short, `1MB`, `1MB`) + app.run(List()).unsafeRunSync().code mustBe 0 + } + } + Feature("OkHttp client") { Scenario("Post request small payload size") { val app = new OkHttpClientTest(short, `1kB`, `1kB`) From 31a4e97d1ab93a7e649b9cd6e97908efb2b4a447 Mon Sep 17 00:00:00 2001 From: slve Date: Sun, 14 Feb 2021 19:04:33 +0000 Subject: [PATCH 8/8] Add Async Http client and acceptance test --- build.sbt | 1 + src/main/scala/AsyncHttpClientTest.scala | 31 ++++++++++++++++++++++++ src/test/scala/Http4sClientsSuite.scala | 12 +++++++++ 3 files changed, 44 insertions(+) create mode 100644 src/main/scala/AsyncHttpClientTest.scala diff --git a/build.sbt b/build.sbt index 947c866..a635183 100644 --- a/build.sbt +++ b/build.sbt @@ -15,6 +15,7 @@ libraryDependencies ++= Seq( "org.http4s" %% "http4s-blaze-client", "org.http4s" %% "http4s-blaze-server", "org.http4s" %% "http4s-dsl", + "org.http4s" %% "http4s-async-http-client", "org.http4s" %% "http4s-ember-client", "org.http4s" %% "http4s-jetty-client", "org.http4s" %% "http4s-okhttp-client" diff --git a/src/main/scala/AsyncHttpClientTest.scala b/src/main/scala/AsyncHttpClientTest.scala new file mode 100644 index 0000000..b9b7b64 --- /dev/null +++ b/src/main/scala/AsyncHttpClientTest.scala @@ -0,0 +1,31 @@ +import cats.effect.{ExitCode, IO, IOApp, Resource} +import fs2.Stream +import helpers.requestStream +import org.http4s._ +import org.http4s.client.Client +import org.http4s.client.asynchttpclient.AsyncHttpClient +import org.http4s.dsl.io._ +import org.http4s.implicits._ + +import scala.concurrent.duration._ + +class AsyncHttpClientTest(appTime: FiniteDuration, requestPayloadSize: Int, responsePayloadSize: Int) extends IOApp { + + val uri = uri"http://localhost:8099" + val body = "x" * requestPayloadSize + val req = Request[IO](POST, uri).withEntity(body) + val response = "x" * responsePayloadSize + + override def run(args: List[String]): IO[ExitCode] = { + def request(client: Client[IO]): Stream[IO, String] = client.stream(req).flatMap(_.bodyText) + + val simpleClient: Resource[IO, Client[IO]] = { + AsyncHttpClient.resource[IO]() + } + + simpleClient.use { client => + new Server(requestStream(request(client), appTime), appTime, response).run(List()) + } + } + +} diff --git a/src/test/scala/Http4sClientsSuite.scala b/src/test/scala/Http4sClientsSuite.scala index 09696bc..1a03639 100644 --- a/src/test/scala/Http4sClientsSuite.scala +++ b/src/test/scala/Http4sClientsSuite.scala @@ -4,6 +4,18 @@ import org.scalatest.matchers.must.Matchers class Http4sClientsSuite extends AnyFeatureSpec with Matchers { + Feature("Async Http client") { + Scenario("Post request small payload size") { + val app = new AsyncHttpClientTest(short, `1kB`, `1kB`) + app.run(List()).unsafeRunSync().code mustBe 0 + } + + Scenario("Post request large payload size") { + val app = new AsyncHttpClientTest(short, `1MB`, `1MB`) + app.run(List()).unsafeRunSync().code mustBe 0 + } + } + Feature("Blaze client") { Scenario("Post request small payload size") { val app = new BlazeClientTest(short, `1kB`, `1kB`)