Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal: provide an OxApp trait as main entry point for app #152

Closed
lbialy opened this issue Jun 9, 2024 · 12 comments
Closed

Proposal: provide an OxApp trait as main entry point for app #152

lbialy opened this issue Jun 9, 2024 · 12 comments

Comments

@lbialy
Copy link
Contributor

lbialy commented Jun 9, 2024

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-L511C6

I have built a small draft for myself here:

trait OxApp:
  def main(args: Array[String]): Unit =
    supervised:
      val forkedMain = fork(supervised(run(args.toVector)))
      forkedMain.joinEither() match
        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]

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 is Either[Throwable, Unit])

It could, for example, open an either block for the user in run so that ox.either.ok() combinator can be used in the main method. It would, however, make the run signature a bit more cumbersome due to (using Label[Either[Throwable, Unit]], Ox).

@lbialy
Copy link
Contributor Author

lbialy commented Jun 9, 2024

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

@adamw
Copy link
Member

adamw commented Jun 10, 2024

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?

@lbialy
Copy link
Contributor Author

lbialy commented Jun 10, 2024 via email

@adamw
Copy link
Member

adamw commented Jun 10, 2024

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?

@lbialy
Copy link
Contributor Author

lbialy commented Jun 10, 2024 via email

@adamw
Copy link
Member

adamw commented Jun 10, 2024

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.

@lbialy
Copy link
Contributor Author

lbialy commented Jun 10, 2024 via email

@adamw
Copy link
Member

adamw commented Jun 11, 2024

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?

@lbialy
Copy link
Contributor Author

lbialy commented Jun 11, 2024

yeah, this would be very nice!

@lbialy
Copy link
Contributor Author

lbialy commented Jun 12, 2024

Would you like a PR for that or are you already working on it?

@adamw
Copy link
Member

adamw commented Jun 12, 2024

@lbialy a PR would be great, I'm back to "normal" work only next week

@lbialy
Copy link
Contributor Author

lbialy commented Jun 20, 2024

implemented in #157

@adamw adamw closed this as completed Jul 12, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants