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.
For a summary of requirements and use cases, please refer to [the requirements document](https://docs.google.com/document/d/1UxXzGmqS2sp4P2-i_u0jSgEF4PtvX7mVa9hAByyn0Mg/edit).
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.
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);
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.
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 |
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.
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.
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).
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.
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.
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.
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.
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.
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.
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.
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.
Updated to include the idea of compatibility with existing bytecode recorders.