jdaugherty commented on code in PR #16554:
URL: https://github.com/apache/grails-core/pull/16554#discussion_r4218340269
##########
grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/liquibase/GrailsLiquibase.groovy:
##########
@@ -82,25 +87,66 @@ class GrailsLiquibase extends SpringLiquibase {
@Override
protected void performUpdate(Liquibase liquibase) throws
LiquibaseException {
- if (!applicationContext.containsBean('migrationCallbacks')) {
- super.performUpdate(liquibase)
- return
- }
+ // begun before the migration callbacks run; a change listener a
callback sets of its own is added to the one
+ // that reports each change set, so the callback works as it did
before the update was reported
+ StartupTask task = startTask(liquibase)
Review Comment:
Confirmed and fixed in 2095f7c94b. Liquibase 4.27 caches the parsed
changelog in `getDatabaseChangeLog()`, and `update()` passes that cached
changelog to the update command. Counting first therefore froze the parameters
before `onStartMigration` ran. With recording on, the new test created a table
literally named `${SHELFTABLE}`.
`performUpdate` now runs `beforeStartMigration` and `onStartMigration`
first. Only then does it start the task (count + listener), and it ends the
task before `afterMigrations`. Recording on or off, the update now sees the
same parameters. The task's change listener is now added after any listener a
callback sets. `MultiListenerLiquibase` still keeps every listener, so all of
them hear each change set; the specs' listener-count expectations were updated
to match.
Added `a change log parameter a migration callback sets is used by the
update, whether its start is recorded or not` (recorded: true/false). It uses
`startup-task-parameter-changelog.xml`, whose table name is `${shelfTable}`,
set by the callback. The recorded case failed before the fix. The migration
docs now say the count happens after the callback runs.
##########
grails-data-hibernate5/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/liquibase/GrailsLiquibase.groovy:
##########
@@ -82,25 +87,66 @@ class GrailsLiquibase extends SpringLiquibase {
@Override
protected void performUpdate(Liquibase liquibase) throws
LiquibaseException {
- if (!applicationContext.containsBean('migrationCallbacks')) {
- super.performUpdate(liquibase)
- return
- }
+ // begun before the migration callbacks run; a change listener a
callback sets of its own is added to the one
+ // that reports each change set, so the callback works as it did
before the update was reported
+ StartupTask task = startTask(liquibase)
Review Comment:
Same fix in 2095f7c94b for the Hibernate 5 copy: the callbacks run before
the task is started, and the same recorded/unrecorded parameter spec is added
there.
##########
grails-doc/src/en/guide/gettingStarted/runningAndDebuggingAnApplication.adoc:
##########
@@ -74,3 +74,190 @@ For debugging a Grails app, you have two options. You can
either right-click on
$ ./gradlew bootRun --debug-jvm
For more information on the `bootRun` command, please refer to the
link:{commandLineRef}bootRun.html[bootRun section of the Grails reference
guide].
+
+[[startupProgress]]
+=== Watching the Application Start
+
+The `grails-startup-progress` module shows how far an application has got as
it starts. Add it to the application's `build.gradle`, or select the
`grails-startup-progress` feature when generating the application:
+
+[source,groovy]
+----
+dependencies {
+ implementation 'org.apache.grails:grails-startup-progress'
+}
+----
+
+With it, in development mode, which is the `development` environment run from
the project directory as `./gradlew bootRun` does, Grails answers on the
application's port as soon as the application begins to start, rather than
leaving the port closed until the embedded server is ready. Opening the
application in a browser while it starts shows a progress page with:
+
+* the stage the start has reached: preparing the application context, loading
plugins and bean definitions, creating beans, starting the web server, and
running plugin startup hooks and `BootStrap` classes
+* how many of the application's beans have been created, and which bean is
being created now
+* the beans that have taken longest to create so far, not counting the beans
they depend on, which is usually the quickest way to find out why a start is
slow
+
+The page reloads the address it was opened at once the application is ready,
which is after `BootStrap` has finished, not merely once the web server is
listening. A browser that opens the application while `BootStrap` runs is shown
the progress page too, rather than pages from an application that has not
finished starting.
+
+Until the web server is listening, any request that is not for a web page,
such as a call to a REST endpoint, gets a `503 Service Unavailable` response
with a `Retry-After` header. Once it is listening, such requests reach the
application as they always have, including requests the application makes to
itself from `BootStrap`.
+
+If the start fails, the page shows the exception and its stack trace and stays
open. Start the application again and the page follows the new start and
reloads when it is ready.
+
+The stages, the beans, the exception and the Grails version are details of the
application's internals, so the page shows them only to a browser signed in
with the address the application logs as it starts:
+
+[source,console]
+----
+Startup progress is shown at http://localhost:8080/?grailsStartupToken=… until
the application is ready
+----
+
+Opening that address once signs the browser in for as long as the application
runs, through restarts by Spring Boot DevTools, and takes the token back out of
the address bar. A browser opened by the application, as described below, is
signed in already. Anyone else reaching the port sees only the progress bar:
the details are left out of the page and out of its data, rather than hidden in
them. The sign-in is a cookie, so it covers every tab of the browser that
signed in. The token is made afresh each time the JVM starts and is only ever
written to the log, so seeing the details takes the same access as reading the
log.
+
+Until the embedded server takes the port over, the page is served by the HTTP
server built into the JDK, so it changes nothing about how the application
itself serves requests. In the interactive shell, `grails run-app` keeps
waiting while the page holds the port, and stops waiting once the embedded
server has taken the port over, as it does for an application without the
module, so it does not wait for `BootStrap`. The page is not served when the
application:
+
+* is deployed as a WAR to a servlet container
+* uses a random port (`server.port: 0`) or SSL
+* finds its port already in use, in which case the application fails to start
exactly as it would without the page
+* is to take a CRaC checkpoint on refresh
(`-Dspring.context.checkpoint=onRefresh`), which an open port would fail
+
+These settings control the page. The three left commented out have no fixed
default: unset, each is on in development mode and off elsewhere, so set one
only to decide it yourself:
+
+[source,yaml]
+----
+grails:
+ startup:
+ progress:
+ # enabled: true # whether to serve the page; unset, only in
development mode
+ # showDetails: true # whether a signed-in browser sees bean names
and failures; unset, only in development mode
+ openBrowser: false # whether to open a browser on the page as
the application starts
+ browserCommand: [] # the command that opens the browser; by
default, the operating system's own
+ statusPath: /__grails/startup-progress # where the page polls
for progress, below the context path
+ endpoint:
+ # enabled: true # whether to serve the startup report; unset,
only in development mode
+ path: /__grails/startup # where to serve it, below the
context path
+----
+
+The page follows the start by polling `statusPath`. Once the application is
ready, a poll there is told so rather than reaching the application, which
would log every request it has no mapping for, so the path is taken from the
application for as long as it runs. It cannot be the report's path.
+
+With `openBrowser` set, a browser opens on the progress page as soon as it is
served, so the start can be watched from the beginning. When the page is not
served, for example because the application uses SSL, the browser opens on the
application once it is ready instead. A browser is opened once per address for
the life of the JVM, so the page already open follows a restart by Spring Boot
DevTools rather than another window opening. The browser is opened with `open`
on macOS, `xdg-open` elsewhere on Unix and `rundll32
url.dll,FileProtocolHandler` on Windows; `browserCommand` names another
command, which is given the address as its last argument:
+
+[source,yaml]
+----
+grails:
+ startup:
+ progress:
+ browserCommand: ['/usr/bin/firefox', '--new-window']
+----
+
+Whether a browser opens is usually one developer's preference rather than the
application's. Under `bootRun`, any Gradle property whose name starts with
`grails.startup.progress.` is passed to the application under the same name, so
it can be set for every application on one machine in
`~/.gradle/gradle.properties`:
+
+[source,properties]
+----
+grails.startup.progress.openBrowser=true
+----
+
+or for a single run, where a property given without a value is `true`:
+
+[source,console]
+----
+$ ./gradlew bootRun -Pgrails.startup.progress.openBrowser
+----
+
+Setting `grails.startup.progress.enabled` to `true` outside development mode,
for example in production, serves only the progress bar, unless
`grails.startup.progress.showDetails` is set as well, and then the details go
only to a browser signed in with the address in the log.
+
+==== The Startup Report
+
+Once the application is running, the same view is available as a report of how
it started, at `/__grails/startup` below the context path, in development mode
by default. It shows how long each stage took, how many beans were created, and
the slowest beans, with the total time measured as Spring Boot measures it in
its `Started ... in` log message. The application logs where the report is,
with the token that signs a browser in to see how the start went, as it
finishes starting. Anyone else is told only how long the start took:
+
+[source,console]
+----
+Startup report is at
http://localhost:8080/__grails/startup?grailsStartupToken=…
+----
+
+A client that asks for JSON, with an `Accept: application/json` header or a
`format=json` parameter, gets the report as data, which suits keeping track of
how long an application takes to start:
+
+[source,console]
+----
+$ curl -H 'Accept: application/json' http://localhost:8080/__grails/startup
+----
+
+[source,json]
+----
+{
+ "application": "bookstore",
+ "phase": "READY",
+ "elapsedMillis": 9421,
+ "phases": [
+ {"name": "PREPARING", "millis": 1288},
+ {"name": "LOADING_DEFINITIONS", "millis": 1404},
+ {"name": "CREATING_BEANS", "millis": 6305},
+ {"name": "STARTING_WEB_SERVER", "millis": 31},
+ {"name": "INITIALIZING", "millis": 393}
+ ],
+ "beansCreated": 512,
+ "beansExpected": 512,
+ ...
+}
+----
+
+[cols="1,3"]
+|===
+|Field |Meaning
+
+|`application`
+|The application's name, from `spring.application.name` or `info.app.name`.
+
+|`phase`
+|How far the start has got: `PREPARING`, `LOADING_DEFINITIONS`,
`CREATING_BEANS`, `STARTING_WEB_SERVER`, `INITIALIZING`, then `READY`, or
`FAILED`.
+
+|`elapsedMillis`
+|The time the application took to start, measured as Spring Boot measures the
time in its `Started ... in` log message, so not counting application and
command line runners, or the time so far while it is starting.
+
+|`phases`
+|Each stage begun, in order, with how long it took, or has taken so far. Only
for a signed-in client.
+
+|`beansCreated`, `beansExpected`
+|How many of the singletons the application creates as it starts have been
created, and how many there are. Only for a signed-in client.
+
+|`slowestBeans`
+|The beans that took longest to create, not counting the beans they depend on,
each with its `name` and `millis`. Only for a signed-in client.
+
+|`tasks`
+|The tasks the application and its plugins reported, in the order they began,
each with its `description`, how many items it has `completed` of its `total`
(`-1` when not known), whether it is still `running`, the `item` it is on, and
how long it took in `millis`. Only for a signed-in client.
+
+|`grailsVersion`
+|The version of Grails. Only for a signed-in client.
+
+|`signInForDetails`
+|Whether the client would see the fields only a signed-in client gets by
signing in with the address in the log.
+|===
+
+The report does not need the progress page, so it is served wherever the
application starts its own embedded web server, including over SSL or on a
random port. A different path is set with
`grails.startup.progress.endpoint.path`, and the report is turned on outside
development mode with `grails.startup.progress.endpoint.enabled`.
+
+==== Reporting Progress from Your Own Code
+
+Work an application or a plugin does while the application starts, such as
loading reference data in `BootStrap`, can report how far it has got with
`grails.boot.StartupTask`. A signed-in progress page shows the task while it
runs, with how many items it has done and the one it is on, and the startup
report lists it with how long it took. The database migration plugin reports
the change sets it runs on start this way.
+
+[source,groovy]
+----
+import grails.boot.StartupTask
+import grails.core.GrailsApplication
+
+class BootStrap {
+
+ GrailsApplication grailsApplication
+ ReferenceDataService referenceDataService
+
+ def init = {
+ List<File> files = referenceDataService.files()
+ try (StartupTask task =
StartupTask.start(grailsApplication.mainContext, 'Loading reference data',
files.size())) {
+ for (File file in files) {
+ task.startItem(file.name)
+ referenceDataService.load(file)
+ task.endItem()
+ }
+ }
+ }
+}
+----
+
+A task and each of its items are recorded as steps of the application
context's `ApplicationStartup`, so a task costs nothing when nothing records
the start, and is recorded alongside Spring's own steps by Spring Boot
Actuator's `startup` endpoint when it is in use.
`StartupTask.isRecorded(applicationContext)` says whether anything records the
start, so a task can skip work it does only to report itself, such as counting
its items before it begins. When the number of items is not known, start the
task with a negative total.
+
+==== Health Checks
+
+With the page on, the port accepts connections from the moment the application
begins to start. A health check that makes an HTTP request, such as a
Kubernetes `httpGet` readiness probe, gets a `503` until the application is
ready and so behaves as before, but a check that only opens a connection, such
as a `tcpSocket` probe or a load balancer's TCP health check, reports the
application up while it is still starting.
Review Comment:
Agreed, fixed in 2095f7c94b. The section now separates the two phases. A
`503` comes only from the temporary server, before the embedded server takes
over the port. After that, any request that is not a browser opening a page
reaches the app while plugin startup hooks and `BootStrap` are still running,
so an ordinary `httpGet` probe can succeed early. It also says the status
address answers `200` with progress and is not a readiness check, and it points
operators at Actuator's `/actuator/health/readiness`. That endpoint stays `503`
until `ApplicationReadyEvent`, which is after `BootStrap`; probes are on by
default in Boot 4.1.
--
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]