Allow docker compose service readiness checks to be bypassed

Add `spring.docker.compose.readiness.wait` property that can be used to
determine how Spring Boot should wait for docker compose services to
become ready.

Fixes gh-35545
This commit is contained in:
Phillip Webb 2023-05-17 16:21:34 -07:00
parent d018aa8f6d
commit a8602a1814
4 changed files with 95 additions and 5 deletions

View File

@ -29,6 +29,7 @@ import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.docker.compose.core.DockerCompose;
import org.springframework.boot.docker.compose.core.DockerComposeFile;
import org.springframework.boot.docker.compose.core.RunningService;
import org.springframework.boot.docker.compose.lifecycle.DockerComposeProperties.Readiness.Wait;
import org.springframework.boot.docker.compose.lifecycle.DockerComposeProperties.Start;
import org.springframework.boot.docker.compose.lifecycle.DockerComposeProperties.Stop;
import org.springframework.context.ApplicationContext;
@ -110,15 +111,19 @@ class DockerComposeLifecycleManager {
LifecycleManagement lifecycleManagement = this.properties.getLifecycleManagement();
Start start = this.properties.getStart();
Stop stop = this.properties.getStop();
Wait wait = this.properties.getReadiness().getWait();
if (lifecycleManagement.shouldStart() && !dockerCompose.hasRunningServices()) {
start.getCommand().applyTo(dockerCompose, start.getLogLevel());
wait = (wait != Wait.ONLY_IF_STARTED) ? wait : Wait.ALWAYS;
if (lifecycleManagement.shouldStop()) {
this.shutdownHandlers.add(() -> stop.getCommand().applyTo(dockerCompose, stop.getTimeout()));
}
}
List<RunningService> runningServices = new ArrayList<>(dockerCompose.getRunningServices());
runningServices.removeIf(this::isIgnored);
this.serviceReadinessChecks.waitUntilReady(runningServices);
if (wait == Wait.ALWAYS || wait == null) {
this.serviceReadinessChecks.waitUntilReady(runningServices);
}
publishEvent(new DockerComposeServicesReadyEvent(this.applicationContext, runningServices));
}

View File

