Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow creating a body from Array[Byte] #2581

Merged
merged 3 commits into from
Jan 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,39 @@ import sttp.client3.{HttpURLConnectionBackend, UriContext, basicRequest}
@BenchmarkMode(Array(Mode.Throughput))
@OutputTimeUnit(TimeUnit.SECONDS)
class ServerInboundHandlerBenchmark {
private val random = scala.util.Random
random.setSeed(42)
private val largeString = random.alphanumeric.take(100000).mkString

private val baseUrl = "http://localhost:8080"
private val headers = Headers(Header.ContentType(MediaType.text.`plain`).untyped)

private val arrayEndpoint = "array"
private val arrayResponse = ZIO.succeed(
Response(
status = Status.Ok,
headers = headers,
body = Body.fromArray(largeString.getBytes),
),
)
private val arrayRoute = Route.route(Method.GET / arrayEndpoint)(handler(arrayResponse))
private val arrayRequest = basicRequest.get(uri"$baseUrl/$arrayEndpoint")

private val chunkEndpoint = "chunk"
private val chunkResponse = ZIO.succeed(
Response(
status = Status.Ok,
headers = headers,
body = Body.fromChunk(Chunk.fromArray(largeString.getBytes)),
),
)
private val chunkRoute = Route.route(Method.GET / chunkEndpoint)(handler(chunkResponse))
private val chunkRequest = basicRequest.get(uri"$baseUrl/$chunkEndpoint")

private val testResponse = ZIO.succeed(Response.text("Hello World!"))
private val testEndPoint = "test"
private val testRoute = Route.route(Method.GET / testEndPoint)(handler(testResponse))
private val testUrl = s"http://localhost:8080/$testEndPoint"
private val testUrl = s"$baseUrl/$testEndPoint"
private val testRequest = basicRequest.get(uri"$testUrl")

private val shutdownResponse = Response.text("shutting down")
Expand All @@ -27,7 +56,8 @@ class ServerInboundHandlerBenchmark {

private def shutdownRoute(shutdownSignal: Promise[Nothing, Unit]) =
Route.route(Method.GET / shutdownEndpoint)(handler(shutdownSignal.succeed(()).as(shutdownResponse)))
private def http(shutdownSignal: Promise[Nothing, Unit]) = Routes(testRoute, shutdownRoute(shutdownSignal)).toHttpApp
private def http(shutdownSignal: Promise[Nothing, Unit]) =
Routes(testRoute, arrayRoute, chunkRoute, shutdownRoute(shutdownSignal)).toHttpApp

@Setup(Level.Trial)
def setup(): Unit = {
Expand Down Expand Up @@ -58,7 +88,21 @@ class ServerInboundHandlerBenchmark {
}

@Benchmark
def benchmarkApp(): Unit = {
def benchmarkLargeArray(): Unit = {
val statusCode = arrayRequest.send(backend).code
if (!statusCode.isSuccess)
throw new RuntimeException(s"Received unexpected status code ${statusCode.code}")
}

@Benchmark
def benchmarkLargeChunk(): Unit = {
val statusCode = chunkRequest.send(backend).code
if (!statusCode.isSuccess)
throw new RuntimeException(s"Received unexpected status code ${statusCode.code}")
}

@Benchmark
def benchmarkSimple(): Unit = {
val statusCode = testRequest.send(backend).code
if (!statusCode.isSuccess)
throw new RuntimeException(s"Received unexpected status code ${statusCode.code}")
Expand Down
36 changes: 36 additions & 0 deletions zio-http/src/main/scala/zio/http/Body.scala
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,13 @@ object Body {
*/
def fromChunk(data: Chunk[Byte]): Body = ChunkBody(data)

/**
* Constructs a [[zio.http.Body]] from an array of bytes.
*
* WARNING: The array must not be mutated after creating the body.
*/
def fromArray(data: Array[Byte]): Body = ArrayBody(data)

/**
* Constructs a [[zio.http.Body]] from the contents of a file.
*/
Expand Down Expand Up @@ -293,6 +300,35 @@ object Body {
copy(mediaType = Some(newMediaType), boundary = boundary.orElse(Some(newBoundary)))
}

private[zio] final case class ArrayBody(
data: Array[Byte],
override val mediaType: Option[MediaType] = None,
override val boundary: Option[Boundary] = None,
) extends Body
with UnsafeWriteable
with UnsafeBytes { self =>

override def asArray(implicit trace: Trace): Task[Array[Byte]] = ZIO.succeed(data)

override def isComplete: Boolean = true

override def isEmpty: Boolean = data.isEmpty

override def asChunk(implicit trace: Trace): Task[Chunk[Byte]] = ZIO.succeed(Chunk.fromArray(data))

override def asStream(implicit trace: Trace): ZStream[Any, Throwable, Byte] =
ZStream.unwrap(asChunk.map(ZStream.fromChunk(_)))

override def toString(): String = s"Body.fromArray($data)"

override private[zio] def unsafeAsArray(implicit unsafe: Unsafe): Array[Byte] = data

override def contentType(newMediaType: MediaType): Body = copy(mediaType = Some(newMediaType))

override def contentType(newMediaType: MediaType, newBoundary: Boundary): Body =
copy(mediaType = Some(newMediaType), boundary = boundary.orElse(Some(newBoundary)))
}

private[zio] final case class FileBody(
val file: java.io.File,
chunkSize: Int = 1024 * 4,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,9 @@ object NettyBodyWriter {
}
},
)
case ArrayBody(data, _, _) =>
ctx.writeAndFlush(Unpooled.wrappedBuffer(data))
None
case ChunkBody(data, _, _) =>
ctx.write(Unpooled.wrappedBuffer(data.toArray))
ctx.flush()
Expand Down
2 changes: 2 additions & 0 deletions zio-http/src/test/scala/zio/http/internal/HttpGen.scala
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ object HttpGen {
),
Body.fromString(list.mkString("")),
Body.fromChunk(Chunk.fromArray(list.mkString("").getBytes())),
Body.fromArray(list.mkString("").getBytes()),
Body.empty,
),
)
Expand Down Expand Up @@ -139,6 +140,7 @@ object HttpGen {
),
Body.fromString(list.mkString("")),
Body.fromChunk(Chunk.fromArray(list.mkString("").getBytes())),
Body.fromArray(list.mkString("").getBytes()),
),
)
} yield cnt
Expand Down
Loading