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

Add brotli compression (#2646) #2857

Merged
merged 2 commits into from
May 24, 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
1 change: 1 addition & 0 deletions project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ object Dependencies {
"io.netty" % "netty-transport-native-kqueue" % NettyVersion,
"io.netty" % "netty-transport-native-kqueue" % NettyVersion % Runtime classifier "osx-x86_64",
"io.netty" % "netty-transport-native-kqueue" % NettyVersion % Runtime classifier "osx-aarch_64",
"com.aayushatharva.brotli4j" % "brotli4j" % "1.16.0" % "provided",
Copy link
Collaborator

@kyri-petrou kyri-petrou May 24, 2024

Choose a reason for hiding this comment

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

Very small nit: I believe we should be using optional here instead of provided. A good summary on the semantics from a comment here:

But, as I tried to say, the difference is mainly semantic, i.e. you signal the user that this dependency should come from the container, while for optional dependencies, you signal that you need to explicitly add the necessary dependencies

Unless of course there was some reason for using provided in the first place then please ignore me! 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the hint. I did not know optional

)

val `netty-incubator` =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,11 @@ package zio.http.netty.model

import scala.collection.AbstractIterator

import zio.stacktracer.TracingImplicits.disableAutoTrace

import zio.http.Server.Config.CompressionOptions
import zio.http._

import io.netty.handler.codec.compression.{DeflateOptions, StandardCompressionOptions}
import com.aayushatharva.brotli4j.encoder.Encoder
import io.netty.handler.codec.compression.StandardCompressionOptions
import io.netty.handler.codec.http._
import io.netty.handler.codec.http.websocketx.WebSocketScheme

Expand Down Expand Up @@ -132,14 +131,26 @@ private[netty] object Conversions {
case _ => None
}

def compressionOptionsToNetty(compressionOptions: CompressionOptions): DeflateOptions =
compressionOptions.kind match {
case CompressionOptions.CompressionType.GZip =>
StandardCompressionOptions.gzip(compressionOptions.level, compressionOptions.bits, compressionOptions.mem)
case CompressionOptions.CompressionType.Deflate =>
StandardCompressionOptions.deflate(compressionOptions.level, compressionOptions.bits, compressionOptions.mem)
def compressionOptionsToNetty(
compressionOptions: CompressionOptions,
): io.netty.handler.codec.compression.CompressionOptions =
compressionOptions match {
case CompressionOptions.GZip(cfg) =>
StandardCompressionOptions.gzip(cfg.level, cfg.bits, cfg.mem)
case CompressionOptions.Deflate(cfg) =>
StandardCompressionOptions.deflate(cfg.level, cfg.bits, cfg.mem)
case CompressionOptions.Brotli(cfg) =>
StandardCompressionOptions.brotli(
new Encoder.Parameters().setQuality(cfg.quality).setWindow(cfg.lgwin).setMode(brotliModeToJava(cfg.mode)),
)
}

def brotliModeToJava(brotli: CompressionOptions.Mode): Encoder.Mode = brotli match {
case CompressionOptions.Mode.Font => Encoder.Mode.FONT
case CompressionOptions.Mode.Text => Encoder.Mode.TEXT
case CompressionOptions.Mode.Generic => Encoder.Mode.GENERIC
}

def versionToNetty(version: Version): HttpVersion = version match {
case Version.Http_1_0 => HttpVersion.HTTP_1_0
case Version.Http_1_1 => HttpVersion.HTTP_1_1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ private[zio] final case class ServerInboundHandler(
case Some(cfg) =>
val headers = req.headers()
val headerName = Header.AcceptEncoding.name
cfg.options.exists(opt => headers.containsValue(headerName, opt.kind.name, true))
cfg.options.exists(opt => headers.containsValue(headerName, opt.name, true))
}
}

Expand Down
146 changes: 96 additions & 50 deletions zio-http/shared/src/main/scala/zio/http/Server.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import java.net.{InetAddress, InetSocketAddress}
import java.util.concurrent.atomic._

import zio._
import zio.stacktracer.TracingImplicits.disableAutoTrace

import zio.http.Server.Config.ResponseCompressionConfig

Expand Down Expand Up @@ -248,71 +247,118 @@ object Server extends ServerPlatformSpecific {
ResponseCompressionConfig(0, IndexedSeq(CompressionOptions.gzip(), CompressionOptions.deflate()))
}

/**
* @param level
* defines compression level, {@code 1} yields the fastest compression and
* {@code 9} yields the best compression. {@code 0} means no compression.
* @param bits
* defines windowBits, The base two logarithm of the size of the history
* buffer. The value should be in the range {@code 9} to {@code 15}
* inclusive. Larger values result in better compression at the expense of
* memory usage
* @param mem
* defines memlevel, How much memory should be allocated for the internal
* compression state. {@code 1} uses minimum memory and {@code 9} uses
* maximum memory. Larger values result in better and faster compression
* at the expense of memory usage
*/
final case class CompressionOptions(
level: Int,
bits: Int,
mem: Int,
kind: CompressionOptions.CompressionType,
)
sealed trait CompressionOptions {
val name: String
}

object CompressionOptions {
val DefaultLevel = 6
val DefaultBits = 15
val DefaultMem = 8

final case class GZip(cfg: DeflateConfig) extends CompressionOptions { val name = "gzip" }
final case class Deflate(cfg: DeflateConfig) extends CompressionOptions { val name = "deflate" }
final case class Brotli(cfg: BrotliConfig) extends CompressionOptions { val name = "brotli" }

/**
* @param level
* defines compression level, {@code 1} yields the fastest compression
* and {@code 9} yields the best compression. {@code 0} means no
* compression.
* @param bits
* defines windowBits, The base two logarithm of the size of the history
* buffer. The value should be in the range {@code 9} to {@code 15}
* inclusive. Larger values result in better compression at the expense
* of memory usage
* @param mem
* defines memlevel, How much memory should be allocated for the
* internal compression state. {@code 1} uses minimum memory and
* {@code 9} uses maximum memory. Larger values result in better and
* faster compression at the expense of memory usage
*/
final case class DeflateConfig(
level: Int,
bits: Int,
mem: Int,
)

object DeflateConfig {
val DefaultLevel = 6
val DefaultBits = 15
val DefaultMem = 8
}

final case class BrotliConfig(
quality: Int,
lgwin: Int,
mode: Mode,
)

object BrotliConfig {
val DefaultQuality = 4
val DefaultLgwin = -1
val DefaultMode = Mode.Text
}

sealed trait Mode
object Mode {
case object Generic extends Mode
case object Text extends Mode
case object Font extends Mode

def fromString(s: String): Mode = s.toLowerCase match {
case "generic" => Generic
case "text" => Text
case "font" => Font
case _ => Text
}
}

/**
* Creates GZip CompressionOptions. Defines defaults as per
* io.netty.handler.codec.compression.GzipOptions#DEFAULT
*/
def gzip(level: Int = DefaultLevel, bits: Int = DefaultBits, mem: Int = DefaultMem): CompressionOptions =
CompressionOptions(level, bits, mem, CompressionType.GZip)
def gzip(
level: Int = DeflateConfig.DefaultLevel,
bits: Int = DeflateConfig.DefaultBits,
mem: Int = DeflateConfig.DefaultMem,
): CompressionOptions =
CompressionOptions.GZip(DeflateConfig(level, bits, mem))

/**
* Creates Deflate CompressionOptions. Defines defaults as per
* io.netty.handler.codec.compression.DeflateOptions#DEFAULT
*/
def deflate(level: Int = DefaultLevel, bits: Int = DefaultBits, mem: Int = DefaultMem): CompressionOptions =
CompressionOptions(level, bits, mem, CompressionType.Deflate)

sealed trait CompressionType {
val name: String
}

private[http] object CompressionType {
case object GZip extends CompressionType { val name = "gzip" }
case object Deflate extends CompressionType { val name = "deflate" }
def deflate(
level: Int = DeflateConfig.DefaultLevel,
bits: Int = DeflateConfig.DefaultBits,
mem: Int = DeflateConfig.DefaultMem,
): CompressionOptions =
CompressionOptions.Deflate(DeflateConfig(level, bits, mem))

lazy val config: zio.Config[CompressionType] =
zio.Config.string.mapOrFail {
case "gzip" => Right(GZip)
case "deflate" => Right(Deflate)
case other => Left(zio.Config.Error.InvalidData(message = s"Invalid compression type: $other"))
}
}
/**
* Creates Brotli CompressionOptions. Defines defaults as per
* io.netty.handler.codec.compression.BrotliOptions#DEFAULT
*/
def brotli(
quality: Int = BrotliConfig.DefaultQuality,
lgwin: Int = BrotliConfig.DefaultLgwin,
mode: Mode = BrotliConfig.DefaultMode,
): CompressionOptions =
CompressionOptions.Brotli(BrotliConfig(quality, lgwin, mode))

lazy val config: zio.Config[CompressionOptions] =
(
zio.Config.int("level").withDefault(DefaultLevel) ++
zio.Config.int("bits").withDefault(DefaultBits) ++
zio.Config.int("mem").withDefault(DefaultMem) ++
CompressionOptions.CompressionType.config.nested("type")
).map { case (level, bits, mem, kind) =>
CompressionOptions(level, bits, mem, kind)
(zio.Config.int("level").withDefault(DeflateConfig.DefaultLevel) ++
zio.Config.int("bits").withDefault(DeflateConfig.DefaultBits) ++
zio.Config.int("mem").withDefault(DeflateConfig.DefaultMem)) ++
zio.Config.int("quantity").withDefault(BrotliConfig.DefaultQuality) ++
zio.Config.int("lgwin").withDefault(BrotliConfig.DefaultLgwin) ++
zio.Config.string("mode").map(Mode.fromString).withDefault(BrotliConfig.DefaultMode) ++
zio.Config.string("type")
).map { case (level, bits, mem, quantity, lgwin, mode, typ) =>
typ.toLowerCase match {
case "gzip" => gzip(level, bits, mem)
case "deflate" => deflate(level, bits, mem)
case "brotli" => brotli(quantity, lgwin, mode)
}
}
}
}
Expand Down
Loading