jdaugherty opened a new pull request, #16536:
URL: https://github.com/apache/grails-core/pull/16536
## The problem
A Grails application can take a long time to start: plugins contribute their
bean definitions, GORM builds its session factory, GSPs compile, `BootStrap`
seeds data, and database migrations run. For all of that time `./gradlew
bootRun` leaves port 8080 closed. A developer who opens the application gets a
refused connection and keeps refreshing, with no idea how far the start has
got, which part is slow, or, when it fails, why. The failure only shows up in
the console, and a DevTools restart leaves the same gap.
## What this adds
With the new `grails-startup-progress` module, the application's port
answers from the moment the application context is prepared:
- **A progress page.** A browser opening the application sees the stage the
start has reached (preparing the context, loading plugins and bean definitions,
creating beans, starting the web server, running plugin startup and
`BootStrap`), how many beans have been created, the bean being created now, and
the slowest beans so far, timed without their dependencies so the actual
culprit stands out. The page reloads into the address it was opened at once the
application is ready, which is **after `BootStrap` has run**, not merely once
Tomcat is listening.
- **Failures on the page.** If the start fails, the page shows the exception
and stack trace, including failures in `BootStrap` and while the web server
starts, and stays open to follow the next start. The process still exits as it
always has.
- **Progress from your own code.** `grails.boot.StartupTask` lets
applications and plugins report long startup work, such as loading reference
data, as "n of m, now on X". It records through Spring's own
`ApplicationStartup`, so it costs nothing when nothing records the start, and
Spring Boot Actuator's `startup` endpoint sees it too. The database migration
plugins (Hibernate 5 and 7) use it to show how many change sets are left while
`updateOnStart` runs.
- **A startup report.** Once running, the application serves a report of how
long each stage took, matching Spring Boot's own `Started ... in` time, at
`/__grails/startup`, as a page or, to `Accept: application/json`, as data for
keeping track of startup time in CI.
- **Opening a browser.** `grails.startup.progress.openBrowser` opens a
browser on the page as the start begins. `bootRun` passes any
`grails.startup.progress.*` Gradle property to the application, so a developer
can turn it on for every project once in `~/.gradle/gradle.properties`, or for
one run with `./gradlew bootRun -Pgrails.startup.progress.openBrowser`.
### With the configuration cache
This pairs with the configuration cache work (#16528). Once `bootRun` can
reuse its configuration, the time from `./gradlew bootRun` to the application
JVM running drops to little more than JVM startup. Together, `bootRun`
effectively puts a server on the port almost immediately, with the start's
progress shown in the browser rather than the port staying closed until the
application is fully up.
## How it works
- A run listener binds the port with the JDK's built-in HTTP server when the
application context is prepared. There are no new third-party dependencies.
- A `SmartLifecycle` one phase before Spring Boot's web server start/stop
lifecycle stops that server, so Tomcat binds the port as it always has. The
handoff showed no refused connections at 300 ms polling.
- A filter inside the application then carries the progress on through
plugin startup and `BootStrap`. It takes only the progress paths and real
browser page loads (`Sec-Fetch-Mode: navigate`), so API calls, and a
`BootStrap` calling its own application, reach the application exactly as
before.
- Both servers answer through one shared responder. Once the application is
ready, the page's last poll is answered as ready rather than reaching the
application, so nothing new is logged.
- The page is not served for a WAR deployment, a random port, SSL, or a port
already in use; in that last case the web server reports the port in use
exactly as it did.
## Security
The details (stages, bean names, failures, Grails version) are shown only to
a browser signed in with the address the application logs as it starts,
`?grailsStartupToken=…`. The token is 256 random bits, made once per JVM, and
only ever written to the log, so seeing the details takes the same access as
reading the log. Opening the address sets an `HttpOnly`, `SameSite=Strict`
cookie and redirects to the address without the token. A browser opened with
`openBrowser` is signed in already. Anyone else sees only the progress bar: the
details are left out of the HTML and the JSON on the server rather than hidden
in the page. The page sends a restrictive Content-Security-Policy, and
everything dynamic is written as text, never as markup.
## Adopting it
The module is opt-in. It is not part of `grails-dependencies-starter-web`.
Add the dependency, or select the new `grails-startup-progress` Forge feature
(Development Tools; web and REST API applications only, since a plugin's
dependency would reach every application using it):
```groovy
implementation 'org.apache.grails:grails-startup-progress'
```
| Setting | Default | |
|---|---|---|
| `grails.startup.progress.enabled` | development mode | serve the progress
page |
| `grails.startup.progress.showDetails` | development mode | show the
details to a signed-in browser |
| `grails.startup.progress.openBrowser` | `false` | open a browser on the
page as the start begins |
| `grails.startup.progress.browserCommand` | the OS's own | the command that
opens the browser |
| `grails.startup.progress.statusPath` | `/__grails/startup-progress` |
where the page polls |
| `grails.startup.progress.endpoint.enabled` | development mode | serve the
startup report |
| `grails.startup.progress.endpoint.path` | `/__grails/startup` | where the
report is served |
All are in the module's `spring-configuration-metadata.json` and the
Application Properties reference. The guide covers the feature under *Running
and Debugging an Application → Watching the Application Start*, with a section
on reporting progress from your own code.
`grails run-app` used to treat "the port accepts a connection" as "the
application is running", which the progress page would have satisfied at once.
It now waits for an answer without the startup phase header, falling back to
the connection check for ports that do not speak plain HTTP, such as SSL.
## Trying it
`grails-test-examples/startup-progress` has three deliberately slow beans
and a slow, task-reporting `BootStrap`:
```
./gradlew :grails-test-examples-startup-progress:bootRun
-Pgrails.startup.progress.openBrowser
./gradlew :grails-test-examples-startup-progress:bootRun
--args='--startup.demo.fail=bootstrap' # or =bean
```
The application logs the signed-in address for the progress page as it
starts, and for the startup report once it is running.
## Testing
Run locally:
- `grails-startup-progress`: `StartupProgressSpec`, 25 features against a
real embedded Tomcat. They cover:
- the handoff and every phase, the sign-in and what anonymous clients
receive
- each failure path, the report as a page and as JSON, and the
configurable paths
- `openBrowser` and the token on a random port
- that Actuator's `BufferingApplicationStartup` is preserved
- that the total time matches Spring Boot's logged `Started ... in` time
with a slow runner
- `grails-core`: `StartupTaskSpec`.
- `grails-data-hibernate5-dbmigration` and
`grails-data-hibernate7-dbmigration`: full unit suites, including a new spec
that runs `GrailsLiquibase` against H2 and checks the reported change sets, the
context-filtered total, and that a migration callback's own change listener
still works.
- `grails-gradle-plugins`: `GrailsGradlePluginToolchainSpec`, including
property forwarding to `bootRun`.
- `grails-shell-cli`: a new `ServerInteractionSpec`.
- `grails-forge-core`: a new `GrailsStartupProgressSpec`, plus
`GrailsBaseSpec`, `BaseAvailableFeaturesSpec`, `FeatureOperationsSpec` and
`CreateAppCommandSpec`.
- `grails-test-examples-startup-progress`: an integration test that starts
the real Grails application and checks the page reports bean creation and
`BootStrap`, and hands over only once `BootStrap` has seeded its data.
- `codeStyle` for every touched module, plus `rat`,
`validateRepositoryConventions` and `:grails-doc:publishGuide`.
- Checked by hand in Chrome against the example application: both themes,
anonymous and signed-in views, a `BootStrap` failure, the running task, and the
report.
Not run locally: the full test suite and the full `aggregateViolations`
gate, which CI runs.
One flaky test to watch: `does not answer on the port while starting when
the page is not configured` failed once in about 16 full runs and did not recur
in 14 reruns. It had passed, unchanged, through every earlier run.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]