Skip to content

Commit 1cdae8b

Browse files
authored
Merge pull request #33 from avast/WaitingForStartingContainers
Check health state of containers Closes #31
2 parents 7d9b0f7 + d6a58b1 commit 1cdae8b

File tree

4 files changed

+32
-4
lines changed

4 files changed

+32
-4
lines changed

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
Simplifies usage of [Docker Compose](https://www.docker.com/docker-compose) for local development and integration testing in [Gradle](https://gradle.org/) environment.
55

6-
`composeUp` task starts the application and waits till all exposed TCP ports are open (so till the application is ready). It reads assigned host and ports of particular containers and stores them into `dockerCompose.servicesInfos` property.
6+
`composeUp` task starts the application and waits till all containers become [healthy](https://docs.docker.com/engine/reference/builder/#/healthcheck) and all exposed TCP ports are open (so till the application is ready). It reads assigned host and ports of particular containers and stores them into `dockerCompose.servicesInfos` property.
77

88
`composeDown` task stops the application and removes the containers.
99

@@ -14,8 +14,8 @@ Simplifies usage of [Docker Compose](https://www.docker.com/docker-compose) for
1414
## Why this plugin?
1515
You could easily ensure that `docker-compose up` is called before your tests but there are few gotchas that this plugin solves:
1616

17-
1. If you execute `docker-compose up -d` (_detached_) then this command returns immediately and your application is probably not able to serve requests at this time. This plugin waits till all exported TCP ports of all services are open.
18-
- If waiting for open TCP ports timeouts (default 15 minutes) then it prints log of related service.
17+
1. If you execute `docker-compose up -d` (_detached_) then this command returns immediately and your application is probably not able to serve requests at this time. This plugin waits till all containers become [healthy](https://docs.docker.com/engine/reference/builder/#/healthcheck) and all exported TCP ports of all services are open.
18+
- If waiting for healthy state or open TCP ports timeouts (default is 15 minutes) then it prints log of related service.
1919
2. It's recommended not to assign fixed values of exposed ports in `docker-compose.yml` (i.e. `8888:80`) because it can cause ports collision on integration servers. If you don't assign a fixed value for exposed port (use just `80`) then the port is exposed as a random free port. This plugin reads assigned ports (and even IP addresses of containers) and stores them into `dockerCompose.servicesInfo` map.
2020

2121
# Usage
@@ -63,7 +63,7 @@ test.doFirst {
6363
# Tips
6464
* You can call `dockerCompose.isRequiredBy(anyTask)` for any task, for example for your custom `integrationTest` task.
6565
* If some Dockerfile needs an artifact generated by Gradle then you can declare this dependency in a standard way, like `composeUp.dependsOn project(':my-app').distTar`
66-
* All properties in `dockerCompose` have meaningfull default values so you don't have to touch it. If you are interested then you can look at [ComposeExtension.groovy](/src/main/groovy/com/avast/gradle/dockercompose/ComposeExtension.groovy) for reference.
66+
* All properties in `dockerCompose` have meaningful default values so you don't have to touch it. If you are interested then you can look at [ComposeExtension.groovy](/src/main/groovy/com/avast/gradle/dockercompose/ComposeExtension.groovy) for reference.
6767
* `dockerCompose.servicesInfos` contains information about running containers so you must access this property after `composeUp` task is finished. So `doFirst` of your test task is perfect place where to access it.
6868
* Plugin honours a `docker-compose.override.yml` file, but only when no files are specified with `useComposeFiles` (conform command-line behavior).
6969
* Check [ServiceInfo.groovy](/src/main/groovy/com/avast/gradle/dockercompose/ServiceInfo.groovy) to see what you can know about running containers.

src/main/groovy/com/avast/gradle/dockercompose/ComposeExtension.groovy

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ class ComposeExtension {
1919
boolean waitForTcpPorts = true
2020
Duration waitAfterTcpProbeFailure = Duration.ofSeconds(1)
2121
Duration waitForTcpPortsTimeout = Duration.ofMinutes(15)
22+
Duration waitAfterHealthyStateProbeFailure = Duration.ofSeconds(5)
23+
Duration waitForHealthyStateTimeout = Duration.ofMinutes(15)
2224
List<String> useComposeFiles = []
2325

2426
boolean stopContainers = true

src/main/groovy/com/avast/gradle/dockercompose/ServiceInfo.groovy

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ class ServiceInfo {
1111
String containerHostname
1212
/* Docker inspection */
1313
Map<String, Object> inspection
14+
String getContainerId() { inspection.Id }
1415

1516
String getHost() { serviceHost.host }
1617
Map<Integer, Integer> getPorts() { tcpPorts }

src/main/groovy/com/avast/gradle/dockercompose/tasks/ComposeUp.groovy

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ class ComposeUp extends DefaultTask {
4141
}
4242
try {
4343
servicesInfos = loadServicesInfo().collectEntries { [(it.name): (it)] }
44+
waitForHealthyContainers(servicesInfos.values())
4445
if (extension.waitForTcpPorts) {
4546
waitForOpenTcpPorts(servicesInfos.values())
4647
}
@@ -162,6 +163,30 @@ class ComposeUp extends DefaultTask {
162163
ports
163164
}
164165

166+
void waitForHealthyContainers(Iterable<ServiceInfo> servicesInfos) {
167+
servicesInfos.forEach { service ->
168+
def start = Instant.now()
169+
while (true) {
170+
Map<String, Object> inspectionState = getDockerInspection(service.getContainerId()).State
171+
if (inspectionState.containsKey('Health')) {
172+
String healthStatus = inspectionState.Health.Status
173+
if (!"starting".equalsIgnoreCase(healthStatus)) {
174+
logger.lifecycle("${service.name} healh state reported as '$healthStatus' - continuing...")
175+
return
176+
}
177+
} else {
178+
logger.debug("Service ${service.name} or this version of Docker doesn't support healtchecks")
179+
return
180+
}
181+
if (start.plus(extension.waitForHealthyStateTimeout) < Instant.now()) {
182+
throw new RuntimeException("Container ${service.containerId} of service ${service.name} is still reported as 'starting'. Logs:${System.lineSeparator()}${getServiceLogs(service.name)}")
183+
}
184+
logger.lifecycle("Waiting for ${service.name} to become healthy")
185+
sleep(extension.waitAfterHealthyStateProbeFailure.toMillis())
186+
}
187+
}
188+
}
189+
165190
void waitForOpenTcpPorts(Iterable<ServiceInfo> servicesInfos) {
166191
servicesInfos.forEach { service ->
167192
service.tcpPorts.forEach { exposedPort, forwardedPort ->

0 commit comments

Comments
 (0)