forked from TechEmpower/FrameworkBenchmarks
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Scala/otavia] Add new framework otavia: Your shiny new IO & Actor pr…
…ogramming model! (TechEmpower#9158)
- Loading branch information
1 parent
848d684
commit 64fd93c
Showing
17 changed files
with
832 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
0.11.8 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
version = "3.5.3" | ||
|
||
runner.dialect = scala3 | ||
maxColumn = 120 | ||
docstrings.blankFirstLine = no | ||
docstrings.style = AsteriskSpace | ||
docstrings.removeEmpty = true | ||
docstrings.oneline = fold | ||
docstrings.wrap = yes | ||
docstrings.wrapMaxColumn = 120 | ||
docstrings.forceBlankLineBefore = true | ||
align.preset = more | ||
|
||
indent.main = 4 | ||
|
||
newlines.topLevelBodyIfMinStatements = [before,after] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
## Introduction | ||
|
||
[GitHub - otavia-projects/otavia : Your shiny new IO & Actor programming model!](https://github.com/otavia-projects/otavia) | ||
|
||
`otavia` is an IO and Actor programming model power by Scala 3, it provides a toolkit to make writing high-performance | ||
concurrent programs more easily. | ||
|
||
You can get a quick overview of the basic usage and core design of `otavia` in the following documentation: | ||
|
||
- [Quick Start](https://github.com/otavia-projects/otavia/blob/main/docs/_docs/quick_start.md) | ||
- [Core Concepts and Design](https://github.com/otavia-projects/otavia/blob/main/docs/_docs/core_concept.md) | ||
|
||
More document can be found at [website](https://otavia.cc/home.html) |
106 changes: 106 additions & 0 deletions
106
frameworks/Scala/otavia/benchmark/src/app/controller/DBController.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
package app.controller | ||
|
||
import app.controller.DBController.* | ||
import app.model.World | ||
import cc.otavia.core.actor.{MessageOf, StateActor} | ||
import cc.otavia.core.address.Address | ||
import cc.otavia.core.message.{Ask, Reply} | ||
import cc.otavia.core.stack.helper.{FutureState, FuturesState, StartState} | ||
import cc.otavia.core.stack.{AskStack, StackState, StackYield} | ||
import cc.otavia.http.server.{HttpRequest, HttpResponse} | ||
import cc.otavia.sql.Connection | ||
import cc.otavia.sql.Statement.{ModifyRows, PrepareQuery} | ||
|
||
import java.util.SplittableRandom | ||
|
||
class DBController extends StateActor[REQ] { | ||
|
||
private var connection: Address[MessageOf[Connection]] = _ | ||
|
||
private val random = new SplittableRandom() | ||
|
||
override protected def afterMount(): Unit = connection = autowire[Connection]() | ||
|
||
override protected def resumeAsk(stack: AskStack[REQ & Ask[? <: Reply]]): StackYield = | ||
stack match | ||
case stack: AskStack[SingleQueryRequest] if stack.ask.isInstanceOf[SingleQueryRequest] => | ||
handleSingleQuery(stack) | ||
case stack: AskStack[MultipleQueryRequest] if stack.ask.isInstanceOf[MultipleQueryRequest] => | ||
handleMultipleQuery(stack) | ||
case stack: AskStack[UpdateRequest] if stack.ask.isInstanceOf[UpdateRequest] => | ||
handleUpdateQuery(stack) | ||
|
||
// Test 2: Single database query | ||
private def handleSingleQuery(stack: AskStack[SingleQueryRequest]): StackYield = { | ||
stack.state match | ||
case _: StartState => | ||
val state = FutureState[World]() | ||
connection.ask(PrepareQuery.fetchOne[World](SELECT_WORLD, Tuple1(randomWorld())), state.future) | ||
stack.suspend(state) | ||
case state: FutureState[World] => | ||
stack.`return`(state.future.getNow) | ||
} | ||
|
||
// Test 3: Multiple database queries | ||
private def handleMultipleQuery(stack: AskStack[MultipleQueryRequest]): StackYield = { | ||
stack.state match | ||
case _: StartState => | ||
stack.suspend(selectWorlds(normalizeQueries(stack.ask.params))) | ||
case state: FuturesState[World] => | ||
val response = HttpResponse.builder.setContent(state.futures.map(_.getNow)).build() | ||
stack.`return`(response) | ||
} | ||
|
||
// Test 5: Database updates | ||
private def handleUpdateQuery(stack: AskStack[UpdateRequest]): StackYield = { | ||
stack.state match | ||
case _: StartState => | ||
stack.suspend(selectWorlds(normalizeQueries(stack.ask.params))) | ||
case state: FuturesState[World] => | ||
val worlds = state.futures.map(_.getNow) | ||
stack.attach(worlds) | ||
val newState = FutureState[ModifyRows]() | ||
val newWorlds = worlds.sortBy(_.id).map(_.copy(randomNumber = randomWorld())) | ||
connection.ask(PrepareQuery.update(UPDATE_WORLD, newWorlds), newState.future) | ||
stack.suspend(newState) | ||
case state: FutureState[ModifyRows] => | ||
if (state.future.isFailed) state.future.causeUnsafe.printStackTrace() | ||
val response = HttpResponse.builder.setContent(stack.attach[Seq[World]]).build() | ||
stack.`return`(response) | ||
} | ||
|
||
private def selectWorlds(queries: Int): StackState = { | ||
val state = FuturesState[World](queries) | ||
for (future <- state.futures) | ||
connection.ask(PrepareQuery.fetchOne[World](SELECT_WORLD, Tuple1(randomWorld())), future) | ||
state | ||
} | ||
|
||
private def randomWorld(): Int = 1 + random.nextInt(10000) | ||
|
||
private def normalizeQueries(params: Map[String, String]): Int = { | ||
params.get("queries") match | ||
case Some(value) => | ||
try { | ||
val queries = value.toInt | ||
if (queries < 1) 1 else if (queries > 500) 500 else queries | ||
} catch { | ||
case e: Throwable => 1 | ||
} | ||
case None => 1 | ||
} | ||
|
||
} | ||
|
||
object DBController { | ||
|
||
type REQ = SingleQueryRequest | MultipleQueryRequest | UpdateRequest | ||
|
||
class SingleQueryRequest extends HttpRequest[Nothing, World] | ||
class MultipleQueryRequest extends HttpRequest[Nothing, HttpResponse[Seq[World]]] | ||
class UpdateRequest extends HttpRequest[Nothing, HttpResponse[Seq[World]]] | ||
|
||
private val SELECT_WORLD = "SELECT id, randomnumber from WORLD where id=$1" | ||
private val UPDATE_WORLD = "update world set randomnumber=$2 where id=$1" | ||
|
||
} |
41 changes: 41 additions & 0 deletions
41
frameworks/Scala/otavia/benchmark/src/app/controller/FortuneController.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
package app.controller | ||
|
||
import app.controller.FortuneController.* | ||
import app.model.Fortune | ||
import cc.otavia.core.actor.{MessageOf, StateActor} | ||
import cc.otavia.core.address.Address | ||
import cc.otavia.core.stack.helper.{FutureState, StartState} | ||
import cc.otavia.core.stack.{AskStack, StackState, StackYield} | ||
import cc.otavia.http.server.{HttpRequest, HttpResponse} | ||
import cc.otavia.sql.Statement.PrepareQuery | ||
import cc.otavia.sql.{Connection, RowSet} | ||
|
||
class FortuneController extends StateActor[FortuneRequest] { | ||
|
||
private var connection: Address[MessageOf[Connection]] = _ | ||
|
||
override protected def afterMount(): Unit = connection = autowire[Connection]() | ||
|
||
// Test 4: Fortunes | ||
override protected def resumeAsk(stack: AskStack[FortuneRequest]): StackYield = { | ||
stack.state match | ||
case _: StartState => | ||
val state = FutureState[RowSet[Fortune]]() | ||
connection.ask(PrepareQuery.fetchAll[Fortune](SELECT_FORTUNE), state.future) | ||
stack.suspend(state) | ||
case state: FutureState[RowSet[Fortune]] => | ||
val fortunes = (state.future.getNow.rows :+ Fortune(0, "Additional fortune added at request time.")) | ||
.sortBy(_.message) | ||
val response = HttpResponse.builder.setContent(fortunes).build() | ||
stack.`return`(response) | ||
} | ||
|
||
} | ||
|
||
object FortuneController { | ||
|
||
class FortuneRequest extends HttpRequest[Nothing, HttpResponse[Seq[Fortune]]] | ||
|
||
private val SELECT_FORTUNE = "SELECT id, message from FORTUNE" | ||
|
||
} |
7 changes: 7 additions & 0 deletions
7
frameworks/Scala/otavia/benchmark/src/app/model/Fortune.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package app.model | ||
|
||
import cc.otavia.json.JsonSerde | ||
import cc.otavia.sql.{Row, RowDecoder} | ||
|
||
/** The model for the "fortune" database table. */ | ||
case class Fortune(id: Int, message: String) extends Row derives RowDecoder, JsonSerde |
5 changes: 5 additions & 0 deletions
5
frameworks/Scala/otavia/benchmark/src/app/model/Message.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
package app.model | ||
|
||
import cc.otavia.json.JsonSerde | ||
|
||
case class Message(message: String) derives JsonSerde |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
package app.model | ||
|
||
import cc.otavia.json.JsonSerde | ||
import cc.otavia.serde.annotation.rename | ||
import cc.otavia.sql.{Row, RowDecoder} | ||
|
||
/** The model for the "world" database table. */ | ||
case class World(id: Int, @rename("randomnumber") randomNumber: Int) extends Row derives RowDecoder, JsonSerde |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
package app | ||
|
||
import app.controller.DBController.* | ||
import app.controller.FortuneController.* | ||
import app.controller.{DBController, FortuneController} | ||
import app.model.* | ||
import app.util.FortunesRender | ||
import cc.otavia.core.actor.ChannelsActor.{Bind, ChannelEstablished} | ||
import cc.otavia.core.actor.MainActor | ||
import cc.otavia.core.slf4a.LoggerFactory | ||
import cc.otavia.core.stack.helper.{FutureState, StartState} | ||
import cc.otavia.core.stack.{NoticeStack, StackYield} | ||
import cc.otavia.core.system.ActorSystem | ||
import cc.otavia.http.HttpMethod.* | ||
import cc.otavia.http.MediaType | ||
import cc.otavia.http.MediaType.* | ||
import cc.otavia.http.server.* | ||
import cc.otavia.http.server.Router.* | ||
import cc.otavia.json.JsonSerde | ||
import cc.otavia.serde.helper.BytesSerde | ||
import cc.otavia.sql.Connection | ||
|
||
import java.io.File | ||
import java.nio.charset.StandardCharsets.UTF_8 | ||
import java.nio.file.Path | ||
|
||
private class ServerMain(val port: Int = 8080) extends MainActor(Array.empty) { | ||
|
||
override def main0(stack: NoticeStack[MainActor.Args]): StackYield = stack.state match | ||
case _: StartState => | ||
val worldResponseSerde = HttpResponseSerde.json(summon[JsonSerde[World]]) | ||
val worldsResponseSerde = HttpResponseSerde.json(JsonSerde.derived[Seq[World]]) | ||
val fortunesResponseSerde = HttpResponseSerde(new FortunesRender(), MediaType.TEXT_HTML_UTF8) | ||
|
||
val dbController = autowire[DBController]() | ||
val fortuneController = autowire[FortuneController]() | ||
|
||
val routers = Seq( | ||
// Test 6: plaintext | ||
constant[Array[Byte]](GET, "/plaintext", "Hello, World!".getBytes(UTF_8), BytesSerde, TEXT_PLAIN_UTF8), | ||
// Test 1: JSON serialization | ||
constant[Message](GET, "/json", Message("Hello, World!"), summon[JsonSerde[Message]], APP_JSON), | ||
// Test 2: Single database query. | ||
get("/db", dbController, () => new SingleQueryRequest(), worldResponseSerde), | ||
// Test 3: Multiple database queries | ||
get("/queries", dbController, () => new MultipleQueryRequest(), worldsResponseSerde), | ||
// Test 5: Database updates | ||
get("/updates", dbController, () => new UpdateRequest(), worldsResponseSerde), | ||
// Test 4: Fortunes | ||
get("/fortunes", fortuneController, () => new FortuneRequest(), fortunesResponseSerde) | ||
) | ||
val server = system.buildActor(() => new HttpServer(system.actorWorkerSize, routers)) | ||
val state = FutureState[ChannelEstablished]() | ||
server.ask(Bind(port), state.future) | ||
stack.suspend(state) | ||
case state: FutureState[ChannelEstablished] => | ||
if (state.future.isFailed) state.future.causeUnsafe.printStackTrace() | ||
logger.info(s"http server bind port $port success") | ||
stack.`return`() | ||
|
||
} | ||
|
||
@main def startup(url: String, user: String, password: String, poolSize: Int): Unit = | ||
val system = ActorSystem() | ||
val logger = LoggerFactory.getLogger("startup", system) | ||
logger.info("starting http server") | ||
system.buildActor(() => new Connection(url, user, password), global = true, num = poolSize) | ||
system.buildActor(() => new DBController(), global = true, num = system.actorWorkerSize) | ||
system.buildActor(() => new FortuneController(), global = true, num = system.actorWorkerSize) | ||
system.buildActor(() => new ServerMain()) |
64 changes: 64 additions & 0 deletions
64
frameworks/Scala/otavia/benchmark/src/app/util/FortunesRender.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
package app.util | ||
|
||
import app.model.Fortune | ||
import cc.otavia.buffer.{Buffer, BufferUtils} | ||
import cc.otavia.serde.Serde | ||
|
||
import java.nio.charset.StandardCharsets | ||
import scala.annotation.switch | ||
|
||
class FortunesRender extends Serde[Seq[Fortune]] { | ||
|
||
private val text1 = | ||
"<!DOCTYPE html><html><head><title>Fortunes</title></head><body><table><tr><th>id</th><th>message</th></tr>" | ||
.getBytes(StandardCharsets.UTF_8) | ||
|
||
private val text2 = "<tr><td>".getBytes(StandardCharsets.UTF_8) | ||
|
||
private val text3 = "</td><td>".getBytes(StandardCharsets.UTF_8) | ||
|
||
private val text4 = "</td></tr>".getBytes(StandardCharsets.UTF_8) | ||
|
||
private val text5 = "</table></body></html>".getBytes(StandardCharsets.UTF_8) | ||
|
||
private val lt = "<".getBytes() | ||
private val gt = ">".getBytes() | ||
private val quot = """.getBytes() | ||
private val squot = "'".getBytes() | ||
private val amp = "&".getBytes() | ||
|
||
override def serialize(fortunes: Seq[Fortune], out: Buffer): Unit = { | ||
out.writeBytes(text1) | ||
for (fortune <- fortunes) { | ||
out.writeBytes(text2) | ||
BufferUtils.writeIntAsString(out, fortune.id) | ||
out.writeBytes(text3) | ||
writeEscapeMessage(out, fortune.message) | ||
out.writeBytes(text4) | ||
} | ||
out.writeBytes(text5) | ||
} | ||
|
||
override def deserialize(in: Buffer): Seq[Fortune] = throw new UnsupportedOperationException() | ||
|
||
private def writeEscapeMessage(buffer: Buffer, message: String): Unit = { | ||
var i = 0 | ||
while (i < message.length) { | ||
val ch = message.charAt(i) | ||
writeChar(buffer, ch) | ||
i += 1 | ||
} | ||
} | ||
|
||
private def writeChar(buffer: Buffer, ch: Char): Unit = (ch: @switch) match | ||
case '<' => buffer.writeBytes(lt) | ||
case '>' => buffer.writeBytes(gt) | ||
case '"' => buffer.writeBytes(quot) | ||
case '\'' => buffer.writeBytes(squot) | ||
case '&' => buffer.writeBytes(amp) | ||
case _ => | ||
if (ch < 0x80) buffer.writeByte(ch.toByte) | ||
else if (ch < 0x800) buffer.writeShortLE((ch >> 6 | (ch << 8 & 0x3f00) | 0x80c0).toShort) | ||
else buffer.writeMediumLE(ch >> 12 | (ch << 2 & 0x3f00) | (ch << 16 & 0x3f0000) | 0x8080e0) | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
{ | ||
"framework": "otavia", | ||
"tests": [ | ||
{ | ||
"default": { | ||
"json_url": "/json", | ||
"plaintext_url": "/plaintext", | ||
"db_url": "/db", | ||
"query_url": "/queries?queries=", | ||
"fortune_url": "/fortunes", | ||
"update_url": "/updates?queries=", | ||
"port": 8080, | ||
"approach": "Realistic", | ||
"classification": "Micro", | ||
"database": "Postgres", | ||
"framework": "otavia", | ||
"language": "Scala", | ||
"flavor": "None", | ||
"orm": "Micro", | ||
"platform": "Otavia", | ||
"webserver": "None", | ||
"os": "Linux", | ||
"database_os": "Linux", | ||
"display_name": "otavia", | ||
"notes": "", | ||
"versus": "Otavia" | ||
}, | ||
"reserve": { | ||
"json_url": "/json", | ||
"plaintext_url": "/plaintext", | ||
"db_url": "/db", | ||
"query_url": "/queries?queries=", | ||
"fortune_url": "/fortunes", | ||
"update_url": "/updates?queries=", | ||
"port": 8080, | ||
"approach": "Realistic", | ||
"classification": "Micro", | ||
"database": "Postgres", | ||
"framework": "otavia", | ||
"language": "Scala", | ||
"flavor": "None", | ||
"orm": "Micro", | ||
"platform": "Otavia", | ||
"webserver": "None", | ||
"os": "Linux", | ||
"database_os": "Linux", | ||
"display_name": "otavia", | ||
"notes": "", | ||
"versus": "Otavia" | ||
} | ||
} | ||
] | ||
} |
Oops, something went wrong.