From 61987a0d309e264f3721cc466962a959d92fa6e3 Mon Sep 17 00:00:00 2001 From: Lorenzo Gabriele Date: Mon, 9 Oct 2023 10:17:28 +0200 Subject: [PATCH] Update Cats Effect to 3.6 - Drop scala-native-loop and epollcat - Drop support for Future and AsyncServerBuilder --- .mill-version | 2 +- README.md | 66 ++++----- build.sc | 52 ++----- .../tests/http4s-helloworld/src/Main.scala | 11 +- .../src/Main.scala | 8 +- .../tapir-helloworld-future/src/Main.scala | 19 --- .../src/snunit/CEAsyncServerBuilder.scala | 127 ++++++++++-------- ...nalEventPollingExecutorSchedulerImpl.scala | 20 --- .../EventPollingExecutorScheduler.scala | 14 -- .../EventPollingExecutorScheduler.scala | 16 --- snunit-http4s/src/snunit/Http4sApp.scala | 3 +- snunit-http4s/src/snunit/http4s/Impl.scala | 37 ++--- .../snunit/http4s/SNUnitServerBuilder.scala | 5 +- .../src/EventPollingExecutorScheduler.java | 14 -- snunit-mill-plugin/src/SNUnit.scala | 1 - .../src/snunit/TapirApp.scala | 3 +- .../src/snunit/tapir/SNUnitServer.scala | 3 - .../snunit/tapir/SNUnitServerBuilder.scala | 26 ++-- .../tapir/SNUnitFutureServerInterpreter.scala | 15 --- snunit/src/snunit/AsyncServer.scala | 7 - versions.sc | 19 ++- 21 files changed, 169 insertions(+), 299 deletions(-) delete mode 100644 integration/tests/tapir-helloworld-future/src/Main.scala rename snunit/src/snunit/AsyncServerBuilder.scala => snunit-async-cats-effect/src/snunit/CEAsyncServerBuilder.scala (50%) delete mode 100644 snunit-async-epollcat/src/epollcat/unsafe/InternalEventPollingExecutorSchedulerImpl.scala delete mode 100644 snunit-async-epollcat/src/snunit/EventPollingExecutorScheduler.scala delete mode 100644 snunit-async-loop/src/snunit/EventPollingExecutorScheduler.scala delete mode 100644 snunit-internal-api/src/EventPollingExecutorScheduler.java delete mode 100644 snunit-tapir-cats-effect/src/snunit/tapir/SNUnitServer.scala delete mode 100644 snunit-tapir/src/snunit/tapir/SNUnitFutureServerInterpreter.scala delete mode 100644 snunit/src/snunit/AsyncServer.scala diff --git a/.mill-version b/.mill-version index e5cbde3..03ce552 100644 --- a/.mill-version +++ b/.mill-version @@ -1 +1 @@ -0.11.6 +0.11.7-70-654f58 diff --git a/README.md b/README.md index ae51693..ff00542 100644 --- a/README.md +++ b/README.md @@ -2,20 +2,19 @@ ```scala import snunit.* -object HelloWorldExample { - def main(args: Array[String]): Unit = { - SyncServerBuilder - .setRequestHandler(req => - req.send( - statusCode = StatusCode.OK, - content = "Hello world!\n", - headers = Headers("Content-Type" -> "text/plain") - ) + +@main +def run = + SyncServerBuilder + .setRequestHandler(req => + req.send( + statusCode = StatusCode.OK, + content = "Hello world!\n", + headers = Headers("Content-Type" -> "text/plain") ) - .build() - .listen() - } -} + ) + .build() + .listen() ``` SNUnit is a Scala Native library to write HTTP server applications on top of @@ -96,14 +95,10 @@ anything else while listening. Moreover, all the request handlers need to respond directly and can't be implemented using `Future`s or any other asyncronous mechanism since no `Future` will run, being the process stuck on the `listen()` Unit event loop. -With `AsyncServerBuilder` the server is automatically scheduled to run either on the -[scala-native-loop](https://github.com/scala-native/scala-native-loop) event loop -(based on the libuv library) or [epollcat](https://github.com/armanbilge/epollcat) event -loop, based on epoll/kqueue. +With http4s or tapir-cats-effect the server is automatically scheduled to run either on the +cats effect event loop, based on epoll/kqueue. This allows you to complete requests asyncronously using whatever mechanism you prefer. A process can accept multiple requests concurrently, allowing great parallelism. -Add either `snunit-async-loop` or `snunit-async-epollcat` to decide what implementation -you want to use. ## Tapir support @@ -111,17 +106,15 @@ SNUnit offers interpreters for [Tapir](https://tapir.softwaremill.com) server en You can write all your application using Tapir and the convert your Tapir endpoints with logic into a SNUnit `Handler`. -Currently three interpreters are available: +Currently two interpreters are available: - `SNUnitIdServerInterpreter` which works best with `SyncServerHandler` for synchronous applications - You can find an example [in tests](./integration/tests/tapir-helloworld/src/Main.scala) -- `SNUnitFutureServerInterpreter` which requires `AsyncServerHandler` for asynchronous applications - - You can find an example [in tests](./integration/tests/tapir-helloworld-future/src/Main.scala) - An interpreter for cats hidden behind `snunit.tapir.SNUnitServerBuilder` in the `snunit-tapir-cats-effect` artifact. - You can find an example [in tests](./integration/tests/tapir-helloworld-cats-effect/src/Main.scala) ### Automatic server creation -`snunit.TapirApp` extends `epollcat.EpollApp` building the SNUnit server. +`snunit.TapirApp` extends `cats.effect.IOApp` building the SNUnit server. It exposes a `def serverEndpoints: Resource[IO, List[ServerEndpoint[Any, IO]]]` that you need to implement with your server logic. @@ -152,7 +145,7 @@ There are two ways you can build a http4s server. ### Automatic server creation -`snunit.Http4sApp` extends `epollcat.EpollApp` building the SNUnit server. +`snunit.Http4sApp` extends `cats.effect.IOApp` building the SNUnit server. It exposes a `def routes: Resource[IO, HttpApp[IO]]` that you need to implement with your server logic. @@ -160,9 +153,9 @@ server logic. Here an example "Hello world" app: ```scala -import cats.effect._ -import org.http4s._ -import org.http4s.dsl.io._ +import cats.effect.* +import org.http4s.* +import org.http4s.dsl.io.* object app extends snunit.Http4sApp { def routes = Resource.pure( @@ -180,29 +173,26 @@ object app extends snunit.Http4sApp { If you want to have more control over the server creation, you can use the `SNUnitServerBuilder` and manually use it. -For example, here you see it in combination with `epollcat.EpollApp` +For example, here you see it in combination with `cats.effect.IOApp` ```scala package snunit.tests -import cats.effect._ -import epollcat.EpollApp -import org.http4s._ -import org.http4s.dsl.io._ -import snunit.http4s._ +import cats.effect.* +import org.http4s.* +import org.http4s.dsl.io.* +import snunit.http4s.* -object Http4sHelloWorld extends EpollApp.Simple { - def helloWorldRoutes: HttpRoutes[IO] = { +object Http4sHelloWorld extends IOApp.Simple { + def helloWorldRoutes: HttpRoutes[IO] = HttpRoutes.of[IO] { case GET -> Root => Ok("Hello Http4s!") } - } - def run: IO[Unit] = { + def run: IO[Unit] = SNUnitServerBuilder .default[IO] .withHttpApp(helloWorldRoutes.orNotFound) .run - } } ``` diff --git a/build.sc b/build.sc index b8acc31..e052f2f 100644 --- a/build.sc +++ b/build.sc @@ -97,35 +97,21 @@ trait Publish extends CiReleaseModule with Mima { ) def mimaPreviousVersions = Seq("0.8.0") } -object `snunit-internal-api` extends JavaModule object snunit extends Cross[SNUnitModule](scalaVersions) trait SNUnitModule extends Common.Cross with Publish { - def compileModuleDeps = Seq(`snunit-internal-api`) object test extends ScalaNativeTests with TestModule.Utest { def ivyDeps = super.ivyDeps() ++ Agg(utest) } } -object `snunit-async-loop` extends Cross[SNUnitAsyncModule](scalaVersions) -trait SNUnitAsyncModule extends Common.Cross with Publish { +object `snunit-async-cats-effect` extends Cross[SNUnitAsyncCatsEffectModule](scalaVersions) +trait SNUnitAsyncCatsEffectModule extends Common.Cross with Publish { def moduleDeps = Seq(snunit()) def ivyDeps = T { super.ivyDeps() ++ Agg( - ivy"com.github.lolgab::native-loop-core::${Versions.scalaNativeLoop}" - ) - } -} - -object `snunit-async-epollcat` extends Cross[SNUnitAsyncEpollcatModule](scalaVersions) -trait SNUnitAsyncEpollcatModule extends Common.Cross with Publish { - def moduleDeps = Seq(snunit()) - - def ivyDeps = - T { - super.ivyDeps() ++ Agg( - ivy"com.armanbilge::epollcat::${Versions.epollcat}" + ivy"org.typelevel::cats-effect::${Versions.catsEffect}" ) } } @@ -143,8 +129,8 @@ trait SNUnitTapirModule extends Common.Cross with Publish { object `snunit-tapir-cats-effect` extends Cross[SNUnitTapirCatsEffect](scalaVersions) trait SNUnitTapirCatsEffect extends Common.Cross with Publish { def moduleDeps = Seq( - `snunit-tapir`(crossScalaVersion), - `snunit-async-epollcat`(crossScalaVersion) + `snunit-tapir`(), + `snunit-async-cats-effect`() ) def ivyDeps = super.ivyDeps() ++ Agg( ivy"com.softwaremill.sttp.tapir::tapir-cats-effect::${Versions.tapir}" @@ -156,14 +142,16 @@ trait SNUnitHttp4s extends Common.Cross with Cross.Module2[String, String] with val http4sVersion = crossValue2 def moduleDeps = Seq( snunit(), - `snunit-async-epollcat`(crossScalaVersion) + `snunit-async-cats-effect`() ) val http4sBinaryVersion = http4sVersion match { case s"0.23.$_" => "0.23" case s"1.$_" => "1" } def artifactName = s"snunit-http4s$http4sBinaryVersion" - def ivyDeps = super.ivyDeps() ++ Agg(ivy"org.http4s::http4s-server::$http4sVersion") + def ivyDeps = super.ivyDeps() ++ Agg( + ivy"org.http4s::http4s-server::$http4sVersion" + ) def sources = T.sources { super.sources() ++ Agg(PathRef(millSourcePath / s"http4s-$http4sBinaryVersion" / "src")) } @@ -206,15 +194,6 @@ object integration extends ScalaModule { object `multiple-handlers` extends Common.Scala3Only { def moduleDeps = Seq(snunit(crossScalaVersion)) } - object async extends Common.Scala3Only { - def moduleDeps = Seq(`snunit-async-loop`(crossScalaVersion)) - } - object `async-epollcat` extends Common.Scala3Only { - def moduleDeps = Seq(`snunit-async-epollcat`(crossScalaVersion)) - } - object `async-multiple-handlers` extends Common.Scala3Only { - def moduleDeps = Seq(`snunit-async-loop`(crossScalaVersion)) - } object `undertow-helloworld` extends CrossPlatform { object native extends CrossPlatformScalaModule with Common.Scala3Only { def moduleDeps = Seq(`snunit-undertow`(crossScalaVersion)) @@ -258,12 +237,6 @@ object integration extends ScalaModule { ivy"org.http4s::http4s-dsl::$http4sVersion" ) } - object `tapir-helloworld-future` extends Common.Scala3Only { - def moduleDeps = Seq( - `snunit-async-loop`(crossScalaVersion), - `snunit-tapir`(crossScalaVersion) - ) - } object `tapir-helloworld-cats-effect` extends Common.Scala3Only { def moduleDeps = Seq( `snunit-tapir-cats-effect`(crossScalaVersion) @@ -276,7 +249,6 @@ object integration extends ScalaModule { BuildInfo.Value("port", testServerPort.toString), BuildInfo.Value("scalaVersions", scalaVersions.mkString(":")), BuildInfo.Value("http4sVersions", http4sVersions.mkString(":")), - BuildInfo.Value("scala213", Versions.scala213), BuildInfo.Value("unitControl", unitd.control.toString) ) def buildInfoPackageName = "snunit.test" @@ -303,15 +275,15 @@ trait SnunitPluginsShared extends CrossScalaModule with Publish with BuildInfo { } } object `snunit-mill-plugin` extends ScalaModule with Publish { - def artifactName = s"mill-snunit_mill${Versions.mill010.split('.').take(2).mkString(".")}" + def artifactName = s"mill-snunit_mill${Versions.mill011.split('.').take(2).mkString(".")}" def moduleDeps = Seq(`snunit-plugins-shared`(Versions.scala213)) def scalaVersion = Versions.scala213 def compileIvyDeps = super.compileIvyDeps() ++ Agg( - ivy"com.lihaoyi::mill-scalanativelib:${Versions.mill010}" + ivy"com.lihaoyi::mill-scalanativelib:${Versions.mill011}" ) } object `snunit-mill-plugin-itest` extends MillIntegrationTestModule { - def millTestVersion = Versions.mill010 + def millTestVersion = Versions.mill011 def pluginsUnderTest = Seq(`snunit-mill-plugin`) def temporaryIvyModules = Seq(`snunit-plugins-shared`(Versions.scala213), snunit(Versions.scala3)) } diff --git a/integration/tests/http4s-helloworld/src/Main.scala b/integration/tests/http4s-helloworld/src/Main.scala index 9e5f2d4..62bb67f 100644 --- a/integration/tests/http4s-helloworld/src/Main.scala +++ b/integration/tests/http4s-helloworld/src/Main.scala @@ -1,12 +1,11 @@ package snunit.tests -import cats.effect._ -import epollcat.EpollApp -import org.http4s._ -import org.http4s.dsl.io._ -import snunit.http4s._ +import cats.effect.* +import org.http4s.* +import org.http4s.dsl.io.* +import snunit.http4s.* -object Http4sHelloWorld extends EpollApp.Simple { +object Http4sHelloWorld extends IOApp.Simple { def helloWorldRoutes: HttpRoutes[IO] = { HttpRoutes.of[IO] { case GET -> Root => Ok("Hello Http4s!") diff --git a/integration/tests/tapir-helloworld-cats-effect/src/Main.scala b/integration/tests/tapir-helloworld-cats-effect/src/Main.scala index b0ec0e3..5b016b1 100644 --- a/integration/tests/tapir-helloworld-cats-effect/src/Main.scala +++ b/integration/tests/tapir-helloworld-cats-effect/src/Main.scala @@ -1,10 +1,10 @@ package snunit.tests -import cats.effect._ -import snunit.tapir._ -import sttp.tapir._ +import cats.effect.* +import snunit.tapir.* +import sttp.tapir.* -object TapirHelloWorldIO extends epollcat.EpollApp.Simple { +object TapirHelloWorldIO extends IOApp.Simple { val helloWorld = endpoint.get .in("hello") .in(query[String]("name")) diff --git a/integration/tests/tapir-helloworld-future/src/Main.scala b/integration/tests/tapir-helloworld-future/src/Main.scala deleted file mode 100644 index 17705b9..0000000 --- a/integration/tests/tapir-helloworld-future/src/Main.scala +++ /dev/null @@ -1,19 +0,0 @@ -package snunit.tests - -import snunit.tapir.SNUnitFutureServerInterpreter._ -import sttp.tapir._ - -import scala.concurrent.Future - -object TapirHelloWorldFuture { - val helloWorld = endpoint.get - .in("hello") - .in(query[String]("name")) - .out(stringBody) - .serverLogic[Future](name => Future.successful(Right(s"Hello $name!"))) - - def main(args: Array[String]): Unit = - snunit.AsyncServerBuilder - .setRequestHandler(toHandler(helloWorld :: Nil)) - .build() -} diff --git a/snunit/src/snunit/AsyncServerBuilder.scala b/snunit-async-cats-effect/src/snunit/CEAsyncServerBuilder.scala similarity index 50% rename from snunit/src/snunit/AsyncServerBuilder.scala rename to snunit-async-cats-effect/src/snunit/CEAsyncServerBuilder.scala index 7837a44..149e66f 100644 --- a/snunit/src/snunit/AsyncServerBuilder.scala +++ b/snunit-async-cats-effect/src/snunit/CEAsyncServerBuilder.scala @@ -2,6 +2,9 @@ package snunit import snunit.unsafe.{*, given} +import cats.effect.* +import cats.effect.std.Dispatcher + import scala.annotation.tailrec import scala.scalanative.libc.errno.errno import scala.scalanative.libc.string.strerror @@ -13,10 +16,30 @@ import scala.scalanative.runtime.fromRawPtr import scala.scalanative.runtime.toRawPtr import scala.scalanative.unsafe.* import scala.util.control.NonFatal +import scala.concurrent.Future -object AsyncServerBuilder { +private[snunit] object CEAsyncServerBuilder { private val initArray: Array[Byte] = new Array[Byte](sizeof[nxt_unit_init_t].toInt) private val init: nxt_unit_init_t_* = initArray.at(0).asInstanceOf[nxt_unit_init_t_*] + + private var dispatcher: Dispatcher[IO] = dispatcher + def setDispatcher[F[_]: LiftIO](dispatcher: Dispatcher[F]): this.type = + this.dispatcher = new Dispatcher[IO] { + override def unsafeToFutureCancelable[A](fa: IO[A]): (Future[A], () => Future[Unit]) = + dispatcher.unsafeToFutureCancelable[A](summon[LiftIO[F]].liftIO(fa)) + } + this + + private var poller: FileDescriptorPoller = null + def setFileDescriptorPoller(poller: FileDescriptorPoller): this.type = + this.poller = poller + this + + private var shutdownDeferred: Deferred[IO, IO[Unit]] = null + def setShutdownDeferred(deferred: Deferred[IO, IO[Unit]]): this.type = + this.shutdownDeferred = deferred + this + def setRequestHandler(requestHandler: RequestHandler): this.type = { ServerBuilder.setRequestHandler(requestHandler) this @@ -25,22 +48,16 @@ object AsyncServerBuilder { ServerBuilder.setWebsocketHandler(websocketHandler) this } - private var shutdownHandler: (() => Unit) => Unit = shutdown => shutdown() - def setShutdownHandler(shutdownHandler: (() => Unit) => Unit): this.type = { - this.shutdownHandler = shutdownHandler - this - } - def build(): AsyncServer = { + def build: IO[Unit] = IO { ServerBuilder.setBaseHandlers(init) - init.callbacks.add_port = AsyncServerBuilder.add_port - init.callbacks.remove_port = AsyncServerBuilder.remove_port - init.callbacks.quit = AsyncServerBuilder.quit - val ctx: nxt_unit_ctx_t_* = nxt_unit_init(init) - if (ctx.isNull) { - throw new Exception("Failed to create Unit object") - } - new AsyncServer(ctx) - } + init.callbacks.add_port = CEAsyncServerBuilder.add_port + init.callbacks.remove_port = CEAsyncServerBuilder.remove_port + init.callbacks.quit = CEAsyncServerBuilder.quit + nxt_unit_init(init) + }.flatMap(ctx => + if (ctx.isNull) IO.raiseError(new Exception("Failed to create Unit object")) + else IO.unit + ) private val add_port: add_port_t = add_port_t { (ctx: nxt_unit_ctx_t_*, port: nxt_unit_port_t_*) => { @@ -67,19 +84,18 @@ object AsyncServerBuilder { } } - private val remove_port: remove_port_t = remove_port_t { - (_: nxt_unit_t_*, ctx: nxt_unit_ctx_t_*, port: nxt_unit_port_t_*) => + private val remove_port: remove_port_t = + remove_port_t { (_: nxt_unit_t_*, ctx: nxt_unit_ctx_t_*, port: nxt_unit_port_t_*) => { if (port.data != null && !ctx.isNull) { - PortData.fromPort(port).stop() + val portData = PortData.fromPort(port) + portData.stop() } } - } + } private val quit: quit_t = quit_t { (ctx: nxt_unit_ctx_t_*) => - shutdownHandler { () => - nxt_unit_done(ctx) - } + dispatcher.unsafeRunAndForget(shutdownDeferred.complete(IO(nxt_unit_done(ctx)))) } private class PortData private ( @@ -87,45 +103,44 @@ object AsyncServerBuilder { val port: nxt_unit_port_t_* ) { private var stopped: Boolean = false - val process_port_msg: Runnable = new Runnable { - def run(): Unit = { - val rc = nxt_unit_process_port_msg(ctx, port) - - // ideally this shouldn't be needed. - // in theory rc == NXT_UNIT_AGAIN - // would mean that there aren't any messages - // to read. In practice if we stop at rc == NXT_UNIT_AGAIN - // there are some unprocessed messages which effect in - // epollcat (which uses edge-triggering) to hang on close - // since one port to remain open and one callback registered - def continueReading: Boolean = { - val bytesAvailable = stackalloc[Int]() - ioctl(port.in_fd, FIONREAD, bytesAvailable.asInstanceOf[Ptr[Byte]]) - !bytesAvailable > 0 - } - - val continue = rc == NXT_UNIT_OK || continueReading - if (stopped && PortData.isLastFDStopped) { - shutdownHandler { () => - nxt_unit_done(ctx) - } - } else if (continue) { - // The NGINX Unit implementation uses a cancelable timer - // so it can cancel this callback in stop() - // We don't do that here, also because doing that would - // allocate a new Lambda for every process_port_msg - // and it's hard to cancel since it happens immediately - EventPollingExecutorScheduler.execute(process_port_msg) - } - } + // ideally this shouldn't be needed. + // in theory rc == NXT_UNIT_AGAIN + // would mean that there aren't any messages + // to read. In practice if we stop at rc == NXT_UNIT_AGAIN + // there are some unprocessed messages which effect in + // epollcat (which uses edge-triggering) to hang on close + // since one port to remain open and one callback registered + def continueReading: Boolean = { + val bytesAvailable = stackalloc[Int]() + ioctl(port.in_fd, FIONREAD, bytesAvailable.asInstanceOf[Ptr[Byte]]) + !bytesAvailable > 0 } - val stopMonitorCallback: Runnable = EventPollingExecutorScheduler.monitorReads(port.in_fd, process_port_msg) + dispatcher + .unsafeRunAndForget( + poller + .registerFileDescriptor(port.in_fd, monitorReadReady = true, monitorWriteReady = false) + .use(handle => + handle + .pollReadRec[Unit, Unit](()) { _ => + IO { + // process messages until we are blocked + while (nxt_unit_process_port_msg(ctx, port) == NXT_UNIT_OK || continueReading) {} + + if (stopped && PortData.isLastFDStopped) + Right(()) + else + // suspend until more data is available on the socket, then we will be invoked again + Left(()) + } + } + .race(shutdownDeferred.get) + ) + ) def stop(): Unit = { stopped = true - stopMonitorCallback.run() PortData.stopped.put(this, ()) } } diff --git a/snunit-async-epollcat/src/epollcat/unsafe/InternalEventPollingExecutorSchedulerImpl.scala b/snunit-async-epollcat/src/epollcat/unsafe/InternalEventPollingExecutorSchedulerImpl.scala deleted file mode 100644 index e6a8d3d..0000000 --- a/snunit-async-epollcat/src/epollcat/unsafe/InternalEventPollingExecutorSchedulerImpl.scala +++ /dev/null @@ -1,20 +0,0 @@ -package epollcat.unsafe - -import epollcat.unsafe.EpollRuntime - -object InternalEventPollingExecutorSchedulerImpl { - private val scheduler = EpollRuntime.global.compute.asInstanceOf[epollcat.unsafe.EventPollingExecutorScheduler] - - // This initializes the EpollRuntime and schedules it to the - // global Scala Native ExecutionContext - scheduler.execute(() => ()) - - def monitorReads(fd: Int, cb: Runnable): Runnable = { - val newCb = new epollcat.unsafe.EventNotificationCallback { - def notifyEvents(reads: Boolean, writes: Boolean): Unit = { - cb.run() - } - } - scheduler.monitor(fd = fd, reads = true, writes = false)(newCb) - } -} diff --git a/snunit-async-epollcat/src/snunit/EventPollingExecutorScheduler.scala b/snunit-async-epollcat/src/snunit/EventPollingExecutorScheduler.scala deleted file mode 100644 index 70f47dd..0000000 --- a/snunit-async-epollcat/src/snunit/EventPollingExecutorScheduler.scala +++ /dev/null @@ -1,14 +0,0 @@ -package snunit - -private[snunit] object EventPollingExecutorScheduler { - def monitorReads( - fd: Int, - cb: Runnable - ): Runnable = { - epollcat.unsafe.InternalEventPollingExecutorSchedulerImpl.monitorReads(fd, cb) - } - - def execute(runnable: Runnable): Unit = { - epollcat.unsafe.EpollRuntime.global.compute.execute(runnable) - } -} diff --git a/snunit-async-loop/src/snunit/EventPollingExecutorScheduler.scala b/snunit-async-loop/src/snunit/EventPollingExecutorScheduler.scala deleted file mode 100644 index 99a43d7..0000000 --- a/snunit-async-loop/src/snunit/EventPollingExecutorScheduler.scala +++ /dev/null @@ -1,16 +0,0 @@ -package snunit - -import scala.concurrent.duration._ -import scala.scalanative.loop._ - -private[snunit] object EventPollingExecutorScheduler { - def monitorReads(fd: Int, cb: Runnable): Runnable = { - val poll = Poll(fd) - poll.startRead(_ => cb.run()) - () => poll.stop() - } - - def execute(runnable: Runnable): Unit = { - scala.concurrent.ExecutionContext.global.execute(runnable) - } -} diff --git a/snunit-http4s/src/snunit/Http4sApp.scala b/snunit-http4s/src/snunit/Http4sApp.scala index e33d3f8..432814b 100644 --- a/snunit-http4s/src/snunit/Http4sApp.scala +++ b/snunit-http4s/src/snunit/Http4sApp.scala @@ -1,11 +1,12 @@ package snunit import cats.effect.IO +import cats.effect.IOApp import cats.effect.Resource import org.http4s.HttpApp import snunit.http4s.SNUnitServerBuilder -trait Http4sApp extends epollcat.EpollApp.Simple { +trait Http4sApp extends IOApp.Simple { def routes: Resource[IO, HttpApp[IO]] override def run = routes.use { r => diff --git a/snunit-http4s/src/snunit/http4s/Impl.scala b/snunit-http4s/src/snunit/http4s/Impl.scala index 9915eab..9868be8 100644 --- a/snunit-http4s/src/snunit/http4s/Impl.scala +++ b/snunit-http4s/src/snunit/http4s/Impl.scala @@ -2,8 +2,8 @@ package snunit.http4s import cats.effect.* import cats.effect.std.Dispatcher -import cats.effect.syntax.all._ -import cats.syntax.all._ +import cats.effect.syntax.all.* +import cats.syntax.all.* import org.http4s import org.http4s.HttpApp import org.typelevel.ci.CIString @@ -13,20 +13,24 @@ import snunit.* import java.util.concurrent.CancellationException private[http4s] object Impl { - def buildServer[F[_]: Async]( + def buildServer[F[_]: Async: LiftIO]( httpApp: HttpApp[F], errorHandler: Throwable => F[http4s.Response[F]] ): F[Unit] = { for - shutdownDeferred <- Deferred[F, F[Unit]] + shutdownDeferred <- Deferred[IO, IO[Unit]].to[F] + pollers <- IO.pollers.to[F] shutdown <- Dispatcher .parallel[F](await = true) .use { dispatcher => - Async[F].delay( - snunit.AsyncServerBuilder - .setRequestHandler(new snunit.RequestHandler { - def handleRequest(req: snunit.Request): Unit = { - val run = httpApp + snunit.CEAsyncServerBuilder + .setDispatcher(dispatcher) + .setFileDescriptorPoller(pollers.head.asInstanceOf) + .setShutdownDeferred(shutdownDeferred) + .setRequestHandler(new snunit.RequestHandler { + def handleRequest(req: snunit.Request): Unit = { + dispatcher.unsafeRunAndForget( + httpApp .run { val method = req.method match { case snunit.Method.GET => http4s.Method.GET @@ -86,16 +90,13 @@ private[http4s] object Impl { val headers = Headers(response.headers.headers, _.name.toString, _.value) VersionSpecific.writeResponse(req, response, response.status.code, headers) } - dispatcher.unsafeRunAndForget(run) - } - }) - .setShutdownHandler(shutdown => - dispatcher.unsafeRunAndForget(shutdownDeferred.complete(Async[F].delay(shutdown()))) - ) - .build() - ) *> shutdownDeferred.get + ) + } + }) + .build + .to[F] *> shutdownDeferred.get.to[F] } - _ <- shutdown + _ <- shutdown.to[F] yield () } } diff --git a/snunit-http4s/src/snunit/http4s/SNUnitServerBuilder.scala b/snunit-http4s/src/snunit/http4s/SNUnitServerBuilder.scala index fa46b11..8035815 100644 --- a/snunit-http4s/src/snunit/http4s/SNUnitServerBuilder.scala +++ b/snunit-http4s/src/snunit/http4s/SNUnitServerBuilder.scala @@ -1,13 +1,14 @@ package snunit.http4s import cats.effect.Resource +import cats.effect.LiftIO import cats.effect.kernel.Async import cats.effect.std.Dispatcher import org.http4s.HttpApp import org.http4s.Response import org.http4s.Status -class SNUnitServerBuilder[F[_]: Async]( +class SNUnitServerBuilder[F[_]: Async: LiftIO]( private val httpApp: HttpApp[F], private val errorHandler: Throwable => F[Response[F]] ) { @@ -25,7 +26,7 @@ class SNUnitServerBuilder[F[_]: Async]( } object SNUnitServerBuilder { - def default[F[_]: Async]: SNUnitServerBuilder[F] = { + def default[F[_]: Async: LiftIO]: SNUnitServerBuilder[F] = { val serverFailure = Response(Status.InternalServerError).putHeaders(org.http4s.headers.`Content-Length`.zero) def errorHandler: Throwable => F[Response[F]] = { case (_: Throwable) => Async[F].pure(serverFailure.covary[F]) diff --git a/snunit-internal-api/src/EventPollingExecutorScheduler.java b/snunit-internal-api/src/EventPollingExecutorScheduler.java deleted file mode 100644 index 1adc495..0000000 --- a/snunit-internal-api/src/EventPollingExecutorScheduler.java +++ /dev/null @@ -1,14 +0,0 @@ -package snunit; - -final class EventPollingExecutorScheduler { - private EventPollingExecutorScheduler() { - } - - public static Runnable monitorReads(int fd, Runnable cb) { - throw new AssertionError("stub"); - } - - public static void execute(Runnable runnable) { - throw new AssertionError("stub"); - } -} diff --git a/snunit-mill-plugin/src/SNUnit.scala b/snunit-mill-plugin/src/SNUnit.scala index 94b7c0c..99109ec 100644 --- a/snunit-mill-plugin/src/SNUnit.scala +++ b/snunit-mill-plugin/src/SNUnit.scala @@ -1,7 +1,6 @@ package snunit.plugin import mill._ -import mill.define._ import mill.scalanativelib._ trait SNUnit extends ScalaNativeModule { diff --git a/snunit-tapir-cats-effect/src/snunit/TapirApp.scala b/snunit-tapir-cats-effect/src/snunit/TapirApp.scala index 77405d9..fe3e167 100644 --- a/snunit-tapir-cats-effect/src/snunit/TapirApp.scala +++ b/snunit-tapir-cats-effect/src/snunit/TapirApp.scala @@ -1,10 +1,11 @@ package snunit import cats.effect.IO +import cats.effect.IOApp import cats.effect.Resource import sttp.tapir.server.ServerEndpoint -trait TapirApp extends epollcat.EpollApp.Simple { +trait TapirApp extends IOApp.Simple { def serverEndpoints: Resource[IO, List[ServerEndpoint[Any, IO]]] override def run = serverEndpoints.use { se => diff --git a/snunit-tapir-cats-effect/src/snunit/tapir/SNUnitServer.scala b/snunit-tapir-cats-effect/src/snunit/tapir/SNUnitServer.scala deleted file mode 100644 index 4155ee4..0000000 --- a/snunit-tapir-cats-effect/src/snunit/tapir/SNUnitServer.scala +++ /dev/null @@ -1,3 +0,0 @@ -package snunit.tapir - -class SNUnitServer private[tapir] () diff --git a/snunit-tapir-cats-effect/src/snunit/tapir/SNUnitServerBuilder.scala b/snunit-tapir-cats-effect/src/snunit/tapir/SNUnitServerBuilder.scala index cd19788..04154b6 100644 --- a/snunit-tapir-cats-effect/src/snunit/tapir/SNUnitServerBuilder.scala +++ b/snunit-tapir-cats-effect/src/snunit/tapir/SNUnitServerBuilder.scala @@ -7,7 +7,7 @@ import sttp.model._ import sttp.tapir._ import sttp.tapir.server._ -class SNUnitServerBuilder[F[_]: Async] private (serverEndpoints: List[ServerEndpoint[Any, F]]) { +class SNUnitServerBuilder[F[_]: Async: LiftIO] private (serverEndpoints: List[ServerEndpoint[Any, F]]) { private def copy(serverEndpoints: List[ServerEndpoint[Any, F]]) = new SNUnitServerBuilder( serverEndpoints = serverEndpoints @@ -18,27 +18,27 @@ class SNUnitServerBuilder[F[_]: Async] private (serverEndpoints: List[ServerEndp } def run: F[Unit] = for - shutdownDeferred <- Deferred[F, F[Unit]] + shutdownDeferred <- Deferred[IO, IO[Unit]].to[F] + pollers <- IO.pollers.to[F] shutdown <- Dispatcher.parallel[F](await = true).use { dispatcher => for handler <- new SNUnitCatsServerInterpreter[F](dispatcher).toHandler(serverEndpoints) - _ <- Async[F].delay( - snunit.AsyncServerBuilder - .setRequestHandler(handler) - .setShutdownHandler(shutdown => - dispatcher.unsafeRunAndForget(shutdownDeferred.complete(Async[F].delay(shutdown()))) - ) - .build() - ) - shutdown <- shutdownDeferred.get + _ <- snunit.CEAsyncServerBuilder + .setDispatcher(dispatcher) + .setFileDescriptorPoller(pollers.head.asInstanceOf) + .setShutdownDeferred(shutdownDeferred) + .setRequestHandler(handler) + .build + .to[F] + shutdown <- shutdownDeferred.get.to[F] yield shutdown } - _ <- shutdown + _ <- shutdown.to[F] yield () } object SNUnitServerBuilder { - def default[F[_]: Async]: SNUnitServerBuilder[F] = { + def default[F[_]: Async: LiftIO]: SNUnitServerBuilder[F] = { new SNUnitServerBuilder[F]( serverEndpoints = endpoint.out(statusCode(StatusCode.NotFound)).serverLogicSuccess(_ => Async[F].pure(())) :: Nil ) diff --git a/snunit-tapir/src/snunit/tapir/SNUnitFutureServerInterpreter.scala b/snunit-tapir/src/snunit/tapir/SNUnitFutureServerInterpreter.scala deleted file mode 100644 index dd91ea3..0000000 --- a/snunit-tapir/src/snunit/tapir/SNUnitFutureServerInterpreter.scala +++ /dev/null @@ -1,15 +0,0 @@ -package snunit.tapir - -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent._ - -object SNUnitFutureServerInterpreter extends SNUnitGenericServerInterpreter { - private[tapir] type Wrapper[T] = Future[T] - private[tapir] type HandlerWrapper = snunit.RequestHandler - private[tapir] val dispatcher = new WrapperDispatcher { - inline def dispatch(f: => Future[Unit]): Unit = f - } - inline private[tapir] def createHandleWrapper(f: => snunit.RequestHandler): HandlerWrapper = f - inline private[tapir] def wrapSideEffect[T](f: => T): Wrapper[T] = Future.successful(f) - implicit private[tapir] val monadError: sttp.monad.MonadError[Future] = new sttp.monad.FutureMonad -} diff --git a/snunit/src/snunit/AsyncServer.scala b/snunit/src/snunit/AsyncServer.scala deleted file mode 100644 index d1c76cf..0000000 --- a/snunit/src/snunit/AsyncServer.scala +++ /dev/null @@ -1,7 +0,0 @@ -package snunit - -import snunit.unsafe._ - -import scala.scalanative.unsafe._ - -class AsyncServer private[snunit] (private val ctx: nxt_unit_ctx_t_*) extends Server diff --git a/versions.sc b/versions.sc index bee9236..06cef1c 100644 --- a/versions.sc +++ b/versions.sc @@ -2,22 +2,21 @@ // It needs to be a valid Scala file so we can't use top level `val`s object Versions { val scalaNative = "0.4.16" - val upickle = "3.1.0" + val upickle = "3.2.0" val undertow = "2.3.10.Final" - val scala212 = "2.12.18" - val scala213 = "2.13.11" - val scala3 = "3.3.1" - val scalaNativeLoop = "0.2.1" + val scala212 = "2.12.19" + val scala213 = mill.main.BuildInfo.scalaVersion + val scala3 = "3.3.3" val tapir = "1.9.2" val cask = "0.9.1" - val http4s023 = "0.23.24" - val http4s1 = "1.0.0-M40" - val mill010 = "0.10.12" + val catsEffect = "3.6-623178c" + val http4s023 = "0.23.26" + val http4s1 = "1.0.0-M41" + val mill011 = "0.11.7" val utest = "0.8.2" val osLib = "0.9.2" - val sttp = "3.9.1" + val sttp = "3.9.6" val pprint = "0.8.1" val castor = "0.3.0" val sjavatime = "1.1.9" - val epollcat = "0.1.6" }