Skip to content

Instantly share code, notes, and snippets.

@dmlloyd
Last active October 18, 2019 14:19
Show Gist options
  • Save dmlloyd/d271f6a19bd60990a5977fe7b2a43950 to your computer and use it in GitHub Desktop.
Save dmlloyd/d271f6a19bd60990a5977fe7b2a43950 to your computer and use it in GitHub Desktop.
Command mode proposal

Command Mode

We have a number of minor design issues around application startup, dev mode, and the ability to integrate and support command-based applications. After going over the requirements (and implementation ideas) and looking at the current code, I am ready to outline a design proposal to hopefully solve all these issues at once.

Requirements

For a summary of requirements and use cases, please refer to [the requirements document](https://docs.google.com/document/d/1UxXzGmqS2sp4P2-i_u0jSgEF4PtvX7mVa9hAByyn0Mg/edit).

Status quo

Presently when we start an application, we just execute one straight generated block of code which registers Closeable instances as setup progresses. There are multiple redundant code paths as dev mode proceeds slightly differently and supports restart, and has its own shutdown hooks. There is no integration point where an application can exit immediately, or return a status code, or process arguments; there is no integration point where we can process arguments for configuration; there is no place to support restart/reload of any kind.

Proposed new model

I would propose a new model that is built a bit like an interceptor chain. Each step implements the interface ExecutionHandler, which has a single method:

/**
 * An application execution handler.
 */
public interface ExecutionHandler {

    /**
     * Run the application, returning an exit code when complete.  The given {@code context} can be used
     * to pass control to the next execution handler, or the handler may perform a function and then immediately return.
     *
     * @param chain the execution chain (not {@code null})
     * @param context the application execution context (not {@code null})
     * @return the exit code to return
     * @throws Exception on execution failure
     */
    int run(ExecutionChain chain, ExecutionContext context) throws Exception;
}

The class ExecutionChain has a method like this:

    public int proceed(ExecutionContext context) throws Exception;

The class ExecutionContext has methods such as:

    public Object getValue(String key);
    public int getArgumentCount();
    public String getArgument(int index);

Execution handler capabilities

An execution handler may:

  • Peform setup actions

  • Parse command line arguments

  • Use data from a previous stage via getValue

  • Throw an exception

  • Return an exit code immediately

  • Call the next stage via proceed

  • Return the exit code that was returned by the next stage

  • Return an exit code that is different from the one returned by the next stage

  • Perform teardown actions before returning or before propagating an exception

  • Catch and optionally rethrow an exception from the next stage

  • Re-call the next stage via proceed in a loop to facilitate reload, restart, or retry

  • Pass a modified startup context to subsequent steps to add, remove, or mutate arguments and attached values

The final stage would be the normal "server mode" execution: essentially, the thread is blocked in park until application shutdown is triggered either by System.exit or by a clean shutdown or restart request.

Exit codes

The following exit codes should be used:

Code Description

0

Normal exit (no error, clean shutdown)

1

Exit due to uncaught exception

2

Exit due to configuration or usage error

10

(reserved due to its usage with JBoss)

11

Reload the process from the start of the startup chain

12

Partially reload the process as with a dev mode redeploy

Asynchronous exit

To handle the case of an asynchronous System.exit, including when it is caused by a signal, a new exception AsynchronousExitException is introduced which is thrown by proceed. Any returned exit code would be ignored in this case because the JVM would already have decided on the exit code.

Dev mode and restart

For restart/reload/dev mode, special exit codes can be used to indicate to earlier handlers that the application should not exit at this time. For example, WildFly uses an exit code of 10 to indicate that the server should restart.

Restarting handlers could be inserted at various points in the chain in order to perform different degrees of restarting. For example, development mode does not restart thread pools or the web container, but it does reinstall the application due to it being changed and loaded into a new class loader. A full reload might shut down and restart everything.

For situations like development mode, part of the chain might be regenerated during run, so a handler implementation would be provided which allows the subsequent chain to be plugged in.

Ordering

Handlers would be ordered using the existing logic that we use to order the main method steps (see quarkusio/quarkus#1020 for discussion of any problems with this approach).

Extension interface

Extensions need the ability to provide a handler to perform the startup activities that relate to that extension. This would generally replace the current mechanism of recording main method startup items.

Integration with configuration and templates

Startup handlers should be able to inject build and run time configuration objects. It should also be possible to inject templates, but this might not be useful since a startup handler can itself act as a template of sorts.

Relationship to bytecode recording

A compatibility layer will be introduced that takes the current main method bytecode blocks and creates execution handlers for each of them. The execution handler will be based on an abstract class which bridges between the execution handler API and the classic StartupContext-based API. It will automatically call the cleanup actions on exit, and it will always return the exit code of the next stage.

The compatibility layer can stay around permanently or it can be removed if all extensions end up migrating off of the classic approach.

User handlers

It is required that applications should be able to provide their own startup handlers to implement command-only program behavior and also to intercept server startup and shutdown. The integration should allow for handlers to be installed at various stages, including but not limited to:

  • The very beginning (before command arguments are processed)

  • After ArC is initialized

  • After thread pool startup

  • After web container startup

  • After everything (but before the final blocking stage)

Any handler which is installed after ArC is initialized should be able to be treated as a CDI bean for the purposes of injection. However, attempting to inject anything which is initialized by a later stage would be expected to fail.

User handlers within a single stage should be able to be reordered using a numeric priority. The standard @Priority annotation seems well suited to this task.

Run time characteristics

Instead of having one long method which performs all startup activities with one class (or lambda) per cleanup operation, which stores the cleanup data on the heap, the startup process would consist of one class per startup operation which would also include cleanup, storing most of the cleanup data on the execution stack.

Assembly

The startup chain should generally be assembled in a static initialization block and stored in a static final field. One easy way to do this without a lot of code generation is to use Java serialization to deserialize a resource during static init and assign it to its field. Then the main method is simply a one-line call to kick off the chain and exit with the result.

JVM mode

In JVM mode, the amount of method bytecode which needs to be processed under the new approach should be similar to the amount of method bytecode processed by the existing approach. It would be expected that in either situation, as the code is run only once, it would be interpreted in most cases.

Native image

In native image mode, the startup sequence would be compiled. In addition, most or all of the startup steps would be force-inlined, which should result in essentially similar performance and code output compared to today. The hierarchical and segmented structure of the startup sequence should be highly optimizable.

Since the startup chain would be set in a static final field, it would be stored directly into the image’s initial heap, and thus should be immediately available upon boot.

@dmlloyd
Copy link
Author

dmlloyd commented May 17, 2019

Updated to include the idea of compatibility with existing bytecode recorders.

@remkop
Copy link

remkop commented Oct 18, 2019

Note on the exit codes for usage errors vs uncaught exceptions: the picocli library by default (this is configurable) uses 1 for uncaught exceptions and 2 for invalid usage. There is no accepted standard here but this stackoverflow answer convinced me that 2 for invalid usage is a unix convention.

@dmlloyd
Copy link
Author

dmlloyd commented Oct 18, 2019

Good note, thanks. I'll make sure it's updated in the final PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment