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]

Reply via email to