@ -244,6 +244,11 @@ public class DockerComposeProperties {
*/
public static class Readiness {
/**
* Wait strategy to use.
*/
private Wait wait = Wait.ALWAYS;
/**
* Timeout of the readiness checks.
*/
@ -254,6 +259,14 @@ public class DockerComposeProperties {
*/
private final Tcp tcp = new Tcp();
public Wait getWait() {
return this.wait;
}
public void setWait(Wait wait) {
this.wait = wait;
}
public Duration getTimeout() {
return this.timeout;
}
@ -266,6 +279,29 @@ public class DockerComposeProperties {
return this.tcp;
}
/**
* Readiness wait strategies.
*/
public enum Wait {
/**
* Always perform readiness checks.
*/
ALWAYS,
/**
* Always perform readiness checks.
*/
NEVER,
/**
* Only perform readiness checks if docker was started with lifecycle
* management.
*/
ONLY_IF_STARTED
}
/**
* TCP properties.
*/

View File

@ -36,6 +36,7 @@ import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.docker.compose.core.DockerCompose;
import org.springframework.boot.docker.compose.core.DockerComposeFile;
import org.springframework.boot.docker.compose.core.RunningService;
import org.springframework.boot.docker.compose.lifecycle.DockerComposeProperties.Readiness.Wait;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationListener;
import org.springframework.context.support.GenericApplicationContext;
@ -268,7 +269,7 @@ class DockerComposeLifecycleManagerTests {
void startWhenHasIgnoreLabelIgnoresService() {
EventCapturingListener listener = new EventCapturingListener();
this.eventListeners.add(listener);
setUpRunningServices(Map.of("org.springframework.boot.ignore", "true"));
setUpRunningServices(true, Map.of("org.springframework.boot.ignore", "true"));
this.lifecycleManager.start();
this.shutdownHandlers.run();
assertThat(listener.getEvent()).isNotNull();
@ -285,6 +286,40 @@ class DockerComposeLifecycleManagerTests {
then(this.serviceReadinessChecks).should().waitUntilReady(this.runningServices);
}
@Test
void startWhenWaitNeverDoesNotWaitUntilReady() {
this.properties.getReadiness().setWait(Wait.NEVER);
EventCapturingListener listener = new EventCapturingListener();
this.eventListeners.add(listener);
setUpRunningServices();
this.lifecycleManager.start();
this.shutdownHandlers.run();
then(this.serviceReadinessChecks).should(never()).waitUntilReady(this.runningServices);
}
@Test
void startWhenWaitOnlyIfStartedAndNotStartedDoesNotWaitUntilReady() {
this.properties.getReadiness().setWait(Wait.ONLY_IF_STARTED);
this.properties.setLifecycleManagement(LifecycleManagement.NONE);
EventCapturingListener listener = new EventCapturingListener();
this.eventListeners.add(listener);
setUpRunningServices();
this.lifecycleManager.start();
this.shutdownHandlers.run();
then(this.serviceReadinessChecks).should(never()).waitUntilReady(this.runningServices);
}
@Test
void startWhenWaitOnlyIfStartedAndStartedWaitsUntilReady() {
this.properties.getReadiness().setWait(Wait.ONLY_IF_STARTED);
EventCapturingListener listener = new EventCapturingListener();
this.eventListeners.add(listener);
setUpRunningServices(false);
this.lifecycleManager.start();
this.shutdownHandlers.run();
then(this.serviceReadinessChecks).should().waitUntilReady(this.runningServices);
}
@Test
void startGetsDockerComposeWithActiveProfiles() {
this.properties.getProfiles().setActive(Set.of("my-profile"));
@ -306,16 +341,26 @@ class DockerComposeLifecycleManagerTests {
}
private void setUpRunningServices() {
setUpRunningServices(Collections.emptyMap());
setUpRunningServices(true);
}
private void setUpRunningServices(Map<String, String> labels) {
private void setUpRunningServices(boolean started) {
setUpRunningServices(started, Collections.emptyMap());
}
@SuppressWarnings("unchecked")
private void setUpRunningServices(boolean started, Map<String, String> labels) {
given(this.dockerCompose.hasDefinedServices()).willReturn(true);
given(this.dockerCompose.hasRunningServices()).willReturn(true);
RunningService runningService = mock(RunningService.class);
given(runningService.labels()).willReturn(labels);
this.runningServices = List.of(runningService);
given(this.dockerCompose.getRunningServices()).willReturn(this.runningServices);
if (started) {
given(this.dockerCompose.getRunningServices()).willReturn(this.runningServices);
}
else {
given(this.dockerCompose.getRunningServices()).willReturn(Collections.emptyList(), this.runningServices);
}
}
/**

View File

@ -25,6 +25,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.context.properties.source.MapConfigurationPropertySource;
import org.springframework.boot.docker.compose.lifecycle.DockerComposeProperties.Readiness.Wait;
import static org.assertj.core.api.Assertions.assertThat;
@ -48,6 +49,7 @@ class DockerComposePropertiesTests {
assertThat(properties.getStop().getCommand()).isEqualTo(StopCommand.STOP);
assertThat(properties.getStop().getTimeout()).isEqualTo(Duration.ofSeconds(10));
assertThat(properties.getProfiles().getActive()).isEmpty();
assertThat(properties.getReadiness().getWait()).isEqualTo(Wait.ALWAYS);
assertThat(properties.getReadiness().getTimeout()).isEqualTo(Duration.ofMinutes(2));
assertThat(properties.getReadiness().getTcp().getConnectTimeout()).isEqualTo(Duration.ofMillis(200));
assertThat(properties.getReadiness().getTcp().getReadTimeout()).isEqualTo(Duration.ofMillis(200));
@ -63,6 +65,7 @@ class DockerComposePropertiesTests {
source.put("spring.docker.compose.stop.command", "down");
source.put("spring.docker.compose.stop.timeout", "5s");
source.put("spring.docker.compose.profiles.active", "myprofile");
source.put("spring.docker.compose.readiness.wait", "only-if-started");
source.put("spring.docker.compose.readiness.timeout", "10s");
source.put("spring.docker.compose.readiness.tcp.connect-timeout", "400ms");
source.put("spring.docker.compose.readiness.tcp.read-timeout", "500ms");
@ -75,6 +78,7 @@ class DockerComposePropertiesTests {
assertThat(properties.getStop().getCommand()).isEqualTo(StopCommand.DOWN);
assertThat(properties.getStop().getTimeout()).isEqualTo(Duration.ofSeconds(5));
assertThat(properties.getProfiles().getActive()).containsExactly("myprofile");
assertThat(properties.getReadiness().getWait()).isEqualTo(Wait.ONLY_IF_STARTED);
assertThat(properties.getReadiness().getTimeout()).isEqualTo(Duration.ofSeconds(10));
assertThat(properties.getReadiness().getTcp().getConnectTimeout()).isEqualTo(Duration.ofMillis(400));
assertThat(properties.getReadiness().getTcp().getReadTimeout()).isEqualTo(Duration.ofMillis(500));