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

first draft of OxApp with extension companion traits #157

Merged
merged 8 commits into from
Jul 9, 2024

Conversation

lbialy
Copy link
Contributor

@lbialy lbialy commented Jun 20, 2024

No description provided.

@lbialy
Copy link
Contributor Author

lbialy commented Jun 20, 2024

@adamw CI crash is unrelated to this code 😮

@adamw
Copy link
Member

adamw commented Jun 21, 2024

@lbialy yes, already fixed the test: 7a4a73b


def run(args: Vector[String])(using Ox): ExitCode = Success

Main1.main(Array.empty)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could also assert that the result is 0 here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but main is Unit, we check ec to be equal 0 below


private[ox] def printStackTrace(t: Throwable): Unit = t.printStackTrace()

private[OxApp] final def handleRun(args: Vector[String])(using Ox): ExitCode =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is Ox needed here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah we need it for run, ok

case Left(fatalErr) => throw fatalErr
case Right(exitCode) => exit(exitCode.code)

private[ox] def exit(code: Int): Unit = System.exit(code)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe add a comment that these are private[ox] vs private because of testing

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure


private[ox] def mountShutdownHook(thread: Thread): Unit =
try Runtime.getRuntime.addShutdownHook(thread)
catch case _: IllegalStateException => ()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't we re-throw the exception if mounding the hook fails?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, it's thrown only if we're already in shut down of the vm so there's nothing more to do

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for reference: CE IOApp only shuts down it's thread pools when it gets ISE here

mountShutdownHook(interruptThread)

cancellableMainFork.joinEither() match
case Left(iex: InterruptedException) => exit(0)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this success, if the app was interrupted?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

highly debatable, some sources treat interruption as failure and return 1 or 130 (128+SIGINT(2)), some return 0 if application was able to interrupt itself and shut down cleanly (as there were no issues during shutdown), it's up to us to decide. JVM itself returns 130 on unhandled sigint.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, so here we would treat this as "handled sigint"?

but that's great info, can you put the above in a comment in the code? for future readers :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, I'll even make this configurable


def run(using Ox): Unit

trait WithErrors[E] extends OxApp:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we use an arbitrary error mode WithErrorMode, and specialise to eithers with WithEither?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure

Copy link
Contributor Author

@lbialy lbialy Jun 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is something like:

  trait WithErrorMode[E, F[_]](em: ErrorMode[E, F]) extends OxApp:
    override def run(args: Vector[String])(using ox: Ox): ExitCode =
      val result = run(args.toList)
      if em.isError(result) then ExitCode.Failure(1)
      else ExitCode.Success

    def run(args: List[String])(using Ox): F[ExitCode]

  abstract class WithEitherErrors[E] extends WithErrorMode(EitherMode[E]()):

    type EitherScope[Err] = Label[Either[Err, ExitCode]]

    override final def run(args: List[String])(using ox: Ox): Either[E, ExitCode] =
      either[E, ExitCode](label ?=> run(args.toVector)(using ox, label))

    def run(args: Vector[String])(using Ox, EitherScope[E]): ExitCode

what you had in mind?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mind the change of Vector to List in args because we have conflicting signatures so there's that, we could do a disambiguation by always-provided implicit but that's even more... arcane

also without handleErrors there's no translation of error into exit code

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it could be also:

  trait WithErrorMode[E, F[_]](em: ErrorMode[E, F]) extends OxApp:
    override def run(args: Vector[String])(using ox: Ox): ExitCode =
      val result = run(args.toList)
      if em.isError(result) then
        try handleError(em.getError(result))
        catch
          case NonFatal(e) =>
            e.printStackTrace()
            ExitCode.Failure(1)
      else ExitCode.Success

    def handleError(e: E): ExitCode

    def run(args: List[String])(using Ox): F[ExitCode]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and F[ExitCode] doesn't make much sense in case where we have Label[Either[E, Nothing]] in scope as it means that errors are to be delivered with .ok() / break() so unwrapped ExitCode as retval

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hm the Vector/List is also hacky. Maybe we simply need a different name. We've got fork & forkError for the fork-with-error-mode variant, maybe here we could keep a similar convention? (run & runError). Or come up with better names for both :)

another nitpick: I'd avoid using "scope" in names, as we already have concurrency scopes etc. Maybe rename EitherScope -> EitherError?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also, does : F[ErrorCode] and ErrorCode have the same erasure? Isn't the first erased to Object, making the signatures different?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[error] -- [E120] Naming Error: /Users/lbialy/Projects/foss/ox/core/src/main/scala/ox/OxApp.scala:97:8
[error] 97 |    def run(args: Vector[String])(using Ox): F[ExitCode]
[error]    |        ^
[error]    |Double definition:
[error]    |override def run(args: Vector[String])(using ox: ox.Ox): ox.ExitCode in trait WithErrorMode at line 90 and
[error]    |def run(args: Vector[String])(using x$2: ox.Ox): F[ox.ExitCode] in trait WithErrorMode at line 97
[error]    |have matching parameter types.

Unfortunately this is illegal.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah right, signature is only name+parameters, not the return type

@adamw
Copy link
Member

adamw commented Jun 21, 2024

Looks great! :) Some docs would be good :)

case Left(fatalErr) => throw fatalErr
case Right(exitCode) => exit(exitCode.code)

private[ox] def exit(code: Int): Unit = System.exit(code)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't it make sense for this function to accept ExitCode, not Int?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, it does

case Failure(exitCode: Int = 1) extends ExitCode(exitCode)

trait OxApp:
def main(args: Array[String]): Unit =

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

final?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

true

@adamw
Copy link
Member

adamw commented Jul 2, 2024

Looks good! I think we're just missing the documentation.

Only thing I think I would change, is renaming the run method instead of using the Vector/List signatures trick

@lbialy
Copy link
Contributor Author

lbialy commented Jul 2, 2024

The signature that requires List[String] is only present in WithErrorMode trait and is expected to be used in the child trait so yeah, can be renamed to runWithErrors.

@adamw
Copy link
Member

adamw commented Jul 2, 2024

@lbialy yeah this should work fine, also will be consistent with supervisedError and forkError that we already have

@lbialy
Copy link
Contributor Author

lbialy commented Jul 9, 2024

@adamw this is now ready :)

/** This value is returned to the operating system as the exit code when the app receives SIGINT and shuts itself down gracefully.
* Default value is `ExitCode.Success` (0). JVM itself returns code `130` when it receives `SIGINT`.
*/
gracefulShutdownExitCode: ExitCode = ExitCode.Success

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I generally prefer to avoid default parameters for case classes - they are hard to change later. But doesn't seem likely to change.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, since we have the default instance let's move this there

@adamw adamw merged commit f3332bd into softwaremill:master Jul 9, 2024
5 checks passed
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

Successfully merging this pull request may close these issues.

None yet

3 participants