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

Micro-optimizations and improved IOException error handling #2638

Merged
merged 2 commits into from
Jan 20, 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
@@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love this change 👍

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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just for me: Is this an optional hint? Might the compiler create a switch without it? Might the compiler not create a switch with it?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might the compiler create a switch without it

Yes

Might the compiler not create a switch with it

No

This is very similar to tailrec; if the compiler can create a lookupswitch or tableswitch, it will do it even without the annotation. However, if the annotation is present, then it'll raise an error if a lookupswitch / tableswitch cannot be created from the expression.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thx! 👍

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
Loading