Open lbialy opened 3 weeks ago
Tbh handling signals with interruption seems kinda easy:
//> using scala 3.3.3
//> using dep com.softwaremill.ox::core:0.2.1
import ox.*
import java.lang.Runtime
trait OxApp:
def main(args: Array[String]): Unit =
val rt = Runtime.getRuntime()
unsupervised:
val cancellableMainFork = forkCancellable(supervised(run(args.toVector)))
rt.addShutdownHook:
new Thread(() => {
println()
println("shutting down")
cancellableMainFork.cancel()
println("interrupted the main fork")
})
cancellableMainFork.joinEither() match
case Left(iex: InterruptedException) => System.exit(0)
case Left(err) => throw err
case Right(userEither) =>
userEither match
case Left(err) => throw err
case Right(()) => System.exit(0)
def run(args: Vector[String])(using Ox): Either[Throwable, Unit] = run
def run(using Ox): Either[Throwable, Unit]
object Main extends OxApp:
def run(using Ox): Either[Throwable, Unit] = either:
try
Thread.sleep(20000)
println("what's up?")
catch
case iex: InterruptedException =>
println("main got interrupted")
throw iex
Ah, this would solve #131 - great idea! Let's do it :) I'm not convinced about the def run(using Ox): Either[Throwable, Unit]
(specifically, returning an Either
- I'd rather say any logical - type-safe errors - should be handled in the app). So maybe we can go with simply the shutdown hook + OxApp
+ docs for now?
I was personally interested in finding a set of idioms that allow me to program without lying in signatures (so without throwing exceptions other than flow control). If you make it return Unit or ExitCode user using either block in main function will be forced to patmat+throw. Maybe multiple variants should be available? OxEitherApp? Maybe different methods available for override (this is a bit shitty though because there's no way to force user to override just one)?
On Mon 10. Jun 2024 at 10:07, Adam Warski @.***> wrote:
Ah, this would solve #131 https://github.com/softwaremill/ox/issues/131
- great idea! Let's do it :) I'm not convinced about the def run(using Ox): Either[Throwable, Unit] (specifically, returning an Either - I'd rather say any logical - type-safe errors - should be handled in the app). So maybe we can go with simply the shutdown hook + OxApp + docs for now?
— Reply to this email directly, view it on GitHub https://github.com/softwaremill/ox/issues/152#issuecomment-2157619533, or unsubscribe https://github.com/notifications/unsubscribe-auth/ACBVNUQNMSMY23CLEC7P5GLZGVNFBAVCNFSM6AAAAABJAZBGKOVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDCNJXGYYTSNJTGM . You are receiving this because you authored the thread.Message ID: @.***>
I see, though I don't think we'll be able to escape from exceptions. Any I/O method can throw exceptions, do you have an idea how to handle this?
(I'm all for not lying in signatures, though :) And I like ZIO's concept of untyped defects, which in our case are represented as exceptions)
But even if we don't lie in signatures, if the main returns an either, this means that some errors are unhandled by the app? So at the top-level, if any error is unhandled - it's a bug - hence should be thrown as an exception, no?
Hmm, it's the same with IOApp, right? They expect your final IO to be successful and contain an ExitCode (meaning you translated any logical expected errors to a meaningful exit code no and handled informing the user) but if it's a failed IO they just recover to exit code 1 and print stack trace, I think. Ok, so maybe mirror CE:
trait OxApp: def run(args: Vector[String]): ExitCode
object OxApp: trait Simple extends OxApp: def run(args: Vector[String]): Unit
and then we joinEither anyway, handle any exception thrown on main fiber and map it to exit code as recovery? For now there's no need to worry about cross-platform code at least.
I think I'd still prefer my entry point to allow me to use ox.either combinators but if these traits can be extended, it's not a big deal.
On Mon 10. Jun 2024 at 10:23, Adam Warski @.***> wrote:
I see, though I don't think we'll be able to escape from exceptions. Any I/O method can throw exceptions, do you have an idea how to handle this?
(I'm all for not lying in signatures, though :) And I like ZIO's concept of untyped defects, which in our case are represented as exceptions)
But even if we don't lie in signatures, if the main returns an either, this means that some errors are unhandled by the app? So at the top-level, if any error is unhandled - it's a bug - hence should be thrown as an exception, no?
— Reply to this email directly, view it on GitHub https://github.com/softwaremill/ox/issues/152#issuecomment-2157682952, or unsubscribe https://github.com/notifications/unsubscribe-auth/ACBVNUTZOPUW27AFDHHS7RDZGVO6ZAVCNFSM6AAAAABJAZBGKOVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDCNJXGY4DEOJVGI . You are receiving this because you authored the thread.Message ID: @.***>
Or going the ZIOApp
route you could return an Either[Any, Any]
and printout any error that occurs to stdout - not necessarily exceptions only.
So this would bring the variants to 3 ("simple", "regular" and "either"). I'd prefer to have a single variant, but maybe that's essential, not accidental complexity.
Either[Any, Any] sounds like very bad. trait OxEitherApp[E, A]: def handleError(e: E): ExitCode def handleResult(a: A): ExitCode def run(args: Vector[String])(using Label[Either[E, A]], Ox): Either[E, A]
is an option but it's quite complex. But it's would probably be best for my needs tbh.
On Mon 10. Jun 2024 at 10:39, Adam Warski @.***> wrote:
Or going the ZIOApp route you could return an Either[Any, Any] and printout any error that occurs to stdout - not necessarily exceptions only.
So this would bring the variants to 3 ("simple", "regular" and "either"). I'd prefer to have a single variant, but maybe that's essential, not accidental complexity.
— Reply to this email directly, view it on GitHub https://github.com/softwaremill/ox/issues/152#issuecomment-2157719031, or unsubscribe https://github.com/notifications/unsubscribe-auth/ACBVNUVGZSYGMTSDE2RSWDLZGVQ25AVCNFSM6AAAAABJAZBGKOVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDCNJXG4YTSMBTGE . You are receiving this because you authored the thread.Message ID: @.***>
So we could have:
trait OxApp:
def run(using Ox): Unit
trait WithExitCode:
def run(using Ox): ExitCode
trait WithErrors[E]:
def run(using Ox, Label[Either[E, ExitCode]]): Either[E, ExitCode]
then?
yeah, this would be very nice!
Would you like a PR for that or are you already working on it?
@lbialy a PR would be great, I'm back to "normal" work only next week
implemented in https://github.com/softwaremill/ox/pull/157
One things that would improve usability of Ox would be a way to define an entry point to the application which would start user's program on a separate virtual thread, block the main thread until user's program finish and translate any errors into app exit with an error code. Moreover, it could handle signals too so that it would be possible to interrupt user's code on SIGTERM as #131 proposes. I guess some inspiration for signal handling could be borrowed from CE: https://github.com/typelevel/cats-effect/blob/1e445fb1750fdf3878f85f9a1bccab7720ce9817/core/jvm/src/main/java/cats/effect/Signal.java
But on JVM CE seems to just use
addShutdownHook
: https://github.com/typelevel/cats-effect/blob/1e445fb1750fdf3878f85f9a1bccab7720ce9817/core/jvm/src/main/scala/cats/effect/IOApp.scala#L502C1-L511C6I have built a small draft for myself here:
It's extremely limited, only thing it does is: a) run user's
run
method on a forked virtual thread with a supervised scope so that user can fork off the main "fiber" b) handle both thrown and returned errors (expected return type from user isEither[Throwable, Unit]
)It could, for example, open an
either
block for the user in run so thatox.either.ok()
combinator can be used in the main method. It would, however, make therun
signature a bit more cumbersome due to(using Label[Either[Throwable, Unit]], Ox)
.