Skip to content

Commit b93a629

Browse files
committed
Shutdown in-memory R2DBC databases before devtools restart
Add `DevToolsR2dbcAutoConfiguration` to automatically shutdown in-memory R2DBC databases before restarting. Prior to this commit, restarts that involved SQL initialization scripts could fail due to dirty database content. The `DevToolsR2dbcAutoConfiguration` class is similar in design to `DevToolsDataSourceAutoConfiguration`, but it applies to both pooled and non-pooled connection factories. The `DataSource` variant does not need to deal with non-pooled connections due to the fact that `EmbeddedDataSourceConfiguration` calls `EmbeddedDatabase.shutdown` as a `destroyMethod`. With R2DB we don't have an `EmbeddedDatabase` equivalent so we can always trigger a shutdown for devtools. Fixes gh-28345
1 parent 19d3007 commit b93a629

File tree

4 files changed

+357
-0
lines changed

4 files changed

+357
-0
lines changed

spring-boot-project/spring-boot-devtools/build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ dependencies {
3232

3333
intTestRuntimeOnly("org.springframework:spring-web")
3434

35+
optional("io.projectreactor:reactor-core")
36+
optional("io.r2dbc:r2dbc-spi")
3537
optional("javax.servlet:javax.servlet-api")
3638
optional("org.apache.derby:derby")
3739
optional("org.hibernate:hibernate-core")
@@ -72,6 +74,7 @@ dependencies {
7274

7375
testRuntimeOnly("org.aspectj:aspectjweaver")
7476
testRuntimeOnly("org.yaml:snakeyaml")
77+
testRuntimeOnly("io.r2dbc:r2dbc-h2")
7578
}
7679

7780
task syncIntTestDependencies(type: Sync) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/*
2+
* Copyright 2012-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.devtools.autoconfigure;
18+
19+
import io.r2dbc.spi.Connection;
20+
import io.r2dbc.spi.ConnectionFactory;
21+
import org.reactivestreams.Publisher;
22+
import reactor.core.publisher.Mono;
23+
24+
import org.springframework.beans.factory.DisposableBean;
25+
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
26+
import org.springframework.beans.factory.config.BeanDefinition;
27+
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
28+
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
29+
import org.springframework.boot.autoconfigure.condition.ConditionMessage;
30+
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
31+
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
32+
import org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration;
33+
import org.springframework.boot.devtools.autoconfigure.DevToolsR2dbcAutoConfiguration.DevToolsConnectionFactoryCondition;
34+
import org.springframework.boot.r2dbc.EmbeddedDatabaseConnection;
35+
import org.springframework.context.ApplicationEventPublisher;
36+
import org.springframework.context.annotation.Bean;
37+
import org.springframework.context.annotation.ConditionContext;
38+
import org.springframework.context.annotation.Conditional;
39+
import org.springframework.context.annotation.Configuration;
40+
import org.springframework.context.annotation.ConfigurationCondition;
41+
import org.springframework.core.type.AnnotatedTypeMetadata;
42+
import org.springframework.core.type.MethodMetadata;
43+
44+
/**
45+
* {@link EnableAutoConfiguration Auto-configuration} for DevTools-specific R2DBC
46+
* configuration.
47+
*
48+
* @author Phillip Webb
49+
* @since 2.5.6
50+
*/
51+
@AutoConfigureAfter(R2dbcAutoConfiguration.class)
52+
@Conditional({ OnEnabledDevToolsCondition.class, DevToolsConnectionFactoryCondition.class })
53+
@Configuration(proxyBeanMethods = false)
54+
public class DevToolsR2dbcAutoConfiguration {
55+
56+
@Bean
57+
InMemoryR2dbcDatabaseShutdownExecutor inMemoryR2dbcDatabaseShutdownExecutor(
58+
ApplicationEventPublisher eventPublisher, ConnectionFactory connectionFactory) {
59+
return new InMemoryR2dbcDatabaseShutdownExecutor(eventPublisher, connectionFactory);
60+
}
61+
62+
final class InMemoryR2dbcDatabaseShutdownExecutor implements DisposableBean {
63+
64+
private final ApplicationEventPublisher eventPublisher;
65+
66+
private final ConnectionFactory connectionFactory;
67+
68+
InMemoryR2dbcDatabaseShutdownExecutor(ApplicationEventPublisher eventPublisher,
69+
ConnectionFactory connectionFactory) {
70+
this.eventPublisher = eventPublisher;
71+
this.connectionFactory = connectionFactory;
72+
}
73+
74+
@Override
75+
public void destroy() throws Exception {
76+
if (shouldShutdown()) {
77+
Mono.usingWhen(this.connectionFactory.create(), this::executeShutdown, this::closeConnection,
78+
this::closeConnection, this::closeConnection).block();
79+
this.eventPublisher.publishEvent(new R2dbcDatabaseShutdownEvent(this.connectionFactory));
80+
}
81+
}
82+
83+
private boolean shouldShutdown() {
84+
try {
85+
return EmbeddedDatabaseConnection.isEmbedded(this.connectionFactory);
86+
}
87+
catch (Exception ex) {
88+
return false;
89+
}
90+
}
91+
92+
private Mono<?> executeShutdown(Connection connection) {
93+
return Mono.from(connection.createStatement("SHUTDOWN").execute());
94+
}
95+
96+
private Publisher<Void> closeConnection(Connection connection) {
97+
return closeConnection(connection, null);
98+
}
99+
100+
private Publisher<Void> closeConnection(Connection connection, Throwable ex) {
101+
return connection.close();
102+
}
103+
104+
}
105+
106+
static class DevToolsConnectionFactoryCondition extends SpringBootCondition implements ConfigurationCondition {
107+
108+
@Override
109+
public ConfigurationPhase getConfigurationPhase() {
110+
return ConfigurationPhase.REGISTER_BEAN;
111+
}
112+
113+
@Override
114+
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
115+
ConditionMessage.Builder message = ConditionMessage.forCondition("DevTools ConnectionFactory Condition");
116+
String[] beanNames = context.getBeanFactory().getBeanNamesForType(ConnectionFactory.class, true, false);
117+
if (beanNames.length != 1) {
118+
return ConditionOutcome.noMatch(message.didNotFind("a single ConnectionFactory bean").atAll());
119+
}
120+
BeanDefinition beanDefinition = context.getRegistry().getBeanDefinition(beanNames[0]);
121+
if (beanDefinition instanceof AnnotatedBeanDefinition
122+
&& isAutoConfigured((AnnotatedBeanDefinition) beanDefinition)) {
123+
return ConditionOutcome.match(message.foundExactly("auto-configured ConnectionFactory"));
124+
}
125+
return ConditionOutcome.noMatch(message.didNotFind("an auto-configured ConnectionFactory").atAll());
126+
}
127+
128+
private boolean isAutoConfigured(AnnotatedBeanDefinition beanDefinition) {
129+
MethodMetadata methodMetadata = beanDefinition.getFactoryMethodMetadata();
130+
return methodMetadata != null && methodMetadata.getDeclaringClassName()
131+
.startsWith(R2dbcAutoConfiguration.class.getPackage().getName());
132+
}
133+
134+
}
135+
136+
static class R2dbcDatabaseShutdownEvent {
137+
138+
private final ConnectionFactory connectionFactory;
139+
140+
R2dbcDatabaseShutdownEvent(ConnectionFactory connectionFactory) {
141+
this.connectionFactory = connectionFactory;
142+
}
143+
144+
ConnectionFactory getConnectionFactory() {
145+
return this.connectionFactory;
146+
}
147+
148+
}
149+
150+
}

