diff --git a/frameworks/Scala/otavia/.mill-version b/frameworks/Scala/otavia/.mill-version
new file mode 100644
index 00000000000..772c67a500f
--- /dev/null
+++ b/frameworks/Scala/otavia/.mill-version
@@ -0,0 +1 @@
+0.11.8
\ No newline at end of file
diff --git a/frameworks/Scala/otavia/.scalafmt.conf b/frameworks/Scala/otavia/.scalafmt.conf
new file mode 100644
index 00000000000..ff856188419
--- /dev/null
+++ b/frameworks/Scala/otavia/.scalafmt.conf
@@ -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]
\ No newline at end of file
diff --git a/frameworks/Scala/otavia/README.MD b/frameworks/Scala/otavia/README.MD
new file mode 100644
index 00000000000..0ea11222811
--- /dev/null
+++ b/frameworks/Scala/otavia/README.MD
@@ -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)
\ No newline at end of file
diff --git a/frameworks/Scala/otavia/benchmark/src/app/controller/DBController.scala b/frameworks/Scala/otavia/benchmark/src/app/controller/DBController.scala
new file mode 100644
index 00000000000..6b54504aecc
--- /dev/null
+++ b/frameworks/Scala/otavia/benchmark/src/app/controller/DBController.scala
@@ -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"
+
+}
diff --git a/frameworks/Scala/otavia/benchmark/src/app/controller/FortuneController.scala b/frameworks/Scala/otavia/benchmark/src/app/controller/FortuneController.scala
new file mode 100644
index 00000000000..05ff8c468ba
--- /dev/null
+++ b/frameworks/Scala/otavia/benchmark/src/app/controller/FortuneController.scala
@@ -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"
+
+}
diff --git a/frameworks/Scala/otavia/benchmark/src/app/model/Fortune.scala b/frameworks/Scala/otavia/benchmark/src/app/model/Fortune.scala
new file mode 100644
index 00000000000..edb58d3cdc9
--- /dev/null
+++ b/frameworks/Scala/otavia/benchmark/src/app/model/Fortune.scala
@@ -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
diff --git a/frameworks/Scala/otavia/benchmark/src/app/model/Message.scala b/frameworks/Scala/otavia/benchmark/src/app/model/Message.scala
new file mode 100644
index 00000000000..a0d389090ae
--- /dev/null
+++ b/frameworks/Scala/otavia/benchmark/src/app/model/Message.scala
@@ -0,0 +1,5 @@
+package app.model
+
+import cc.otavia.json.JsonSerde
+
+case class Message(message: String) derives JsonSerde
diff --git a/frameworks/Scala/otavia/benchmark/src/app/model/World.scala b/frameworks/Scala/otavia/benchmark/src/app/model/World.scala
new file mode 100644
index 00000000000..a0e6f6056f8
--- /dev/null
+++ b/frameworks/Scala/otavia/benchmark/src/app/model/World.scala
@@ -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
diff --git a/frameworks/Scala/otavia/benchmark/src/app/startup.scala b/frameworks/Scala/otavia/benchmark/src/app/startup.scala
new file mode 100644
index 00000000000..32eb0c2e539
--- /dev/null
+++ b/frameworks/Scala/otavia/benchmark/src/app/startup.scala
@@ -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())
diff --git a/frameworks/Scala/otavia/benchmark/src/app/util/FortunesRender.scala b/frameworks/Scala/otavia/benchmark/src/app/util/FortunesRender.scala
new file mode 100644
index 00000000000..0c0694ee836
--- /dev/null
+++ b/frameworks/Scala/otavia/benchmark/src/app/util/FortunesRender.scala
@@ -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 =
+ "
Fortunesid | message |
"
+ .getBytes(StandardCharsets.UTF_8)
+
+ private val text2 = "".getBytes(StandardCharsets.UTF_8)
+
+ private val text3 = " | ".getBytes(StandardCharsets.UTF_8)
+
+ private val text4 = " |
".getBytes(StandardCharsets.UTF_8)
+
+ private val text5 = "
".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)
+
+}
diff --git a/frameworks/Scala/otavia/benchmark_config.json b/frameworks/Scala/otavia/benchmark_config.json
new file mode 100644
index 00000000000..ae712574778
--- /dev/null
+++ b/frameworks/Scala/otavia/benchmark_config.json
@@ -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"
+ }
+ }
+ ]
+}
diff --git a/frameworks/Scala/otavia/build.sc b/frameworks/Scala/otavia/build.sc
new file mode 100644
index 00000000000..2b7877b7891
--- /dev/null
+++ b/frameworks/Scala/otavia/build.sc
@@ -0,0 +1,15 @@
+import mill._
+import mill.scalalib._
+
+def otaviaVersion = "0.4.0"
+
+object benchmark extends ScalaModule {
+
+ override def scalaVersion = "3.3.1"
+
+ override def ivyDeps = Agg(
+ ivy"cc.otavia::otavia-codec-http:$otaviaVersion",
+ ivy"cc.otavia::otavia-postgres-driver:$otaviaVersion"
+ )
+
+}
diff --git a/frameworks/Scala/otavia/config.toml b/frameworks/Scala/otavia/config.toml
new file mode 100644
index 00000000000..96613dfd2fd
--- /dev/null
+++ b/frameworks/Scala/otavia/config.toml
@@ -0,0 +1,36 @@
+[framework]
+name = "otavia"
+
+[main]
+urls.plaintext = "/plaintext"
+urls.json = "/json"
+urls.db = "/db"
+urls.query = "/queries?queries="
+urls.update = "/updates?queries="
+urls.fortune = "/fortunes"
+approach = "Realistic"
+classification = "Micro"
+database = "Postgres"
+database_os = "Linux"
+os = "Linux"
+orm = "Micro"
+platform = "Otavia"
+webserver = "None"
+versus = "Otavia"
+
+[reserve]
+urls.plaintext = "/plaintext"
+urls.json = "/json"
+urls.db = "/db"
+urls.query = "/queries?queries="
+urls.update = "/updates?queries="
+urls.fortune = "/fortunes"
+approach = "Realistic"
+classification = "Micro"
+database = "Postgres"
+database_os = "Linux"
+os = "Linux"
+orm = "Micro"
+platform = "Otavia"
+webserver = "None"
+versus = "Otavia"
diff --git a/frameworks/Scala/otavia/millw b/frameworks/Scala/otavia/millw
new file mode 100644
index 00000000000..73bb4d0e4a0
--- /dev/null
+++ b/frameworks/Scala/otavia/millw
@@ -0,0 +1,194 @@
+#!/usr/bin/env sh
+
+# This is a wrapper script, that automatically download mill from GitHub release pages
+# You can give the required mill version with --mill-version parameter
+# If no version is given, it falls back to the value of DEFAULT_MILL_VERSION
+#
+# Project page: https://github.com/lefou/millw
+# Script Version: 0.4.6
+#
+# If you want to improve this script, please also contribute your changes back!
+#
+# Licensed under the Apache License, Version 2.0
+
+set -e
+
+if [ -z "${DEFAULT_MILL_VERSION}" ] ; then
+ DEFAULT_MILL_VERSION="0.10.10"
+fi
+
+
+if [ -z "${GITHUB_RELEASE_CDN}" ] ; then
+ GITHUB_RELEASE_CDN=""
+fi
+
+
+MILL_REPO_URL="https://github.com/com-lihaoyi/mill"
+
+if [ -z "${CURL_CMD}" ] ; then
+ CURL_CMD=curl
+fi
+
+# Explicit commandline argument takes precedence over all other methods
+if [ "$1" = "--mill-version" ] ; then
+ shift
+ if [ "x$1" != "x" ] ; then
+ MILL_VERSION="$1"
+ shift
+ else
+ echo "You specified --mill-version without a version." 1>&2
+ echo "Please provide a version that matches one provided on" 1>&2
+ echo "${MILL_REPO_URL}/releases" 1>&2
+ false
+ fi
+fi
+
+# Please note, that if a MILL_VERSION is already set in the environment,
+# We reuse it's value and skip searching for a value.
+
+# If not already set, read .mill-version file
+if [ -z "${MILL_VERSION}" ] ; then
+ if [ -f ".mill-version" ] ; then
+ MILL_VERSION="$(head -n 1 .mill-version 2> /dev/null)"
+ elif [ -f ".config/mill-version" ] ; then
+ MILL_VERSION="$(head -n 1 .config/mill-version 2> /dev/null)"
+ fi
+fi
+
+if [ -n "${XDG_CACHE_HOME}" ] ; then
+ MILL_DOWNLOAD_PATH="${XDG_CACHE_HOME}/mill/download"
+else
+ MILL_DOWNLOAD_PATH="${HOME}/.cache/mill/download"
+fi
+
+# If not already set, try to fetch newest from Github
+if [ -z "${MILL_VERSION}" ] ; then
+ # TODO: try to load latest version from release page
+ echo "No mill version specified." 1>&2
+ echo "You should provide a version via '.mill-version' file or --mill-version option." 1>&2
+
+ mkdir -p "${MILL_DOWNLOAD_PATH}"
+ LANG=C touch -d '1 hour ago' "${MILL_DOWNLOAD_PATH}/.expire_latest" 2>/dev/null || (
+ # we might be on OSX or BSD which don't have -d option for touch
+ # but probably a -A [-][[hh]mm]SS
+ touch "${MILL_DOWNLOAD_PATH}/.expire_latest"; touch -A -010000 "${MILL_DOWNLOAD_PATH}/.expire_latest"
+ ) || (
+ # in case we still failed, we retry the first touch command with the intention
+ # to show the (previously suppressed) error message
+ LANG=C touch -d '1 hour ago' "${MILL_DOWNLOAD_PATH}/.expire_latest"
+ )
+
+ # POSIX shell variant of bash's -nt operator, see https://unix.stackexchange.com/a/449744/6993
+ # if [ "${MILL_DOWNLOAD_PATH}/.latest" -nt "${MILL_DOWNLOAD_PATH}/.expire_latest" ] ; then
+ if [ -n "$(find -L "${MILL_DOWNLOAD_PATH}/.latest" -prune -newer "${MILL_DOWNLOAD_PATH}/.expire_latest")" ]; then
+ # we know a current latest version
+ MILL_VERSION=$(head -n 1 "${MILL_DOWNLOAD_PATH}"/.latest 2> /dev/null)
+ fi
+
+ if [ -z "${MILL_VERSION}" ] ; then
+ # we don't know a current latest version
+ echo "Retrieving latest mill version ..." 1>&2
+ LANG=C ${CURL_CMD} -s -i -f -I ${MILL_REPO_URL}/releases/latest 2> /dev/null | grep --ignore-case Location: | sed s'/^.*tag\///' | tr -d '\r\n' > "${MILL_DOWNLOAD_PATH}/.latest"
+ MILL_VERSION=$(head -n 1 "${MILL_DOWNLOAD_PATH}"/.latest 2> /dev/null)
+ fi
+
+ if [ -z "${MILL_VERSION}" ] ; then
+ # Last resort
+ MILL_VERSION="${DEFAULT_MILL_VERSION}"
+ echo "Falling back to hardcoded mill version ${MILL_VERSION}" 1>&2
+ else
+ echo "Using mill version ${MILL_VERSION}" 1>&2
+ fi
+fi
+
+MILL="${MILL_DOWNLOAD_PATH}/${MILL_VERSION}"
+
+try_to_use_system_mill() {
+ MILL_IN_PATH="$(command -v mill || true)"
+
+ if [ -z "${MILL_IN_PATH}" ]; then
+ return
+ fi
+
+ UNIVERSAL_SCRIPT_MAGIC="@ 2>/dev/null # 2>nul & echo off & goto BOF"
+
+ if ! head -c 128 "${MILL_IN_PATH}" | grep -qF "${UNIVERSAL_SCRIPT_MAGIC}"; then
+ if [ -n "${MILLW_VERBOSE}" ]; then
+ echo "Could not determine mill version of ${MILL_IN_PATH}, as it does not start with the universal script magic2" 1>&2
+ fi
+ return
+ fi
+
+ # Roughly the size of the universal script.
+ MILL_VERSION_SEARCH_RANGE="2403"
+ MILL_IN_PATH_VERSION=$(head -c "${MILL_VERSION_SEARCH_RANGE}" "${MILL_IN_PATH}" |\
+ sed -n 's/^.*-DMILL_VERSION=\([^\s]*\) .*$/\1/p' |\
+ head -n 1)
+
+ if [ -z "${MILL_IN_PATH_VERSION}" ]; then
+ echo "Could not determine mill version, even though ${MILL_IN_PATH} has the universal script magic" 1>&2
+ return
+ fi
+
+ if [ "${MILL_IN_PATH_VERSION}" = "${MILL_VERSION}" ]; then
+ MILL="${MILL_IN_PATH}"
+ fi
+}
+try_to_use_system_mill
+
+# If not already downloaded, download it
+if [ ! -s "${MILL}" ] ; then
+
+ # support old non-XDG download dir
+ MILL_OLD_DOWNLOAD_PATH="${HOME}/.mill/download"
+ OLD_MILL="${MILL_OLD_DOWNLOAD_PATH}/${MILL_VERSION}"
+ if [ -x "${OLD_MILL}" ] ; then
+ MILL="${OLD_MILL}"
+ else
+ VERSION_PREFIX="$(echo $MILL_VERSION | cut -b -4)"
+ case $VERSION_PREFIX in
+ 0.0. | 0.1. | 0.2. | 0.3. | 0.4. )
+ DOWNLOAD_SUFFIX=""
+ ;;
+ *)
+ DOWNLOAD_SUFFIX="-assembly"
+ ;;
+ esac
+ unset VERSION_PREFIX
+
+ DOWNLOAD_FILE=$(mktemp mill.XXXXXX)
+ MILL_VERSION_TAG=$(echo $MILL_VERSION | sed -E 's/([^-]+)(-M[0-9]+)?(-.*)?/\1\2/')
+ DOWNLOAD_URL="${GITHUB_RELEASE_CDN}${MILL_REPO_URL}/releases/download/${MILL_VERSION_TAG}/${MILL_VERSION}${DOWNLOAD_SUFFIX}"
+ # TODO: handle command not found
+ echo "Downloading mill ${MILL_VERSION} from ${DOWNLOAD_URL} ..." 1>&2
+ ${CURL_CMD} -f -L -o "${DOWNLOAD_FILE}" ${DOWNLOAD_URL}
+ chmod +x "${DOWNLOAD_FILE}"
+ mkdir -p "${MILL_DOWNLOAD_PATH}"
+ mv "${DOWNLOAD_FILE}" "${MILL}"
+
+ unset DOWNLOAD_FILE
+ unset DOWNLOAD_SUFFIX
+ fi
+fi
+
+if [ -z "$MILL_MAIN_CLI" ] ; then
+ MILL_MAIN_CLI="${0}"
+fi
+
+MILL_FIRST_ARG=""
+if [ "$1" = "--bsp" ] || [ "$1" = "-i" ] || [ "$1" = "--interactive" ] || [ "$1" = "--no-server" ] || [ "$1" = "--repl" ] || [ "$1" = "--help" ] ; then
+ # Need to preserve the first position of those listed options
+ MILL_FIRST_ARG=$1
+ shift
+fi
+
+unset MILL_DOWNLOAD_PATH
+unset MILL_OLD_DOWNLOAD_PATH
+unset OLD_MILL
+unset MILL_VERSION
+unset MILL_VERSION_TAG
+unset MILL_REPO_URL
+
+# We don't quote MILL_FIRST_ARG on purpose, so we can expand the empty value without quotes
+# shellcheck disable=SC2086
+exec "${MILL}" $MILL_FIRST_ARG -D "mill.main.cli=${MILL_MAIN_CLI}" "$@"
diff --git a/frameworks/Scala/otavia/millw.bat b/frameworks/Scala/otavia/millw.bat
new file mode 100644
index 00000000000..6359e35e500
--- /dev/null
+++ b/frameworks/Scala/otavia/millw.bat
@@ -0,0 +1,173 @@
+@echo off
+
+rem This is a wrapper script, that automatically download mill from GitHub release pages
+rem You can give the required mill version with --mill-version parameter
+rem If no version is given, it falls back to the value of DEFAULT_MILL_VERSION
+rem
+rem Project page: https://github.com/lefou/millw
+rem Script Version: 0.4.6
+rem
+rem If you want to improve this script, please also contribute your changes back!
+rem
+rem Licensed under the Apache License, Version 2.0
+
+rem setlocal seems to be unavailable on Windows 95/98/ME
+rem but I don't think we need to support them in 2019
+setlocal enabledelayedexpansion
+
+if [!DEFAULT_MILL_VERSION!]==[] (
+ set "DEFAULT_MILL_VERSION=0.10.10"
+)
+
+if [!GITHUB_RELEASE_CDN!]==[] (
+ set "GITHUB_RELEASE_CDN="
+)
+
+set "MILL_REPO_URL=https://github.com/com-lihaoyi/mill"
+
+rem %~1% removes surrounding quotes
+if [%~1%]==[--mill-version] (
+ if not [%~2%]==[] (
+ set MILL_VERSION=%~2%
+ rem shift command doesn't work within parentheses
+ set "STRIP_VERSION_PARAMS=true"
+ ) else (
+ echo You specified --mill-version without a version. 1>&2
+ echo Please provide a version that matches one provided on 1>&2
+ echo %MILL_REPO_URL%/releases 1>&2
+ exit /b 1
+ )
+)
+
+if not defined STRIP_VERSION_PARAMS GOTO AfterStripVersionParams
+rem strip the: --mill-version {version}
+shift
+shift
+:AfterStripVersionParams
+
+if [!MILL_VERSION!]==[] (
+ if exist .mill-version (
+ set /p MILL_VERSION=<.mill-version
+ ) else (
+ if exist .config\mill-version (
+ set /p MILL_VERSION=<.config\mill-version
+ )
+ )
+)
+
+if [!MILL_VERSION!]==[] (
+ set MILL_VERSION=%DEFAULT_MILL_VERSION%
+)
+
+set MILL_DOWNLOAD_PATH=%USERPROFILE%\.mill\download
+
+rem without bat file extension, cmd doesn't seem to be able to run it
+set MILL=%MILL_DOWNLOAD_PATH%\!MILL_VERSION!.bat
+
+if not exist "%MILL%" (
+ set VERSION_PREFIX=%MILL_VERSION:~0,4%
+ set DOWNLOAD_SUFFIX=-assembly
+ if [!VERSION_PREFIX!]==[0.0.] set DOWNLOAD_SUFFIX=
+ if [!VERSION_PREFIX!]==[0.1.] set DOWNLOAD_SUFFIX=
+ if [!VERSION_PREFIX!]==[0.2.] set DOWNLOAD_SUFFIX=
+ if [!VERSION_PREFIX!]==[0.3.] set DOWNLOAD_SUFFIX=
+ if [!VERSION_PREFIX!]==[0.4.] set DOWNLOAD_SUFFIX=
+ set VERSION_PREFIX=
+
+ for /F "delims=- tokens=1" %%A in ("!MILL_VERSION!") do set MILL_VERSION_BASE=%%A
+ for /F "delims=- tokens=2" %%A in ("!MILL_VERSION!") do set MILL_VERSION_MILESTONE=%%A
+ set VERSION_MILESTONE_START=!MILL_VERSION_MILESTONE:~0,1!
+ if [!VERSION_MILESTONE_START!]==[M] (
+ set MILL_VERSION_TAG="!MILL_VERSION_BASE!-!MILL_VERSION_MILESTONE!"
+ ) else (
+ set MILL_VERSION_TAG=!MILL_VERSION_BASE!
+ )
+
+ rem there seems to be no way to generate a unique temporary file path (on native Windows)
+ set DOWNLOAD_FILE=%MILL%.tmp
+
+ set DOWNLOAD_URL=!GITHUB_RELEASE_CDN!%MILL_REPO_URL%/releases/download/!MILL_VERSION_TAG!/!MILL_VERSION!!DOWNLOAD_SUFFIX!
+
+ echo Downloading mill %MILL_VERSION% from !DOWNLOAD_URL! ... 1>&2
+
+ if not exist "%MILL_DOWNLOAD_PATH%" mkdir "%MILL_DOWNLOAD_PATH%"
+ rem curl is bundled with recent Windows 10
+ rem but I don't think we can expect all the users to have it in 2019
+ where /Q curl
+ if %ERRORLEVEL% EQU 0 (
+ curl -f -L "!DOWNLOAD_URL!" -o "!DOWNLOAD_FILE!"
+ ) else (
+ rem bitsadmin seems to be available on Windows 7
+ rem without /dynamic, github returns 403
+ rem bitsadmin is sometimes needlessly slow but it looks better with /priority foreground
+ bitsadmin /transfer millDownloadJob /dynamic /priority foreground "!DOWNLOAD_URL!" "!DOWNLOAD_FILE!"
+ )
+ if not exist "!DOWNLOAD_FILE!" (
+ echo Could not download mill %MILL_VERSION% 1>&2
+ exit /b 1
+ )
+
+ move /y "!DOWNLOAD_FILE!" "%MILL%"
+
+ set DOWNLOAD_FILE=
+ set DOWNLOAD_SUFFIX=
+)
+
+set MILL_DOWNLOAD_PATH=
+set MILL_VERSION=
+set MILL_REPO_URL=
+
+if [!MILL_MAIN_CLI!]==[] (
+ set "MILL_MAIN_CLI=%0"
+)
+
+rem Need to preserve the first position of those listed options
+set MILL_FIRST_ARG=
+if [%~1%]==[--bsp] (
+ set MILL_FIRST_ARG=%1%
+) else (
+ if [%~1%]==[-i] (
+ set MILL_FIRST_ARG=%1%
+ ) else (
+ if [%~1%]==[--interactive] (
+ set MILL_FIRST_ARG=%1%
+ ) else (
+ if [%~1%]==[--no-server] (
+ set MILL_FIRST_ARG=%1%
+ ) else (
+ if [%~1%]==[--repl] (
+ set MILL_FIRST_ARG=%1%
+ ) else (
+ if [%~1%]==[--help] (
+ set MILL_FIRST_ARG=%1%
+ )
+ )
+ )
+ )
+ )
+)
+
+set "MILL_PARAMS=%*%"
+
+if not [!MILL_FIRST_ARG!]==[] (
+ if defined STRIP_VERSION_PARAMS (
+ for /f "tokens=1-3*" %%a in ("%*") do (
+ set "MILL_PARAMS=%%d"
+ )
+ ) else (
+ for /f "tokens=1*" %%a in ("%*") do (
+ set "MILL_PARAMS=%%b"
+ )
+ )
+) else (
+ if defined STRIP_VERSION_PARAMS (
+ for /f "tokens=1-2*" %%a in ("%*") do (
+ rem strip %%a - It's the "--mill-version" option.
+ rem strip %%b - it's the version number that comes after the option.
+ rem keep %%c - It's the remaining options.
+ set "MILL_PARAMS=%%c"
+ )
+ )
+)
+
+"%MILL%" %MILL_FIRST_ARG% -D "mill.main.cli=%MILL_MAIN_CLI%" %MILL_PARAMS%
diff --git a/frameworks/Scala/otavia/otavia-reserve.dockerfile b/frameworks/Scala/otavia/otavia-reserve.dockerfile
new file mode 100644
index 00000000000..5768000706e
--- /dev/null
+++ b/frameworks/Scala/otavia/otavia-reserve.dockerfile
@@ -0,0 +1,15 @@
+FROM nightscape/scala-mill:eclipse-temurin-17.0.8.1_1-jdk-focal_0.11.6_3.3.0
+WORKDIR /otavia
+COPY benchmark benchmark
+COPY build.sc build.sc
+ENV COURSIER_REPOSITORIES=ivy2Local|central
+RUN mill benchmark.assembly
+
+EXPOSE 8080
+
+CMD java -server \
+ -Dcc.otavia.actor.worker.size=18 -Dcc.otavia.nio.worker.size=36 \
+ -jar \
+ out/benchmark/assembly.dest/out.jar \
+ jdbc:postgresql://tfb-database:5432/hello_world \
+ benchmarkdbuser benchmarkdbpass 54
diff --git a/frameworks/Scala/otavia/otavia.dockerfile b/frameworks/Scala/otavia/otavia.dockerfile
new file mode 100644
index 00000000000..c1c944d60aa
--- /dev/null
+++ b/frameworks/Scala/otavia/otavia.dockerfile
@@ -0,0 +1,15 @@
+FROM nightscape/scala-mill:eclipse-temurin-17.0.8.1_1-jdk-focal_0.11.6_3.3.0
+WORKDIR /otavia
+COPY benchmark benchmark
+COPY build.sc build.sc
+ENV COURSIER_REPOSITORIES=ivy2Local|central
+RUN mill benchmark.assembly
+
+EXPOSE 8080
+
+CMD java -server \
+ -Dcc.otavia.actor.worker.size=24 -Dcc.otavia.nio.worker.size=48 \
+ -jar \
+ out/benchmark/assembly.dest/out.jar \
+ jdbc:postgresql://tfb-database:5432/hello_world \
+ benchmarkdbuser benchmarkdbpass 72