Skip to content

Commit 7dcc0bb

Browse files
GH-8680: Check DB for table on start (#8690)
* GH-8680: Check DB for table on start Fixes #8680 If database is not initialized properly before application start, we may lose messages at runtime when we fail to insert data into DB * Implement `SmartLifecycle` on `JdbcMessageStore`, `JdbcChannelMessageStore`, `JdbcMetadataStore`, and `DefaultLockRepository` to perform `SELECT COUNT` query in `start()` to fail fast if no required table is present. * Refactor `AbstractJdbcChannelMessageStoreTests` into JUnit 5 and use `MySqlContainerTest` for more coverage * Fix newly failed tests which had DB not initialized before * Exclude `commons-logging` from `commons-dbcp2` dependency to avoid classpath conflict * Document the new feature * * Fix HTTP URL in the `DataSource-mysql-context.xml` * Fix language in docs Co-authored-by: Gary Russell <[email protected]> * * Add `setCheckDatabaseOnStart(false)` to disable the check query for all the SI JDBC components * Fix language in Javadocs Co-authored-by: Gary Russell <[email protected]> --------- Co-authored-by: Gary Russell <[email protected]>
1 parent 0c7d40d commit 7dcc0bb

25 files changed

+414
-104
lines changed

build.gradle

+3-1
Original file line numberDiff line numberDiff line change
@@ -740,7 +740,9 @@ project('spring-integration-jdbc') {
740740
testImplementation "org.apache.derby:derbyclient:$derbyVersion"
741741
testImplementation "org.postgresql:postgresql:$postgresVersion"
742742
testImplementation "mysql:mysql-connector-java:$mysqlVersion"
743-
testImplementation "org.apache.commons:commons-dbcp2:$commonsDbcp2Version"
743+
testImplementation ("org.apache.commons:commons-dbcp2:$commonsDbcp2Version") {
744+
exclude group: 'commons-logging'
745+
}
744746
testImplementation 'org.testcontainers:mysql'
745747
testImplementation 'org.testcontainers:postgresql'
746748

spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/lock/DefaultLockRepository.java

+57-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.time.LocalDateTime;
2121
import java.time.ZoneOffset;
2222
import java.util.UUID;
23+
import java.util.concurrent.atomic.AtomicBoolean;
2324

2425
import javax.sql.DataSource;
2526

@@ -29,6 +30,8 @@
2930
import org.springframework.beans.factory.SmartInitializingSingleton;
3031
import org.springframework.context.ApplicationContext;
3132
import org.springframework.context.ApplicationContextAware;
33+
import org.springframework.context.SmartLifecycle;
34+
import org.springframework.core.log.LogAccessor;
3235
import org.springframework.dao.DataIntegrityViolationException;
3336
import org.springframework.jdbc.core.JdbcTemplate;
3437
import org.springframework.transaction.PlatformTransactionManager;
@@ -45,6 +48,12 @@
4548
* Otherwise, it opens a possibility to break {@link java.util.concurrent.locks.Lock} contract,
4649
* where {@link JdbcLockRegistry} uses non-shared {@link java.util.concurrent.locks.ReentrantLock}s
4750
* for local synchronizations.
51+
* <p>
52+
* This class implements {@link SmartLifecycle} and calls
53+
* {@code SELECT COUNT(REGION) FROM %sLOCK} query
54+
* according to the provided prefix on {@link #start()} to check if required table is present in DB.
55+
* The application context will fail to start if the table is not present.
56+
* This check can be disabled via {@link #setCheckDatabaseOnStart(boolean)}.
4857
*
4958
* @author Dave Syer
5059
* @author Artem Bilan
@@ -56,7 +65,10 @@
5665
* @since 4.3
5766
*/
5867
public class DefaultLockRepository
59-
implements LockRepository, InitializingBean, ApplicationContextAware, SmartInitializingSingleton {
68+
implements LockRepository, InitializingBean, ApplicationContextAware, SmartInitializingSingleton,
69+
SmartLifecycle {
70+
71+
private static final LogAccessor LOGGER = new LogAccessor(DefaultLockRepository.class);
6072

6173
/**
6274
* Default value for the table prefix property.
@@ -72,6 +84,8 @@ public class DefaultLockRepository
7284

7385
private final JdbcTemplate template;
7486

87+
private final AtomicBoolean started = new AtomicBoolean();
88+
7589
private Duration ttl = DEFAULT_TTL;
7690

7791
private String prefix = DEFAULT_TABLE_PREFIX;
@@ -116,6 +130,10 @@ SELECT COUNT(REGION)
116130
WHERE REGION=? AND LOCK_KEY=? AND CLIENT_ID=?
117131
""";
118132

133+
private String countAllQuery = """
134+
SELECT COUNT(REGION) FROM %sLOCK
135+
""";
136+
119137
private ApplicationContext applicationContext;
120138

121139
private PlatformTransactionManager transactionManager;
@@ -126,6 +144,8 @@ SELECT COUNT(REGION)
126144

127145
private TransactionTemplate serializableTransactionTemplate;
128146

147+
private boolean checkDatabaseOnStart = true;
148+
129149
/**
130150
* Constructor that initializes the client id that will be associated for
131151
* all the locks persisted by the store instance to a random {@link UUID}.
@@ -293,6 +313,7 @@ public void afterPropertiesSet() {
293313
this.insertQuery = String.format(this.insertQuery, this.prefix);
294314
this.countQuery = String.format(this.countQuery, this.prefix);
295315
this.renewQuery = String.format(this.renewQuery, this.prefix);
316+
this.countAllQuery = String.format(this.countAllQuery, this.prefix);
296317
}
297318

298319
@Override
@@ -325,6 +346,41 @@ public void afterSingletonsInstantiated() {
325346
this.serializableTransactionTemplate = new TransactionTemplate(this.transactionManager, transactionDefinition);
326347
}
327348

349+
/**
350+
* The flag to perform a database check query on start or not.
351+
* @param checkDatabaseOnStart false to not perform the database check.
352+
* @since 6.2
353+
*/
354+
public void setCheckDatabaseOnStart(boolean checkDatabaseOnStart) {
355+
this.checkDatabaseOnStart = checkDatabaseOnStart;
356+
if (!checkDatabaseOnStart) {
357+
LOGGER.info("The 'DefaultLockRepository' won't be started automatically " +
358+
"and required table is not going be checked.");
359+
}
360+
}
361+
362+
@Override
363+
public boolean isAutoStartup() {
364+
return this.checkDatabaseOnStart;
365+
}
366+
367+
@Override
368+
public void start() {
369+
if (this.started.compareAndSet(false, true) && this.checkDatabaseOnStart) {
370+
this.template.queryForObject(this.countAllQuery, Integer.class); // If no table in DB, an exception is thrown
371+
}
372+
}
373+
374+
@Override
375+
public void stop() {
376+
this.started.set(false);
377+
}
378+
379+
@Override
380+
public boolean isRunning() {
381+
return this.started.get();
382+
}
383+
328384
@Override
329385
public void close() {
330386
this.defaultTransactionTemplate.executeWithoutResult(

spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/metadata/JdbcMetadataStore.java

+60-4
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,13 @@
1616

1717
package org.springframework.integration.jdbc.metadata;
1818

19+
import java.util.concurrent.atomic.AtomicBoolean;
20+
1921
import javax.sql.DataSource;
2022

2123
import org.springframework.beans.factory.InitializingBean;
24+
import org.springframework.context.SmartLifecycle;
25+
import org.springframework.core.log.LogAccessor;
2226
import org.springframework.dao.DuplicateKeyException;
2327
import org.springframework.dao.EmptyResultDataAccessException;
2428
import org.springframework.integration.metadata.ConcurrentMetadataStore;
@@ -34,14 +38,22 @@
3438
* where <code>*</code> is the target database type.
3539
* <p>
3640
* The transaction management is required to use this {@link ConcurrentMetadataStore}.
41+
* <p>
42+
* This class implements {@link SmartLifecycle} and calls
43+
* {@code SELECT COUNT(METADATA_KEY) FROM %sMETADATA_STORE} query
44+
* according to the provided prefix on {@link #start()} to check if required table is present in DB.
45+
* The application context will fail to start if the table is not present.
46+
* This check can be disabled via {@link #setCheckDatabaseOnStart(boolean)}.
3747
*
3848
* @author Bojan Vukasovic
3949
* @author Artem Bilan
4050
* @author Gary Russell
4151
*
4252
* @since 5.0
4353
*/
44-
public class JdbcMetadataStore implements ConcurrentMetadataStore, InitializingBean {
54+
public class JdbcMetadataStore implements ConcurrentMetadataStore, InitializingBean, SmartLifecycle {
55+
56+
private static final LogAccessor LOGGER = new LogAccessor(JdbcMetadataStore.class);
4557

4658
private static final String KEY_CANNOT_BE_NULL = "'key' cannot be null";
4759

@@ -52,6 +64,8 @@ public class JdbcMetadataStore implements ConcurrentMetadataStore, InitializingB
5264

5365
private final JdbcOperations jdbcTemplate;
5466

67+
private final AtomicBoolean started = new AtomicBoolean();
68+
5569
private String tablePrefix = DEFAULT_TABLE_PREFIX;
5670

5771
private String region = "DEFAULT";
@@ -93,6 +107,12 @@ public class JdbcMetadataStore implements ConcurrentMetadataStore, InitializingB
93107
HAVING COUNT(*)=0
94108
""";
95109

110+
private String countQuery = """
111+
SELECT COUNT(METADATA_KEY) FROM %sMETADATA_STORE
112+
""";
113+
114+
private boolean checkDatabaseOnStart = true;
115+
96116
/**
97117
* Instantiate a {@link JdbcMetadataStore} using provided dataSource {@link DataSource}.
98118
* @param dataSource a {@link DataSource}
@@ -137,7 +157,7 @@ public void setRegion(String region) {
137157
* Specify a row lock hint for the query in the lock-based operations.
138158
* Defaults to {@code FOR UPDATE}. Can be specified as an empty string,
139159
* if the target RDBMS doesn't support locking on tables from queries.
140-
* The value depends from RDBMS vendor, e.g. SQL Server requires {@code WITH (ROWLOCK)}.
160+
* The value depends on the RDBMS vendor, e.g. SQL Server requires {@code WITH (ROWLOCK)}.
141161
* @param lockHint the RDBMS vendor-specific lock hint.
142162
* @since 5.0.7
143163
*/
@@ -154,6 +174,42 @@ public void afterPropertiesSet() {
154174
this.replaceValueByKeyQuery = String.format(this.replaceValueByKeyQuery, this.tablePrefix);
155175
this.removeValueQuery = String.format(this.removeValueQuery, this.tablePrefix);
156176
this.putIfAbsentValueQuery = String.format(this.putIfAbsentValueQuery, this.tablePrefix, this.tablePrefix);
177+
this.countQuery = String.format(this.putIfAbsentValueQuery, this.tablePrefix);
178+
}
179+
180+
/**
181+
* The flag to perform a database check query on start or not.
182+
* @param checkDatabaseOnStart false to not perform the database check.
183+
* @since 6.2
184+
*/
185+
public void setCheckDatabaseOnStart(boolean checkDatabaseOnStart) {
186+
this.checkDatabaseOnStart = checkDatabaseOnStart;
187+
if (!checkDatabaseOnStart) {
188+
LOGGER.info("The 'DefaultLockRepository' won't be started automatically " +
189+
"and required table is not going be checked.");
190+
}
191+
}
192+
193+
@Override
194+
public boolean isAutoStartup() {
195+
return this.checkDatabaseOnStart;
196+
}
197+
198+
@Override
199+
public void start() {
200+
if (this.started.compareAndSet(false, true) && this.checkDatabaseOnStart) {
201+
this.jdbcTemplate.queryForObject(this.countQuery, Integer.class); // If no table in DB, an exception is thrown
202+
}
203+
}
204+
205+
@Override
206+
public void stop() {
207+
this.started.set(false);
208+
}
209+
210+
@Override
211+
public boolean isRunning() {
212+
return this.started.get();
157213
}
158214

159215
@Override
@@ -162,7 +218,7 @@ public String putIfAbsent(String key, String value) {
162218
Assert.notNull(key, KEY_CANNOT_BE_NULL);
163219
Assert.notNull(value, "'value' cannot be null");
164220
while (true) {
165-
//try to insert if does not exists
221+
//try to insert if the entry does not exist
166222
int affectedRows = tryToPutIfAbsent(key, value);
167223
if (affectedRows > 0) {
168224
//it was not in the table, so we have just inserted
@@ -218,7 +274,7 @@ public void put(String key, String value) {
218274
Assert.notNull(key, KEY_CANNOT_BE_NULL);
219275
Assert.notNull(value, "'value' cannot be null");
220276
while (true) {
221-
//try to insert if does not exist, if exists we will try to update it
277+
//try to insert if the entry does not exist, if it exists we will try to update it
222278
int affectedRows = tryToPutIfAbsent(key, value);
223279
if (affectedRows == 0) {
224280
//since value is not inserted, means it is already present

spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/store/JdbcChannelMessageStore.java

+48-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -23,6 +23,7 @@
2323
import java.util.Set;
2424
import java.util.UUID;
2525
import java.util.concurrent.ConcurrentHashMap;
26+
import java.util.concurrent.atomic.AtomicBoolean;
2627
import java.util.concurrent.locks.Lock;
2728
import java.util.concurrent.locks.ReadWriteLock;
2829
import java.util.concurrent.locks.ReentrantReadWriteLock;
@@ -31,6 +32,7 @@
3132
import javax.sql.DataSource;
3233

3334
import org.springframework.beans.factory.InitializingBean;
35+
import org.springframework.context.SmartLifecycle;
3436
import org.springframework.core.log.LogAccessor;
3537
import org.springframework.core.log.LogMessage;
3638
import org.springframework.core.serializer.Deserializer;
@@ -73,6 +75,11 @@
7375
* The SQL scripts for creating the table are packaged
7476
* under {@code org/springframework/integration/jdbc/schema-*.sql},
7577
* where {@code *} denotes the target database type.
78+
* <p>
79+
* This class implements {@link SmartLifecycle} and calls {@link #getMessageGroupCount()}
80+
* on {@link #start()} to check if required table is present in DB.
81+
* The application context will fail to start if the table is not present.
82+
* This check can be disabled via {@link #setCheckDatabaseOnStart(boolean)}.
7683
*
7784
* @author Gunnar Hillert
7885
* @author Artem Bilan
@@ -83,7 +90,7 @@
8390
* @since 2.2
8491
*/
8592
@ManagedResource
86-
public class JdbcChannelMessageStore implements PriorityCapableChannelMessageStore, InitializingBean {
93+
public class JdbcChannelMessageStore implements PriorityCapableChannelMessageStore, InitializingBean, SmartLifecycle {
8794

8895
private static final LogAccessor LOGGER = new LogAccessor(JdbcChannelMessageStore.class);
8996

@@ -121,6 +128,8 @@ private enum Query {
121128

122129
private final Lock idCacheWriteLock = this.idCacheLock.writeLock();
123130

131+
private final AtomicBoolean started = new AtomicBoolean();
132+
124133
private ChannelMessageStoreQueryProvider channelMessageStoreQueryProvider;
125134

126135
private String region = DEFAULT_REGION;
@@ -145,6 +154,8 @@ private enum Query {
145154

146155
private boolean priorityEnabled;
147156

157+
private boolean checkDatabaseOnStart = true;
158+
148159
/**
149160
* Convenient constructor for configuration use.
150161
*/
@@ -411,6 +422,41 @@ public void afterPropertiesSet() {
411422
this.jdbcTemplate.afterPropertiesSet();
412423
}
413424

425+
/**
426+
* The flag to perform a database check query on start or not.
427+
* @param checkDatabaseOnStart false to not perform the database check.
428+
* @since 6.2
429+
*/
430+
public void setCheckDatabaseOnStart(boolean checkDatabaseOnStart) {
431+
this.checkDatabaseOnStart = checkDatabaseOnStart;
432+
if (!checkDatabaseOnStart) {
433+
LOGGER.info("The 'DefaultLockRepository' won't be started automatically " +
434+
"and required table is not going be checked.");
435+
}
436+
}
437+
438+
@Override
439+
public boolean isAutoStartup() {
440+
return this.checkDatabaseOnStart;
441+
}
442+
443+
@Override
444+
public void start() {
445+
if (this.started.compareAndSet(false, true) && this.checkDatabaseOnStart) {
446+
getMessageGroupCount(); // If no table in DB, an exception is thrown
447+
}
448+
}
449+
450+
@Override
451+
public void stop() {
452+
this.started.set(false);
453+
}
454+
455+
@Override
456+
public boolean isRunning() {
457+
return this.started.get();
458+
}
459+
414460
/**
415461
* Store a message in the database. The groupId identifies the channel for which
416462
* the message is to be stored.

0 commit comments

Comments
 (0)