spring-boot-project/spring-boot-devtools/src/main/resources/META-INF/spring.factories

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ org.springframework.boot.devtools.logger.DevToolsLogFactory.Listener
1010
# Auto Configure
1111
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
1212
org.springframework.boot.devtools.autoconfigure.DevToolsDataSourceAutoConfiguration,\
13+
org.springframework.boot.devtools.autoconfigure.DevToolsR2dbcAutoConfiguration,\
1314
org.springframework.boot.devtools.autoconfigure.LocalDevToolsAutoConfiguration,\
1415
org.springframework.boot.devtools.autoconfigure.RemoteDevToolsAutoConfiguration
1516

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
/*
2+
* Copyright 2012-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.devtools.autoconfigure;
18+
19+
import java.util.ArrayList;
20+
import java.util.Collection;
21+
import java.util.Collections;
22+
import java.util.List;
23+
import java.util.concurrent.atomic.AtomicReference;
24+
import java.util.function.Supplier;
25+
26+
import io.r2dbc.spi.Connection;
27+
import io.r2dbc.spi.ConnectionFactory;
28+
import io.r2dbc.spi.ConnectionFactoryMetadata;
29+
import org.junit.jupiter.api.BeforeEach;
30+
import org.junit.jupiter.api.Nested;
31+
import org.junit.jupiter.api.Test;
32+
import org.reactivestreams.Publisher;
33+
34+
import org.springframework.beans.factory.annotation.AnnotatedGenericBeanDefinition;
35+
import org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration;
36+
import org.springframework.boot.devtools.autoconfigure.DevToolsR2dbcAutoConfiguration.R2dbcDatabaseShutdownEvent;
37+
import org.springframework.boot.test.util.TestPropertyValues;
38+
import org.springframework.boot.testsupport.classpath.ClassPathExclusions;
39+
import org.springframework.context.ApplicationListener;
40+
import org.springframework.context.ConfigurableApplicationContext;
41+
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
42+
import org.springframework.context.annotation.Bean;
43+
import org.springframework.context.annotation.Configuration;
44+
import org.springframework.util.ObjectUtils;
45+
46+
import static org.assertj.core.api.Assertions.assertThat;
47+
48+
/**
49+
* Tests for {@link DevToolsR2dbcAutoConfiguration}.
50+
*
51+
* @author Phillip Webb
52+
*/
53+
class DevToolsR2dbcAutoConfigurationTests {
54+
55+
static List<ConnectionFactory> shutdowns = Collections.synchronizedList(new ArrayList<>());
56+
57+
abstract static class Common {
58+
59+
@BeforeEach
60+
void reset() {
61+
shutdowns.clear();
62+
}
63+
64+
@Test
65+
void autoConfiguredInMemoryConnectionFactoryIsShutdown() throws Exception {
66+
ConfigurableApplicationContext context = getContext(() -> createContext());
67+
ConnectionFactory connectionFactory = context.getBean(ConnectionFactory.class);
68+
context.close();
69+
assertThat(shutdowns).contains(connectionFactory);
70+
}
71+
72+
@Test
73+
void nonEmbeddedConnectionFactoryIsNotShutdown() throws Exception {
74+
ConfigurableApplicationContext context = getContext(() -> createContext("r2dbc:h2:file:///testdb"));
75+
ConnectionFactory connectionFactory = context.getBean(ConnectionFactory.class);
76+
context.close();
77+
assertThat(shutdowns).doesNotContain(connectionFactory);
78+
}
79+
80+
@Test
81+
void singleManuallyConfiguredConnectionFactoryIsNotClosed() throws Exception {
82+
ConfigurableApplicationContext context = getContext(
83+
() -> createContext(SingleConnectionFactoryConfiguration.class));
84+
ConnectionFactory connectionFactory = context.getBean(ConnectionFactory.class);
85+
context.close();
86+
assertThat(shutdowns).doesNotContain(connectionFactory);
87+
}
88+
89+
@Test
90+
void multipleConnectionFactoriesAreIgnored() throws Exception {
91+
ConfigurableApplicationContext context = getContext(
92+
() -> createContext(MultipleConnectionFactoriesConfiguration.class));
93+
Collection<ConnectionFactory> connectionFactory = context.getBeansOfType(ConnectionFactory.class).values();
94+
context.close();
95+
assertThat(shutdowns).doesNotContainAnyElementsOf(connectionFactory);
96+
}
97+
98+
@Test
99+
void emptyFactoryMethodMetadataIgnored() throws Exception {
100+
ConfigurableApplicationContext context = getContext(this::getEmptyFactoryMethodMetadataIgnoredContext);
101+
ConnectionFactory connectionFactory = context.getBean(ConnectionFactory.class);
102+
context.close();
103+
assertThat(shutdowns).doesNotContain(connectionFactory);
104+
}
105+
106+
private ConfigurableApplicationContext getEmptyFactoryMethodMetadataIgnoredContext() {
107+
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
108+
ConnectionFactory connectionFactory = new MockConnectionFactory();
109+
AnnotatedGenericBeanDefinition beanDefinition = new AnnotatedGenericBeanDefinition(
110+
connectionFactory.getClass());
111+
context.registerBeanDefinition("connectionFactory", beanDefinition);
112+
context.register(R2dbcAutoConfiguration.class, DevToolsR2dbcAutoConfiguration.class);
113+
context.refresh();
114+
return context;
115+
}
116+
117+
protected ConfigurableApplicationContext getContext(Supplier<ConfigurableApplicationContext> supplier)
118+
throws Exception {
119+
AtomicReference<ConfigurableApplicationContext> atomicReference = new AtomicReference<>();
120+
Thread thread = new Thread(() -> {
121+
ConfigurableApplicationContext context = supplier.get();
122+
atomicReference.getAndSet(context);
123+
});
124+
thread.start();
125+
thread.join();
126+
return atomicReference.get();
127+
}
128+
129+
protected final ConfigurableApplicationContext createContext(Class<?>... classes) {
130+
return createContext(null, classes);
131+
}
132+
133+
protected final ConfigurableApplicationContext createContext(String url, Class<?>... classes) {
134+
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
135+
if (!ObjectUtils.isEmpty(classes)) {
136+
context.register(classes);
137+
}
138+
context.register(R2dbcAutoConfiguration.class, DevToolsR2dbcAutoConfiguration.class);
139+
if (url != null) {
140+
TestPropertyValues.of("spring.r2dbc.url:" + url).applyTo(context);
141+
}
142+
context.addApplicationListener(ApplicationListener.forPayload(this::onEvent));
143+
context.refresh();
144+
return context;
145+
}
146+
147+
private void onEvent(R2dbcDatabaseShutdownEvent event) {
148+
shutdowns.add(event.getConnectionFactory());
149+
}
150+
151+
}
152+
153+
@Nested
154+
@ClassPathExclusions("r2dbc-pool*.jar")
155+
static class Embedded extends Common {
156+
157+
}
158+
159+
@Nested
160+
static class Pooled extends Common {
161+
162+
}
163+
164+
@Configuration(proxyBeanMethods = false)
165+
static class SingleConnectionFactoryConfiguration {
166+
167+
@Bean
168+
ConnectionFactory connectionFactory() {
169+
return new MockConnectionFactory();
170+
}
171+
172+
}
173+
174+
@Configuration(proxyBeanMethods = false)
175+
static class MultipleConnectionFactoriesConfiguration {
176+
177+
@Bean
178+
ConnectionFactory connectionFactoryOne() {
179+
return new MockConnectionFactory();
180+
}
181+
182+
@Bean
183+
ConnectionFactory connectionFactoryTwo() {
184+
return new MockConnectionFactory();
185+
}
186+
187+
}
188+
189+
private static class MockConnectionFactory implements ConnectionFactory {
190+
191+
@Override
192+
public Publisher<? extends Connection> create() {
193+
return null;
194+
}
195+
196+
@Override
197+
public ConnectionFactoryMetadata getMetadata() {
198+
return null;
199+
}
200+
201+
}
202+
203+
}

0 commit comments

Comments
 (0)