Skip to content

Commit 1e8b3c4

Browse files
authored
Add tapir & kyo frameworks (#9816)
* Add tapir framework and its variants * upgrade zio-http to 3.2.0 * fix: update zio-http to adhere to the json test requirement That is "For each request, an object mapping the key message to Hello, World! must be instantiated." * upgrade zio-http to use Java 21 * Add kyo-scheduler variants of zio-http and http4s * Upgrade kyo to 0.18.0 * Move kyo-tapir into the tapir folder
1 parent f498f95 commit 1e8b3c4

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+1236
-101
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# kyo-scheduler Benchmarking Test
2+
3+
This is a simple test to benchmark the performance of the kyo-scheduler libraries along with different backends in Scala.
4+
5+
### Test Type Implementation Source Code
6+
7+
* JSON
8+
* PLAINTEXT
9+
10+
## Software Versions
11+
12+
* [Java OpenJDK 21](https://adoptium.net/temurin/releases/)
13+
* [Kyo 0.17.0](https://github.com/getkyo/kyo)
14+
* [Scala 3.6.4 and Scala 2.13.16](https://www.scala-lang.org/)
15+
16+
### Server Implementations
17+
18+
* [ZIO Http](https://zio.dev/zio-http/)
19+
* [http4s](https://http4s.org/)
20+
21+
## Test URLs
22+
23+
* JSON - http://localhost:8080/json
24+
* PLAINTEXT - http://localhost:8080/plaintext
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
{
2+
"framework": "kyo-scheduler",
3+
"tests": [
4+
{
5+
"default": {
6+
"plaintext_url": "/plaintext",
7+
"json_url": "/json",
8+
"port": 8080,
9+
"database": "None",
10+
"approach": "Realistic",
11+
"classification": "Micro",
12+
"framework": "zio-http",
13+
"language": "Scala",
14+
"flavor": "None",
15+
"orm": "Raw",
16+
"platform": "Netty",
17+
"webserver": "None",
18+
"database_os": "Linux",
19+
"os": "Linux",
20+
"display_name": "zio-http with kyo-scheduler",
21+
"notes": "https://zio.dev/zio-http/",
22+
"versus": "None"
23+
},
24+
"http4s": {
25+
"orm": "Raw",
26+
"database_os": "Linux",
27+
"json_url": "/json",
28+
"plaintext_url": "/plaintext",
29+
"query_url": "/queries?queries=",
30+
"update_url": "/updates?queries=",
31+
"fortune_url": "/fortunes",
32+
"port": 8080,
33+
"approach": "Realistic",
34+
"classification": "Micro",
35+
"database": "Postgres",
36+
"db_url": "/db",
37+
"framework": "http4s",
38+
"language": "Scala",
39+
"platform": "NIO2",
40+
"webserver": "blaze",
41+
"os": "Linux",
42+
"display_name": "http4s with kyo-scheduler",
43+
"notes": "https://http4s.org/",
44+
"flavor": "None",
45+
"versus": "None"
46+
}
47+
}
48+
]
49+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
name := "kyo-scheduler-benchmark"
2+
3+
ThisBuild / version := "1.0.0"
4+
5+
val kyoVersion = "0.18.0"
6+
7+
val commonAssemblySettings = assembly / assemblyMergeStrategy := {
8+
case x if x.contains("io.netty.versions.properties") => MergeStrategy.discard
9+
case x if x.contains("module-info.class") => MergeStrategy.discard
10+
case x =>
11+
val oldStrategy = (assembly / assemblyMergeStrategy).value
12+
oldStrategy(x)
13+
}
14+
15+
// based on the framework/Scala/zio-http implementation
16+
lazy val `zio-http` = (project in file("zio-http"))
17+
.settings(
18+
scalaVersion := "3.6.4",
19+
name := "zio-http-kyo-scheduler-benchmark",
20+
libraryDependencies ++= Seq(
21+
"dev.zio" %% "zio-http" % "3.2.0",
22+
"io.getkyo" %% "kyo-scheduler-zio" % kyoVersion,
23+
),
24+
commonAssemblySettings
25+
)
26+
27+
val http4sVersion = "0.23.22"
28+
val http4sBlazeVersion = "0.23.15"
29+
val http4sTwirlVersion = "0.23.17"
30+
31+
// based on the framework/Scala/http4s implementation
32+
lazy val http4s = (project in file("http4s"))
33+
.settings(
34+
scalaVersion := "2.13.16",
35+
name := "http4s-kyo-scheduler-benchmark",
36+
libraryDependencies ++= Seq(
37+
"org.http4s" %% "http4s-blaze-server" % http4sBlazeVersion,
38+
"org.http4s" %% "http4s-dsl" % http4sVersion,
39+
"org.http4s" %% "http4s-twirl" % http4sTwirlVersion,
40+
"org.http4s" %% "http4s-circe" % http4sVersion,
41+
// Optional for auto-derivation of JSON codecs
42+
"io.circe" %% "circe-generic" % "0.14.5",
43+
"org.typelevel" %% "cats-effect" % "3.5.1",
44+
"co.fs2" %% "fs2-core" % "3.7.0",
45+
"co.fs2" %% "fs2-io" % "3.7.0",
46+
"io.getquill" %% "quill-jasync-postgres" % "3.19.0",
47+
"io.getquill" %% "quill-jasync" % "3.19.0",
48+
"ch.qos.logback" % "logback-classic" % "1.4.8",
49+
"io.getkyo" %% "kyo-scheduler-cats" % kyoVersion,
50+
),
51+
commonAssemblySettings,
52+
addCompilerPlugin("com.olegpy" %% "better-monadic-for" % "0.3.1")
53+
)
54+
.enablePlugins(SbtTwirl)
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
[framework]
2+
name = "kyo-scheduler"
3+
4+
[main]
5+
urls.plaintext = "/plaintext"
6+
urls.json = "/json"
7+
approach = "Realistic"
8+
classification = "Micro"
9+
database = "None"
10+
database_os = "Linux"
11+
os = "Linux"
12+
orm = "Raw"
13+
platform = "Netty"
14+
webserver = "None"
15+
versus = "None"
16+
17+
[http4s]
18+
urls.plaintext = "/plaintext"
19+
urls.json = "/json"
20+
urls.db = "/db"
21+
urls.query = "/queries?queries="
22+
urls.update = "/updates?queries="
23+
urls.fortune = "/fortunes"
24+
approach = "Realistic"
25+
classification = "Micro"
26+
database = "Postgres"
27+
database_os = "Linux"
28+
os = "Linux"
29+
orm = "Raw"
30+
platform = "NIO2"
31+
webserver = "blaze"
32+
versus = "None"
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
ctx.port=5432
2+
ctx.username=benchmarkdbuser
3+
ctx.password=benchmarkdbpass
4+
ctx.database=hello_world
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<configuration>
2+
3+
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
4+
<!-- encoders are assigned the type
5+
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
6+
<encoder>
7+
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
8+
</encoder>
9+
</appender>
10+
11+
<root level="error">
12+
<appender-ref ref="STDOUT" />
13+
</root>
14+
</configuration>
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package http4s.techempower.benchmark
2+
3+
import java.util.concurrent.{Executor, ThreadLocalRandom}
4+
5+
import scala.concurrent.{ExecutionContext, ExecutionContextExecutor}
6+
import cats.effect.{IO => CatsIO}
7+
import cats.syntax.all._
8+
import io.getquill._
9+
10+
class DatabaseService(ctx: PostgresJAsyncContext[LowerCase.type], executor: Executor) {
11+
implicit val dbExecutionContext: ExecutionContextExecutor = ExecutionContext.fromExecutor(executor)
12+
import ctx._
13+
14+
def close(): CatsIO[Unit] = {
15+
CatsIO(ctx.close())
16+
}
17+
18+
// Provide a random number between 1 and 10000 (inclusive)
19+
private def randomWorldId() =
20+
CatsIO(ThreadLocalRandom.current().nextInt(1, 10001))
21+
22+
// Update the randomNumber field with a random number
23+
def updateRandomNumber(world: World): CatsIO[World] =
24+
for {
25+
randomId <- randomWorldId()
26+
} yield world.copy(randomNumber = randomId)
27+
28+
// Select a World object from the database by ID
29+
def selectWorld(id: Int): CatsIO[World] =
30+
CatsIO.fromFuture(
31+
CatsIO.delay(
32+
ctx
33+
.run(quote {
34+
query[World].filter(_.id == lift(id))
35+
})
36+
.map(rq => rq.head)
37+
)
38+
)
39+
40+
// Select a random World object from the database
41+
def selectRandomWorld(): CatsIO[World] =
42+
for {
43+
randomId <- randomWorldId()
44+
world <- selectWorld(randomId)
45+
} yield world
46+
47+
// Select a specified number of random World objects from the database
48+
def getWorlds(numQueries: Int): CatsIO[List[World]] =
49+
(0 until numQueries).toList.traverse(_ => selectRandomWorld())
50+
51+
// Update the randomNumber field with a new random number, for a list of World objects
52+
def getNewWorlds(worlds: List[World]): CatsIO[List[World]] =
53+
worlds.map(updateRandomNumber).sequence
54+
55+
// Update the randomNumber column in the database for a specified set of World objects,
56+
// this uses a batch update SQL call.
57+
def updateWorlds(newWorlds: List[World]): CatsIO[Int] = {
58+
val u = quote {
59+
liftQuery(newWorlds).foreach { world =>
60+
query[World]
61+
.filter(_.id == world.id)
62+
.update(_.randomNumber -> world.randomNumber)
63+
}
64+
}
65+
CatsIO.fromFuture(CatsIO.delay(ctx.run(u).map(_.length)))
66+
}
67+
68+
// Retrieve all fortunes from the database
69+
def getFortunes(): CatsIO[List[Fortune]] =
70+
CatsIO.fromFuture(CatsIO.delay(ctx.run(query[Fortune]).map(_.toList)))
71+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package http4s.techempower.benchmark
2+
3+
4+
import java.util.concurrent.Executors
5+
import cats.effect.{ExitCode, IO, Resource}
6+
import com.typesafe.config.ConfigValueFactory
7+
import io.circe.generic.auto._
8+
import io.circe.syntax._
9+
import io.getquill.util.LoadConfig
10+
import io.getquill.LowerCase
11+
import io.getquill.PostgresJAsyncContext
12+
import org.http4s._
13+
import org.http4s.dsl._
14+
import org.http4s.circe._
15+
import org.http4s.implicits._
16+
import org.http4s.blaze.server.BlazeServerBuilder
17+
import org.http4s.headers.Server
18+
import org.http4s.twirl._
19+
20+
final case class Message(message: String)
21+
final case class World(id: Int, randomNumber: Int)
22+
final case class Fortune(id: Int, message: String)
23+
24+
// Extract queries parameter (with default and min/maxed)
25+
object Queries {
26+
def unapply(params: Map[String, Seq[String]]): Option[Int] =
27+
Some(params.getOrElse("queries", Nil).headOption match {
28+
case None => 1
29+
case Some(x) =>
30+
Math.max(1, Math.min(500, scala.util.Try(x.toInt).getOrElse(1)))
31+
})
32+
}
33+
34+
// based on the framework/Scala/http4s implementation
35+
object WebServer extends kyo.KyoSchedulerIOApp with Http4sDsl[IO] {
36+
def makeDatabaseService(
37+
host: String,
38+
poolSize: Int
39+
): Resource[IO, DatabaseService] = {
40+
for {
41+
executor <- Resource(IO {
42+
val pool = Executors.newFixedThreadPool(poolSize)
43+
(pool, IO(pool.shutdown()))
44+
})
45+
ctx <- Resource.fromAutoCloseable(IO(new PostgresJAsyncContext(
46+
LowerCase,
47+
LoadConfig("ctx")
48+
.withValue("host", ConfigValueFactory.fromAnyRef(host))
49+
.withValue(
50+
"maxActiveConnections",
51+
ConfigValueFactory.fromAnyRef(poolSize)
52+
)
53+
)))
54+
} yield new DatabaseService(ctx, executor)
55+
}
56+
57+
// Add a new fortune to an existing list, and sort by message.
58+
def getSortedFortunes(old: List[Fortune]): List[Fortune] = {
59+
val newFortune = Fortune(0, "Additional fortune added at request time.")
60+
(newFortune :: old).sortBy(_.message)
61+
}
62+
63+
// Add Server header container server address
64+
def addServerHeader(service: HttpRoutes[IO]): HttpRoutes[IO] =
65+
cats.data.Kleisli { req: Request[IO] =>
66+
service.run(req).map(_.putHeaders(server))
67+
}
68+
69+
val server = Server(ProductId("http4s", None))
70+
71+
// HTTP service definition
72+
def service(db: DatabaseService) =
73+
addServerHeader(HttpRoutes.of[IO] {
74+
case GET -> Root / "plaintext" =>
75+
Ok("Hello, World!")
76+
77+
case GET -> Root / "json" =>
78+
Ok(Message("Hello, World!").asJson)
79+
80+
case GET -> Root / "db" =>
81+
Ok(db.selectRandomWorld().map(_.asJson))
82+
83+
case GET -> Root / "queries" :? Queries(numQueries) =>
84+
Ok(db.getWorlds(numQueries).map(_.asJson))
85+
86+
case GET -> Root / "fortunes" =>
87+
Ok(for {
88+
oldFortunes <- db.getFortunes()
89+
newFortunes = getSortedFortunes(oldFortunes)
90+
} yield html.index(newFortunes))
91+
92+
case GET -> Root / "updates" :? Queries(numQueries) =>
93+
Ok(for {
94+
worlds <- db.getWorlds(numQueries)
95+
newWorlds <- db.getNewWorlds(worlds)
96+
_ <- db.updateWorlds(newWorlds)
97+
} yield newWorlds.asJson)
98+
})
99+
100+
// Given a fully constructed HttpService, start the server and wait for completion
101+
def startServer(service: HttpRoutes[IO]) =
102+
BlazeServerBuilder[IO]
103+
.bindHttp(8080, "0.0.0.0")
104+
.withHttpApp(service.orNotFound)
105+
.withSocketKeepAlive(true)
106+
.resource
107+
108+
// Entry point when starting service
109+
override def run(args: List[String]): IO[ExitCode] =
110+
(for {
111+
db <- makeDatabaseService(
112+
args.headOption.getOrElse("localhost"),
113+
sys.env.get("DB_POOL_SIZE").map(_.toInt).getOrElse(64)
114+
)
115+
server <- startServer(service(db))
116+
} yield server)
117+
.use(_ => IO.never)
118+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
@import http4s.techempower.benchmark.Fortune
2+
@(fortunes: Seq[Fortune])
3+
<!DOCTYPE html>
4+
<html>
5+
<head><title>Fortunes</title></head>
6+
<body>
7+
<table>
8+
<tr><th>id</th><th>message</th></tr>
9+
@for(fortune <- fortunes) {
10+
<tr><td>@fortune.id</td><td>@fortune.message</td></tr>
11+
}
12+
</table>
13+
</body>
14+
</html>

0 commit comments

Comments
 (0)