Skip to content

Commit

Permalink
Micro-optimizations and improved IOException error handling (zio#2638)
Browse files Browse the repository at this point in the history
* Micro-optimizations and fixes

* Fix `CharSequenceExtensions.compare` method
  • Loading branch information
kyri-petrou authored Jan 20, 2024
1 parent 842af96 commit 49d4aa4
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 158 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package zio.http.netty.benchmarks

import java.util.concurrent.TimeUnit

import zio.http.internal.{CaseMode, CharSequenceExtensions}
import zio.http.netty.model.Conversions

import io.netty.handler.codec.http.DefaultHttpHeaders
import org.openjdk.jmh.annotations._

@State(Scope.Thread)
@BenchmarkMode(Array(Mode.AverageTime))
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Fork(1)
@Warmup(iterations = 3, time = 3)
@Measurement(iterations = 3, time = 3)
class UtilBenchmark {

private val nettyHeaders =
new DefaultHttpHeaders()
.add("Content-Type", "application/json; charset=utf-8")
.add("Content-Length", "100")
.add("Content-Encoding", "gzip")
.add("Accept", "application/json")
.add("Accept-Encoding", "gzip, deflate, br")
.add("Accept-Language", "en-US,en;q=0.9")
.add("Connection", "keep-alive")
.add("Host", "localhost:8080")
.add("Origin", "http://localhost:8080")
.add("Referer", "http://localhost:8080/")
.add("Sec-Fetch-Dest", "empty")
.add("Sec-Fetch-Mode", "cors")
.add("Sec-Fetch-Site", "same-origin")
.add(
"User-Agent",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/88.0.4324.96 Chrome/88.0.4324.96 Safari/537.36",
)

private val headers = Conversions.headersFromNetty(nettyHeaders)

@Benchmark
def benchmarkEqualsInsensitive(): Unit = {
CharSequenceExtensions.equals(
"application/json; charset=utf-8",
"Application/json; charset=utf-8",
caseMode = CaseMode.Insensitive,
)
()
}

@Benchmark
// For comparison with benchmarkEqualsInsensitive
def benchmarkEqualsInsensitiveJava(): Unit = {
val _ = "application/json; charset=utf-8".equalsIgnoreCase("application/json; Charset=utf-8")
()
}

@Benchmark
def benchmarkHeaderGetUnsafe(): Unit = {
headers.getUnsafe("sec-fetch-site")
()
}

@Benchmark
def benchmarkStatusToNetty(): Unit = {
Conversions.statusToNetty(zio.http.Status.InternalServerError)
()
}

}
154 changes: 16 additions & 138 deletions zio-http/jvm/src/main/scala/zio/http/netty/model/Conversions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import zio.stacktracer.TracingImplicits.disableAutoTrace

import zio.http.Server.Config.CompressionOptions
import zio.http._
import zio.http.internal.{CaseMode, CharSequenceExtensions}

import io.netty.handler.codec.compression.{DeflateOptions, StandardCompressionOptions}
import io.netty.handler.codec.http._
Expand Down Expand Up @@ -60,9 +59,9 @@ private[netty] object Conversions {

def headersToNetty(headers: Headers): HttpHeaders =
headers match {
case Headers.FromIterable(_) => encodeHeaderListToNetty(headers.toList)
case Headers.FromIterable(_) => encodeHeaderListToNetty(headers)
case Headers.Native(value, _, _) => value.asInstanceOf[HttpHeaders]
case Headers.Concat(_, _) => encodeHeaderListToNetty(headers.toList)
case Headers.Concat(_, _) => encodeHeaderListToNetty(headers)
case Headers.Empty => new DefaultHttpHeaders()
}

Expand All @@ -82,152 +81,31 @@ private[netty] object Conversions {
Headers.Native(
headers,
(headers: HttpHeaders) => nettyHeadersIterator(headers),
(headers: HttpHeaders, key: CharSequence) => {
val iterator = headers.iteratorCharSequence()
var result: String = null
while (iterator.hasNext && (result eq null)) {
val entry = iterator.next()
if (CharSequenceExtensions.equals(entry.getKey, key, CaseMode.Insensitive)) {
result = entry.getValue.toString
}
}

result
},
// NOTE: Netty's headers.get is case-insensitive
(headers: HttpHeaders, key: CharSequence) => headers.get(key),
)

private def encodeHeaderListToNetty(headers: Iterable[Header]): HttpHeaders = {
val nettyHeaders = new DefaultHttpHeaders(true)
for (header <- headers) {
if (header.headerName == Header.SetCookie.name) {
nettyHeaders.add(header.headerName, header.renderedValueAsCharSequence)
val nettyHeaders = new DefaultHttpHeaders(true)
val setCookieName = Header.SetCookie.name
val iter = headers.iterator
while (iter.hasNext) {
val header = iter.next()
val name = header.headerName
if (name == setCookieName) {
nettyHeaders.add(name, header.renderedValueAsCharSequence)
} else {
nettyHeaders.set(header.headerName, header.renderedValueAsCharSequence)
nettyHeaders.set(name, header.renderedValueAsCharSequence)
}
}
nettyHeaders
}

def statusToNetty(status: Status): HttpResponseStatus =
status match {
case Status.Continue => HttpResponseStatus.CONTINUE // 100
case Status.SwitchingProtocols => HttpResponseStatus.SWITCHING_PROTOCOLS // 101
case Status.Processing => HttpResponseStatus.PROCESSING // 102
case Status.Ok => HttpResponseStatus.OK // 200
case Status.Created => HttpResponseStatus.CREATED // 201
case Status.Accepted => HttpResponseStatus.ACCEPTED // 202
case Status.NonAuthoritativeInformation => HttpResponseStatus.NON_AUTHORITATIVE_INFORMATION // 203
case Status.NoContent => HttpResponseStatus.NO_CONTENT // 204
case Status.ResetContent => HttpResponseStatus.RESET_CONTENT // 205
case Status.PartialContent => HttpResponseStatus.PARTIAL_CONTENT // 206
case Status.MultiStatus => HttpResponseStatus.MULTI_STATUS // 207
case Status.MultipleChoices => HttpResponseStatus.MULTIPLE_CHOICES // 300
case Status.MovedPermanently => HttpResponseStatus.MOVED_PERMANENTLY // 301
case Status.Found => HttpResponseStatus.FOUND // 302
case Status.SeeOther => HttpResponseStatus.SEE_OTHER // 303
case Status.NotModified => HttpResponseStatus.NOT_MODIFIED // 304
case Status.UseProxy => HttpResponseStatus.USE_PROXY // 305
case Status.TemporaryRedirect => HttpResponseStatus.TEMPORARY_REDIRECT // 307
case Status.PermanentRedirect => HttpResponseStatus.PERMANENT_REDIRECT // 308
case Status.BadRequest => HttpResponseStatus.BAD_REQUEST // 400
case Status.Unauthorized => HttpResponseStatus.UNAUTHORIZED // 401
case Status.PaymentRequired => HttpResponseStatus.PAYMENT_REQUIRED // 402
case Status.Forbidden => HttpResponseStatus.FORBIDDEN // 403
case Status.NotFound => HttpResponseStatus.NOT_FOUND // 404
case Status.MethodNotAllowed => HttpResponseStatus.METHOD_NOT_ALLOWED // 405
case Status.NotAcceptable => HttpResponseStatus.NOT_ACCEPTABLE // 406
case Status.ProxyAuthenticationRequired => HttpResponseStatus.PROXY_AUTHENTICATION_REQUIRED // 407
case Status.RequestTimeout => HttpResponseStatus.REQUEST_TIMEOUT // 408
case Status.Conflict => HttpResponseStatus.CONFLICT // 409
case Status.Gone => HttpResponseStatus.GONE // 410
case Status.LengthRequired => HttpResponseStatus.LENGTH_REQUIRED // 411
case Status.PreconditionFailed => HttpResponseStatus.PRECONDITION_FAILED // 412
case Status.RequestEntityTooLarge => HttpResponseStatus.REQUEST_ENTITY_TOO_LARGE // 413
case Status.RequestUriTooLong => HttpResponseStatus.REQUEST_URI_TOO_LONG // 414
case Status.UnsupportedMediaType => HttpResponseStatus.UNSUPPORTED_MEDIA_TYPE // 415
case Status.RequestedRangeNotSatisfiable => HttpResponseStatus.REQUESTED_RANGE_NOT_SATISFIABLE // 416
case Status.ExpectationFailed => HttpResponseStatus.EXPECTATION_FAILED // 417
case Status.MisdirectedRequest => HttpResponseStatus.MISDIRECTED_REQUEST // 421
case Status.UnprocessableEntity => HttpResponseStatus.UNPROCESSABLE_ENTITY // 422
case Status.Locked => HttpResponseStatus.LOCKED // 423
case Status.FailedDependency => HttpResponseStatus.FAILED_DEPENDENCY // 424
case Status.UnorderedCollection => HttpResponseStatus.UNORDERED_COLLECTION // 425
case Status.UpgradeRequired => HttpResponseStatus.UPGRADE_REQUIRED // 426
case Status.PreconditionRequired => HttpResponseStatus.PRECONDITION_REQUIRED // 428
case Status.TooManyRequests => HttpResponseStatus.TOO_MANY_REQUESTS // 429
case Status.RequestHeaderFieldsTooLarge => HttpResponseStatus.REQUEST_HEADER_FIELDS_TOO_LARGE // 431
case Status.InternalServerError => HttpResponseStatus.INTERNAL_SERVER_ERROR // 500
case Status.NotImplemented => HttpResponseStatus.NOT_IMPLEMENTED // 501
case Status.BadGateway => HttpResponseStatus.BAD_GATEWAY // 502
case Status.ServiceUnavailable => HttpResponseStatus.SERVICE_UNAVAILABLE // 503
case Status.GatewayTimeout => HttpResponseStatus.GATEWAY_TIMEOUT // 504
case Status.HttpVersionNotSupported => HttpResponseStatus.HTTP_VERSION_NOT_SUPPORTED // 505
case Status.VariantAlsoNegotiates => HttpResponseStatus.VARIANT_ALSO_NEGOTIATES // 506
case Status.InsufficientStorage => HttpResponseStatus.INSUFFICIENT_STORAGE // 507
case Status.NotExtended => HttpResponseStatus.NOT_EXTENDED // 510
case Status.NetworkAuthenticationRequired => HttpResponseStatus.NETWORK_AUTHENTICATION_REQUIRED // 511
case Status.Custom(code) => HttpResponseStatus.valueOf(code)
}
HttpResponseStatus.valueOf(status.code)

def statusFromNetty(status: HttpResponseStatus): Status = (status: @unchecked) match {
case HttpResponseStatus.CONTINUE => Status.Continue
case HttpResponseStatus.SWITCHING_PROTOCOLS => Status.SwitchingProtocols
case HttpResponseStatus.PROCESSING => Status.Processing
case HttpResponseStatus.OK => Status.Ok
case HttpResponseStatus.CREATED => Status.Created
case HttpResponseStatus.ACCEPTED => Status.Accepted
case HttpResponseStatus.NON_AUTHORITATIVE_INFORMATION => Status.NonAuthoritativeInformation
case HttpResponseStatus.NO_CONTENT => Status.NoContent
case HttpResponseStatus.RESET_CONTENT => Status.ResetContent
case HttpResponseStatus.PARTIAL_CONTENT => Status.PartialContent
case HttpResponseStatus.MULTI_STATUS => Status.MultiStatus
case HttpResponseStatus.MULTIPLE_CHOICES => Status.MultipleChoices
case HttpResponseStatus.MOVED_PERMANENTLY => Status.MovedPermanently
case HttpResponseStatus.FOUND => Status.Found
case HttpResponseStatus.SEE_OTHER => Status.SeeOther
case HttpResponseStatus.NOT_MODIFIED => Status.NotModified
case HttpResponseStatus.USE_PROXY => Status.UseProxy
case HttpResponseStatus.TEMPORARY_REDIRECT => Status.TemporaryRedirect
case HttpResponseStatus.PERMANENT_REDIRECT => Status.PermanentRedirect
case HttpResponseStatus.BAD_REQUEST => Status.BadRequest
case HttpResponseStatus.UNAUTHORIZED => Status.Unauthorized
case HttpResponseStatus.PAYMENT_REQUIRED => Status.PaymentRequired
case HttpResponseStatus.FORBIDDEN => Status.Forbidden
case HttpResponseStatus.NOT_FOUND => Status.NotFound
case HttpResponseStatus.METHOD_NOT_ALLOWED => Status.MethodNotAllowed
case HttpResponseStatus.NOT_ACCEPTABLE => Status.NotAcceptable
case HttpResponseStatus.PROXY_AUTHENTICATION_REQUIRED => Status.ProxyAuthenticationRequired
case HttpResponseStatus.REQUEST_TIMEOUT => Status.RequestTimeout
case HttpResponseStatus.CONFLICT => Status.Conflict
case HttpResponseStatus.GONE => Status.Gone
case HttpResponseStatus.LENGTH_REQUIRED => Status.LengthRequired
case HttpResponseStatus.PRECONDITION_FAILED => Status.PreconditionFailed
case HttpResponseStatus.REQUEST_ENTITY_TOO_LARGE => Status.RequestEntityTooLarge
case HttpResponseStatus.REQUEST_URI_TOO_LONG => Status.RequestUriTooLong
case HttpResponseStatus.UNSUPPORTED_MEDIA_TYPE => Status.UnsupportedMediaType
case HttpResponseStatus.REQUESTED_RANGE_NOT_SATISFIABLE => Status.RequestedRangeNotSatisfiable
case HttpResponseStatus.EXPECTATION_FAILED => Status.ExpectationFailed
case HttpResponseStatus.MISDIRECTED_REQUEST => Status.MisdirectedRequest
case HttpResponseStatus.UNPROCESSABLE_ENTITY => Status.UnprocessableEntity
case HttpResponseStatus.LOCKED => Status.Locked
case HttpResponseStatus.FAILED_DEPENDENCY => Status.FailedDependency
case HttpResponseStatus.UNORDERED_COLLECTION => Status.UnorderedCollection
case HttpResponseStatus.UPGRADE_REQUIRED => Status.UpgradeRequired
case HttpResponseStatus.PRECONDITION_REQUIRED => Status.PreconditionRequired
case HttpResponseStatus.TOO_MANY_REQUESTS => Status.TooManyRequests
case HttpResponseStatus.REQUEST_HEADER_FIELDS_TOO_LARGE => Status.RequestHeaderFieldsTooLarge
case HttpResponseStatus.INTERNAL_SERVER_ERROR => Status.InternalServerError
case HttpResponseStatus.NOT_IMPLEMENTED => Status.NotImplemented
case HttpResponseStatus.BAD_GATEWAY => Status.BadGateway
case HttpResponseStatus.SERVICE_UNAVAILABLE => Status.ServiceUnavailable
case HttpResponseStatus.GATEWAY_TIMEOUT => Status.GatewayTimeout
case HttpResponseStatus.HTTP_VERSION_NOT_SUPPORTED => Status.HttpVersionNotSupported
case HttpResponseStatus.VARIANT_ALSO_NEGOTIATES => Status.VariantAlsoNegotiates
case HttpResponseStatus.INSUFFICIENT_STORAGE => Status.InsufficientStorage
case HttpResponseStatus.NOT_EXTENDED => Status.NotExtended
case HttpResponseStatus.NETWORK_AUTHENTICATION_REQUIRED => Status.NetworkAuthenticationRequired
case status: HttpResponseStatus => Status.Custom(status.code)
}
def statusFromNetty(status: HttpResponseStatus): Status =
Status.fromInt(status.code)

def schemeToNetty(scheme: Scheme): Option[HttpScheme] = scheme match {
case Scheme.HTTP => Option(HttpScheme.HTTP)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,11 @@ private[zio] final case class ServerInboundHandler(

override def exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable): Unit =
cause match {
case ioe: IOException if ioe.getMessage.startsWith("Connection reset") =>
case t =>
case ioe: IOException if {
val msg = ioe.getMessage
(msg ne null) && msg.contains("Connection reset")
} =>
case t =>
if (app ne null) {
runtime.run(ctx, () => {}) {
// We cannot return the generated response from here, but still calling the handler for its side effect
Expand Down
2 changes: 1 addition & 1 deletion zio-http/shared/src/main/scala/zio/http/Status.scala
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ object Status {
Try(code.toInt).toOption.map(fromInt)

def fromInt(code: Int): Status = {
code match {
(code: @annotation.switch) match {
case 100 => Status.Continue
case 101 => Status.SwitchingProtocols
case 102 => Status.Processing
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ package zio.http.internal
private[http] object CharSequenceExtensions {

def equals(left: CharSequence, right: CharSequence, caseMode: CaseMode = CaseMode.Sensitive): Boolean =
if (left eq right) true else compare(left, right, caseMode) == 0
left.length == right.length && compare(left, right, caseMode) == 0

/**
* Lexicographically compares two `CharSequence`s.
Expand All @@ -35,37 +35,35 @@ private[http] object CharSequenceExtensions {
} else {
val leftLength = left.length
val rightLength = right.length
var result: Int = 0

caseMode match {
case CaseMode.Sensitive =>
var i = 0
while (i < leftLength && i < leftLength && i < rightLength) {
while (i < leftLength && i < rightLength) {
val leftChar = left.charAt(i)
val rightChar = right.charAt(i)
if (leftChar != rightChar) {
result = leftChar - rightChar
i = leftLength
} else {
i += 1
return leftChar - rightChar
}
i += 1
}
case CaseMode.Insensitive =>
var i = 0
while (i < leftLength && i < leftLength && i < rightLength) {
val leftChar = left.charAt(i).toLower
val rightChar = right.charAt(i).toLower
while (i < leftLength && i < rightLength) {
val leftChar = left.charAt(i)
val rightChar = right.charAt(i)
if (leftChar != rightChar) {
result = leftChar - rightChar
i = leftLength
} else {
i += 1
val lLower = leftChar.toLower
val rLower = rightChar.toLower
if (lLower != rLower) {
return lLower - rLower
}
}
i += 1
}
}

if (result != 0) result else leftLength.compare(rightLength)
leftLength.compare(rightLength)
}

}

def hashCode(value: CharSequence): Int = {
Expand Down

0 comments on commit 49d4aa4

Please sign in to comment.