Skip to content

Commit

Permalink
[Scala/otavia] Add new framework otavia: Your shiny new IO & Actor pr…
Browse files Browse the repository at this point in the history
…ogramming model! (TechEmpower#9158)
  • Loading branch information
yankun1992 authored Aug 2, 2024
1 parent 848d684 commit 64fd93c
Show file tree
Hide file tree
Showing 17 changed files with 832 additions and 0 deletions.
1 change: 1 addition & 0 deletions frameworks/Scala/otavia/.mill-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0.11.8
16 changes: 16 additions & 0 deletions frameworks/Scala/otavia/.scalafmt.conf
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]
13 changes: 13 additions & 0 deletions frameworks/Scala/otavia/README.MD
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)
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"

}
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 frameworks/Scala/otavia/benchmark/src/app/model/Fortune.scala
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 frameworks/Scala/otavia/benchmark/src/app/model/Message.scala
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
8 changes: 8 additions & 0 deletions frameworks/Scala/otavia/benchmark/src/app/model/World.scala
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
70 changes: 70 additions & 0 deletions frameworks/Scala/otavia/benchmark/src/app/startup.scala
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())
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 = "&lt;".getBytes()
private val gt = "&gt;".getBytes()
private val quot = "&quot;".getBytes()
private val squot = "&#39;".getBytes()
private val amp = "&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)

}
53 changes: 53 additions & 0 deletions frameworks/Scala/otavia/benchmark_config.json
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"
}
}
]
}
Loading

0 comments on commit 64fd93c

Please sign in to comment.