Skip to content

Commit 18f85cb

Browse files
committed
Extended CRaC checkpoint/restore support, previously limited to Hikari,
also to Oracle UCP, generalizing the approach and preparing the path for supporting other connection pool implementations. Signed-off-by: Fabio Grassi <fabio.grassi.ts@gmail.com>
1 parent 3575c12 commit 18f85cb

7 files changed

Lines changed: 1325 additions & 38 deletions

module/spring-boot-jdbc/src/main/java/org/springframework/boot/jdbc/HikariCheckpointRestoreLifecycle.java

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,11 @@
2525
import java.util.concurrent.TimeoutException;
2626
import java.util.function.Function;
2727

28-
import javax.sql.DataSource;
29-
30-
import com.zaxxer.hikari.HikariConfigMXBean;
3128
import com.zaxxer.hikari.HikariDataSource;
3229
import com.zaxxer.hikari.HikariPoolMXBean;
3330
import com.zaxxer.hikari.pool.HikariPool;
3431
import org.apache.commons.logging.Log;
3532
import org.apache.commons.logging.LogFactory;
36-
import org.jspecify.annotations.Nullable;
3733

3834
import org.springframework.context.ConfigurableApplicationContext;
3935
import org.springframework.context.Lifecycle;
@@ -52,6 +48,7 @@
5248
* @author Christoph Strobl
5349
* @author Andy Wilkinson
5450
* @author Moritz Halbritter
51+
* @author Fabio Grassi
5552
* @since 3.2.0
5653
*/
5754
public class HikariCheckpointRestoreLifecycle implements Lifecycle {
@@ -72,21 +69,20 @@ public class HikariCheckpointRestoreLifecycle implements Lifecycle {
7269

7370
private final Function<HikariPool, Boolean> hasOpenConnections;
7471

75-
private final @Nullable HikariDataSource dataSource;
72+
private final HikariDataSource dataSource;
7673

7774
private final ConfigurableApplicationContext applicationContext;
7875

7976
/**
8077
* Creates a new {@code HikariCheckpointRestoreLifecycle} that will allow the given
81-
* {@code dataSource} to participate in checkpoint-restore. The {@code dataSource} is
82-
* {@link DataSourceUnwrapper#unwrap unwrapped} to a {@link HikariDataSource}. If such
83-
* unwrapping is not possible, the lifecycle will have no effect.
78+
* {@link HikariDataSource} to participate in checkpoint-restore.
8479
* @param dataSource the checkpoint-restore participant
8580
* @param applicationContext the application context
8681
* @since 3.4.0
8782
*/
88-
public HikariCheckpointRestoreLifecycle(DataSource dataSource, ConfigurableApplicationContext applicationContext) {
89-
this.dataSource = DataSourceUnwrapper.unwrap(dataSource, HikariConfigMXBean.class, HikariDataSource.class);
83+
public HikariCheckpointRestoreLifecycle(HikariDataSource dataSource,
84+
ConfigurableApplicationContext applicationContext) {
85+
this.dataSource = dataSource;
9086
this.applicationContext = applicationContext;
9187
this.hasOpenConnections = (pool) -> {
9288
ThreadPoolExecutor closeConnectionExecutor = (ThreadPoolExecutor) ReflectionUtils
@@ -98,7 +94,7 @@ public HikariCheckpointRestoreLifecycle(DataSource dataSource, ConfigurableAppli
9894

9995
@Override
10096
public void start() {
101-
if (this.dataSource == null || this.dataSource.isRunning()) {
97+
if (this.dataSource.isRunning()) {
10298
return;
10399
}
104100
Assert.state(!this.dataSource.isClosed(), "DataSource has been closed and cannot be restarted");
@@ -110,7 +106,7 @@ public void start() {
110106

111107
@Override
112108
public void stop() {
113-
if (this.dataSource == null || !this.dataSource.isRunning()) {
109+
if (!this.dataSource.isRunning()) {
114110
return;
115111
}
116112
if (this.dataSource.isAllowPoolSuspension()) {
@@ -164,7 +160,7 @@ private void waitForConnectionsToClose(HikariDataSource dataSource) {
164160

165161
@Override
166162
public boolean isRunning() {
167-
return this.dataSource != null && this.dataSource.isRunning();
163+
return this.dataSource.isRunning();
168164
}
169165

170166
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/*
2+
* Copyright 2012-present 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.jdbc;
18+
19+
import java.sql.SQLException;
20+
import java.util.Arrays;
21+
22+
import oracle.ucp.UniversalConnectionPoolException;
23+
import oracle.ucp.admin.UniversalConnectionPoolManager;
24+
import oracle.ucp.admin.UniversalConnectionPoolManagerImpl;
25+
import oracle.ucp.jdbc.JDBCConnectionPool;
26+
import oracle.ucp.jdbc.PoolDataSourceImpl;
27+
import org.jspecify.annotations.Nullable;
28+
import org.slf4j.Logger;
29+
import org.slf4j.LoggerFactory;
30+
31+
import org.springframework.context.Lifecycle;
32+
import org.springframework.util.Assert;
33+
34+
/**
35+
* A {@link Lifecycle} over the connection pool of a single
36+
* {@link oracle.ucp.jdbc.PoolDataSourceImpl}, which lets the pool be started and stopped
37+
* along with the application context without being destroyed in between.
38+
* <p>
39+
* {@link #start()} creates the pool when it does not exist yet, since a pool data source
40+
* has no {@code connectionPoolName} until then, and UCP registers a freshly created pool
41+
* in the stopped state. Both {@code start()} and {@link #stop()} guard on the current
42+
* life cycle state, as UCP rejects a transition that has already happened.
43+
*
44+
* @author Fabio Grassi
45+
* @since 4.1.0
46+
*/
47+
public final class OracleUcpCheckpointRestoreLifecycle implements Lifecycle {
48+
49+
private static final Logger logger = LoggerFactory.getLogger(OracleUcpCheckpointRestoreLifecycle.class);
50+
51+
private final PoolDataSourceImpl poolDataSource;
52+
53+
public OracleUcpCheckpointRestoreLifecycle(final PoolDataSourceImpl poolDataSource) {
54+
Assert.notNull(poolDataSource, "Non null PoolDataSourceImpl instance expected");
55+
this.poolDataSource = poolDataSource;
56+
}
57+
58+
@Override
59+
public void start() {
60+
JDBCConnectionPool pool = getPool(this.poolDataSource.getConnectionPoolName());
61+
if (pool == null) {
62+
pool = createPool();
63+
logger.info("Created new Oracle Universal Connection Pool named '{}'", pool.getName());
64+
}
65+
if (!pool.isLifecycleRunning() && !pool.isLifecycleStarting()) {
66+
doWithPool(pool::start);
67+
logger.info("Oracle Universal Connection Pool '{}' started", pool.getName());
68+
}
69+
}
70+
71+
@Override
72+
public void stop() {
73+
final JDBCConnectionPool pool = getPool(this.poolDataSource.getConnectionPoolName());
74+
if (pool != null && !pool.isLifecycleStopped() && !pool.isLifecycleStopping()) {
75+
doWithPool(pool::stop);
76+
logger.info("Oracle Universal Connection Pool '{}' stopped", pool.getName());
77+
}
78+
}
79+
80+
@Override
81+
public boolean isRunning() {
82+
final JDBCConnectionPool pool = getPool(this.poolDataSource.getConnectionPoolName());
83+
final boolean isRunning = pool != null && pool.isLifecycleRunning();
84+
logger.info("Oracle Universal Connection Pool '{}' is {}running", this.poolDataSource.getConnectionPoolName(),
85+
isRunning ? "" : "not ");
86+
return isRunning;
87+
}
88+
89+
private JDBCConnectionPool createPool() {
90+
try {
91+
return (JDBCConnectionPool) this.poolDataSource.createUniversalConnectionPool();
92+
}
93+
catch (SQLException sqle) {
94+
throw new IllegalStateException("Failed to create new Oracle Universal Connection Pool", sqle);
95+
}
96+
}
97+
98+
private static @Nullable JDBCConnectionPool getPool(final @Nullable String poolName) {
99+
try {
100+
final UniversalConnectionPoolManager mgr = UniversalConnectionPoolManagerImpl
101+
.getUniversalConnectionPoolManager();
102+
if (Arrays.asList(mgr.getConnectionPoolNames()).contains(poolName)) {
103+
return (JDBCConnectionPool) mgr.getConnectionPool(poolName);
104+
}
105+
}
106+
catch (UniversalConnectionPoolException ucpe) {
107+
throw new IllegalStateException("Failed to retrieve existing Oracle Universal Connection Pool", ucpe);
108+
}
109+
return null;
110+
}
111+
112+
private static void doWithPool(final PoolCommand command) {
113+
try {
114+
command.execute();
115+
}
116+
catch (UniversalConnectionPoolException ucpe) {
117+
throw new IllegalStateException("Oracle Universal Connection Pool command failed", ucpe);
118+
}
119+
}
120+
121+
@FunctionalInterface
122+
private interface PoolCommand {
123+
124+
void execute() throws UniversalConnectionPoolException;
125+
126+
}
127+
128+
}

module/spring-boot-jdbc/src/main/java/org/springframework/boot/jdbc/autoconfigure/DataSourceCheckpointRestoreConfiguration.java

Lines changed: 137 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,27 +16,43 @@
1616

1717
package org.springframework.boot.jdbc.autoconfigure;
1818

19+
import java.util.Collection;
20+
import java.util.LinkedList;
21+
import java.util.function.Function;
22+
1923
import javax.sql.DataSource;
2024

25+
import com.zaxxer.hikari.HikariConfigMXBean;
2126
import com.zaxxer.hikari.HikariDataSource;
27+
import oracle.jdbc.OracleConnection;
28+
import oracle.ucp.jdbc.PoolDataSource;
29+
import oracle.ucp.jdbc.PoolDataSourceImpl;
2230

23-
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
31+
import org.springframework.beans.factory.ObjectProvider;
32+
import org.springframework.beans.factory.SmartInitializingSingleton;
33+
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
2434
import org.springframework.boot.autoconfigure.condition.ConditionalOnCheckpointRestore;
2535
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
2636
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
37+
import org.springframework.boot.jdbc.DataSourceUnwrapper;
2738
import org.springframework.boot.jdbc.HikariCheckpointRestoreLifecycle;
39+
import org.springframework.boot.jdbc.OracleUcpCheckpointRestoreLifecycle;
40+
import org.springframework.boot.jdbc.autoconfigure.DataSourceCheckpointRestoreConfiguration.CheckpointRestorePoolsAvailableCondition;
2841
import org.springframework.context.ConfigurableApplicationContext;
42+
import org.springframework.context.Lifecycle;
2943
import org.springframework.context.annotation.Bean;
44+
import org.springframework.context.annotation.Conditional;
3045
import org.springframework.context.annotation.Configuration;
3146

3247
/**
3348
* Checkpoint-restore specific configuration.
3449
*
3550
* @author Olga Maciaszek-Sharma
51+
* @author Fabio Grassi
3652
*/
3753
@Configuration(proxyBeanMethods = false)
3854
@ConditionalOnCheckpointRestore
39-
@ConditionalOnBean(DataSource.class)
55+
@Conditional(CheckpointRestorePoolsAvailableCondition.class)
4056
class DataSourceCheckpointRestoreConfiguration {
4157

4258
@Configuration(proxyBeanMethods = false)
@@ -45,9 +61,125 @@ static class Hikari {
4561

4662
@Bean
4763
@ConditionalOnMissingBean
48-
HikariCheckpointRestoreLifecycle hikariCheckpointRestoreLifecycle(DataSource dataSource,
49-
ConfigurableApplicationContext applicationContext) {
50-
return new HikariCheckpointRestoreLifecycle(dataSource, applicationContext);
64+
HikariCheckpointRestoreLifecycleRegistry hikariCheckpointRestoreLifecycle(
65+
final ObjectProvider<DataSource> dataSources, final ConfigurableApplicationContext applicationContext) {
66+
return new HikariCheckpointRestoreLifecycleRegistry(dataSources, applicationContext);
67+
}
68+
69+
static final class HikariCheckpointRestoreLifecycleRegistry
70+
extends DataSourceCheckpointRestoreLifecycleRegistry<HikariConfigMXBean, HikariDataSource> {
71+
72+
HikariCheckpointRestoreLifecycleRegistry(final ObjectProvider<DataSource> dataSources,
73+
final ConfigurableApplicationContext applicationContext) {
74+
super(dataSources, HikariConfigMXBean.class, HikariDataSource.class,
75+
hds -> new HikariCheckpointRestoreLifecycle(hds, applicationContext));
76+
}
77+
78+
}
79+
80+
}
81+
82+
@Configuration(proxyBeanMethods = false)
83+
@ConditionalOnClass({ PoolDataSourceImpl.class, OracleConnection.class })
84+
static class OracleUcp {
85+
86+
@Bean
87+
@ConditionalOnMissingBean
88+
OracleUcpCheckpointRestoreLifecycleRegistry oracleUcpCheckpointRestoreLifecycle(
89+
final ObjectProvider<DataSource> dataSources) {
90+
return new OracleUcpCheckpointRestoreLifecycleRegistry(dataSources);
91+
}
92+
93+
static final class OracleUcpCheckpointRestoreLifecycleRegistry
94+
extends DataSourceCheckpointRestoreLifecycleRegistry<PoolDataSource, PoolDataSourceImpl> {
95+
96+
OracleUcpCheckpointRestoreLifecycleRegistry(final ObjectProvider<DataSource> dataSources) {
97+
super(dataSources, PoolDataSource.class, PoolDataSourceImpl.class,
98+
OracleUcpCheckpointRestoreLifecycle::new);
99+
}
100+
101+
}
102+
103+
}
104+
105+
static class CheckpointRestorePoolsAvailableCondition extends AnyNestedCondition {
106+
107+
CheckpointRestorePoolsAvailableCondition() {
108+
super(ConfigurationPhase.PARSE_CONFIGURATION);
109+
}
110+
111+
@ConditionalOnClass(HikariDataSource.class)
112+
static class HickariAvailable {
113+
114+
}
115+
116+
@ConditionalOnClass({ PoolDataSourceImpl.class, OracleConnection.class })
117+
static class OracleUcpAvailable {
118+
119+
}
120+
121+
}
122+
123+
/**
124+
* A {@link Lifecycle} container that propagates {@code start()} and {@code stop()}
125+
* signals to all its elements and {@code isRunning()} if and only if all its elements
126+
* are running or there are no elements.
127+
* <p>
128+
* This class implements also {@link SmartInitializingSingleton} to hook into the bean
129+
* factory lifecyle after all singleton beans registration and iterate over all
130+
* {@code DataSource}s, including the ones that are neither default nor autowire
131+
* candidates, unwrap each of them to reach the underlying data source, supply it to
132+
* the given factory to create a {@code Lifecycle} and add it its elements.
133+
*
134+
* @author Fabio Grassi
135+
* @since 4.1.0
136+
*/
137+
static sealed class DataSourceCheckpointRestoreLifecycleRegistry<I, T extends I>
138+
implements SmartInitializingSingleton, Lifecycle {
139+
140+
private final ObjectProvider<DataSource> dataSources;
141+
142+
private final Class<I> wrappingInterface;
143+
144+
private final Class<T> targetClass;
145+
146+
private final Function<T, Lifecycle> lifecycleFactory;
147+
148+
private final Collection<Lifecycle> lifecycles;
149+
150+
DataSourceCheckpointRestoreLifecycleRegistry(final ObjectProvider<DataSource> dataSources,
151+
final Class<I> wrappingInterface, final Class<T> targetClass,
152+
final Function<T, Lifecycle> lifecycleFactory) {
153+
this.dataSources = dataSources;
154+
this.wrappingInterface = wrappingInterface;
155+
this.targetClass = targetClass;
156+
this.lifecycleFactory = lifecycleFactory;
157+
this.lifecycles = new LinkedList<>();
158+
}
159+
160+
@Override
161+
public void afterSingletonsInstantiated() {
162+
this.dataSources.stream(ObjectProvider.UNFILTERED, false).forEach(ds -> {
163+
final T unwrapped = DataSourceUnwrapper.unwrap(ds, this.wrappingInterface, this.targetClass);
164+
if (unwrapped != null) {
165+
this.lifecycles.add(this.lifecycleFactory.apply(unwrapped));
166+
}
167+
});
168+
}
169+
170+
@Override
171+
public void start() {
172+
this.lifecycles.forEach(Lifecycle::start);
173+
}
174+
175+
@Override
176+
public void stop() {
177+
this.lifecycles.forEach(Lifecycle::stop);
178+
}
179+
180+
@Override
181+
public boolean isRunning() {
182+
return this.lifecycles.stream().allMatch(Lifecycle::isRunning);
51183
}
52184

53185
}

0 commit comments

Comments
 (0)