Skip to content

Commit 87a2ac5

Browse files
joshisteartembilan
authored andcommitted
GH-8760 Postgres: using DELETE ... RETURNING
Fixes #8760 * Make `PostgresChannelMessageStoreQueryProvider` to use single `DELETE ... RETURNING` for polling statements * Add `isUsingSingleStatementForPoll` and use it from `JdbcChannelMessageStore` * Execute Postgres init scripts to `PostgresContainerTest` * Code clean up * Document the new feature
1 parent 29186e2 commit 87a2ac5

File tree

9 files changed

+156
-43
lines changed

9 files changed

+156
-43
lines changed

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

+7-2
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@
6565
* Channel-specific implementation of
6666
* {@link org.springframework.integration.store.BasicMessageGroupStore} using a relational
6767
* database via JDBC.
68-
*
68+
* <p>
6969
* This message store shall be used for message channels only.
7070
* <p>
7171
* As such, the {@link JdbcChannelMessageStore} uses database specific SQL queries.
@@ -86,6 +86,7 @@
8686
* @author Gary Russell
8787
* @author Meherzad Lahewala
8888
* @author Trung Pham
89+
* @author Johannes Edmeier
8990
*
9091
* @since 2.2
9192
*/
@@ -554,12 +555,16 @@ public void removeMessageGroup(Object groupId) {
554555
public Message<?> pollMessageFromGroup(Object groupId) {
555556
String key = getKey(groupId);
556557
Message<?> polledMessage = doPollForMessage(key);
557-
if (polledMessage != null && !doRemoveMessageFromGroup(groupId, polledMessage)) {
558+
if (polledMessage != null && !isSingleStatementForPoll() && !doRemoveMessageFromGroup(groupId, polledMessage)) {
558559
return null;
559560
}
560561
return polledMessage;
561562
}
562563

564+
private boolean isSingleStatementForPoll() {
565+
return this.channelMessageStoreQueryProvider.isSingleStatementForPoll();
566+
}
567+
563568
/**
564569
* This method executes a call to the DB to get the oldest Message in the
565570
* MessageGroup which in the context of the {@link JdbcChannelMessageStore}

Diff for: spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/store/channel/ChannelMessageStoreQueryProvider.java

+12-1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
* @author Artem Bilan
2626
* @author Gary Russell
2727
* @author Adama Sorho
28+
* @author Johannes Edmeier
2829
*
2930
* @since 2.2
3031
*/
@@ -33,7 +34,7 @@ public interface ChannelMessageStoreQueryProvider {
3334
String SELECT_COMMON = """
3435
SELECT %PREFIX%CHANNEL_MESSAGE.MESSAGE_ID, %PREFIX%CHANNEL_MESSAGE.MESSAGE_BYTES
3536
from %PREFIX%CHANNEL_MESSAGE
36-
where %PREFIX%CHANNEL_MESSAGE.GROUP_KEY = :group_key and %PREFIX%CHANNEL_MESSAGE.REGION = :region\s
37+
where %PREFIX%CHANNEL_MESSAGE.GROUP_KEY = :group_key and %PREFIX%CHANNEL_MESSAGE.REGION = :region
3738
""";
3839

3940
/**
@@ -125,4 +126,14 @@ default String getDeleteMessageGroupQuery() {
125126
*/
126127
String getPriorityPollFromGroupQuery();
127128

129+
/**
130+
* Indicate if the queries for polling are using a single statement (e.g. DELETE ... RETURNING) to
131+
* retrieve and delete the message from the channel store.
132+
* @return true if a single statement is used, false if a select and delete is required.
133+
* @since 6.2
134+
*/
135+
default boolean isSingleStatementForPoll() {
136+
return false;
137+
}
138+
128139
}

Diff for: spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/store/channel/PostgresChannelMessageStoreQueryProvider.java

+52-12
Original file line numberDiff line numberDiff line change
@@ -20,37 +20,77 @@
2020
* @author Gunnar Hillert
2121
* @author Artem Bilan
2222
* @author Adama Sorho
23+
* @author Johannes Edmeier
2324
*
2425
* @since 2.2
2526
*/
2627
public class PostgresChannelMessageStoreQueryProvider implements ChannelMessageStoreQueryProvider {
2728

2829
@Override
2930
public String getPollFromGroupExcludeIdsQuery() {
30-
return SELECT_COMMON
31-
+ "and %PREFIX%CHANNEL_MESSAGE.MESSAGE_ID not in (:message_ids) "
32-
+ "order by CREATED_DATE, MESSAGE_SEQUENCE LIMIT 1 FOR UPDATE SKIP LOCKED";
31+
return """
32+
delete
33+
from %PREFIX%CHANNEL_MESSAGE
34+
where CTID = (select CTID
35+
from %PREFIX%CHANNEL_MESSAGE
36+
where %PREFIX%CHANNEL_MESSAGE.GROUP_KEY = :group_key
37+
and %PREFIX%CHANNEL_MESSAGE.REGION = :region
38+
and %PREFIX%CHANNEL_MESSAGE.MESSAGE_ID not in (:message_ids)
39+
order by CREATED_DATE, MESSAGE_SEQUENCE
40+
limit 1 for update skip locked)
41+
returning MESSAGE_ID, MESSAGE_BYTES;
42+
""";
3343
}
3444

3545
@Override
3646
public String getPollFromGroupQuery() {
37-
return SELECT_COMMON +
38-
"order by CREATED_DATE, MESSAGE_SEQUENCE LIMIT 1 FOR UPDATE SKIP LOCKED";
47+
return """
48+
delete
49+
from %PREFIX%CHANNEL_MESSAGE
50+
where CTID = (select CTID
51+
from %PREFIX%CHANNEL_MESSAGE
52+
where %PREFIX%CHANNEL_MESSAGE.GROUP_KEY = :group_key
53+
and %PREFIX%CHANNEL_MESSAGE.REGION = :region
54+
order by CREATED_DATE, MESSAGE_SEQUENCE
55+
limit 1 for update skip locked)
56+
returning MESSAGE_ID, MESSAGE_BYTES;
57+
""";
3958
}
4059

4160
@Override
4261
public String getPriorityPollFromGroupExcludeIdsQuery() {
43-
return SELECT_COMMON +
44-
"and %PREFIX%CHANNEL_MESSAGE.MESSAGE_ID not in (:message_ids) " +
45-
"order by MESSAGE_PRIORITY DESC NULLS LAST, CREATED_DATE, MESSAGE_SEQUENCE " +
46-
"LIMIT 1 FOR UPDATE SKIP LOCKED";
62+
return """
63+
delete
64+
from %PREFIX%CHANNEL_MESSAGE
65+
where CTID = (select CTID
66+
from %PREFIX%CHANNEL_MESSAGE
67+
where %PREFIX%CHANNEL_MESSAGE.GROUP_KEY = :group_key
68+
and %PREFIX%CHANNEL_MESSAGE.REGION = :region
69+
and %PREFIX%CHANNEL_MESSAGE.MESSAGE_ID not in (:message_ids)
70+
order by MESSAGE_PRIORITY DESC NULLS LAST, CREATED_DATE, MESSAGE_SEQUENCE
71+
limit 1 for update skip locked)
72+
returning MESSAGE_ID, MESSAGE_BYTES;
73+
""";
4774
}
4875

4976
@Override
5077
public String getPriorityPollFromGroupQuery() {
51-
return SELECT_COMMON +
52-
"order by MESSAGE_PRIORITY DESC NULLS LAST, CREATED_DATE, MESSAGE_SEQUENCE " +
53-
"LIMIT 1 FOR UPDATE SKIP LOCKED";
78+
return """
79+
delete
80+
from %PREFIX%CHANNEL_MESSAGE
81+
where CTID = (select CTID
82+
from %PREFIX%CHANNEL_MESSAGE
83+
where %PREFIX%CHANNEL_MESSAGE.GROUP_KEY = :group_key
84+
and %PREFIX%CHANNEL_MESSAGE.REGION = :region
85+
order by MESSAGE_PRIORITY DESC NULLS LAST, CREATED_DATE, MESSAGE_SEQUENCE
86+
limit 1 for update skip locked)
87+
returning MESSAGE_ID, MESSAGE_BYTES;
88+
""";
89+
}
90+
91+
@Override
92+
public boolean isSingleStatementForPoll() {
93+
return true;
5494
}
5595

5696
}
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
-- Autogenerated: do not edit this file
22

3-
DROP INDEX INT_MESSAGE_IX1 ;
4-
DROP INDEX INT_CHANNEL_MSG_DATE_IDX ;
5-
DROP INDEX INT_CHANNEL_MSG_PRIORITY_IDX ;
6-
DROP TABLE INT_MESSAGE ;
7-
DROP TABLE INT_MESSAGE_GROUP ;
8-
DROP TABLE INT_GROUP_TO_MESSAGE ;
9-
DROP TABLE INT_LOCK ;
10-
DROP TABLE INT_CHANNEL_MESSAGE ;
11-
DROP TABLE INT_METADATA_STORE ;
12-
DROP SEQUENCE INT_MESSAGE_SEQ ;
3+
DROP INDEX IF EXISTS INT_MESSAGE_IX1 ;
4+
DROP INDEX IF EXISTS INT_CHANNEL_MSG_DATE_IDX ;
5+
DROP INDEX IF EXISTS INT_CHANNEL_MSG_PRIORITY_IDX ;
6+
DROP TABLE IF EXISTS INT_MESSAGE ;
7+
DROP TABLE IF EXISTS INT_MESSAGE_GROUP ;
8+
DROP TABLE IF EXISTS INT_GROUP_TO_MESSAGE ;
9+
DROP TABLE IF EXISTS INT_LOCK ;
10+
DROP TABLE IF EXISTS INT_CHANNEL_MESSAGE ;
11+
DROP TABLE IF EXISTS INT_METADATA_STORE ;
12+
DROP SEQUENCE IF EXISTS INT_MESSAGE_SEQ ;

Diff for: spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/channel/PostgresChannelMessageTableSubscriberTests.java

-15
Original file line numberDiff line numberDiff line change
@@ -67,21 +67,6 @@
6767
public class PostgresChannelMessageTableSubscriberTests implements PostgresContainerTest {
6868

6969
private static final String INTEGRATION_DB_SCRIPTS = """
70-
CREATE SEQUENCE INT_MESSAGE_SEQ START WITH 1 INCREMENT BY 1 NO CYCLE;
71-
^^^ END OF SCRIPT ^^^
72-
73-
CREATE TABLE INT_CHANNEL_MESSAGE (
74-
MESSAGE_ID CHAR(36) NOT NULL,
75-
GROUP_KEY CHAR(36) NOT NULL,
76-
CREATED_DATE BIGINT NOT NULL,
77-
MESSAGE_PRIORITY BIGINT,
78-
MESSAGE_SEQUENCE BIGINT NOT NULL DEFAULT nextval('INT_MESSAGE_SEQ'),
79-
MESSAGE_BYTES BYTEA,
80-
REGION VARCHAR(100) NOT NULL,
81-
constraint INT_CHANNEL_MESSAGE_PK primary key (REGION, GROUP_KEY, CREATED_DATE, MESSAGE_SEQUENCE)
82-
);
83-
^^^ END OF SCRIPT ^^^
84-
8570
CREATE FUNCTION INT_CHANNEL_MESSAGE_NOTIFY_FCT()
8671
RETURNS TRIGGER AS
8772
$BODY$

Diff for: spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/channel/PostgresContainerTest.java

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2022 the original author or authors.
2+
* Copyright 2022-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.
@@ -27,14 +27,17 @@
2727
* Since the Postgres container instance is shared via static property, it is going to be
2828
* started only once per JVM, therefore the target Docker container is reused automatically.
2929
*
30+
* @author Artem Bilan
3031
* @author Rafael Winterhalter
32+
* @author Johannes Edmeier
3133
*
3234
* @since 6.0
3335
*/
3436
@Testcontainers(disabledWithoutDocker = true)
3537
public interface PostgresContainerTest {
3638

37-
PostgreSQLContainer<?> POSTGRES_CONTAINER = new PostgreSQLContainer<>("postgres:11");
39+
PostgreSQLContainer<?> POSTGRES_CONTAINER = new PostgreSQLContainer<>("postgres:11")
40+
.withInitScript("org/springframework/integration/jdbc/schema-postgresql.sql");
3841

3942
@BeforeAll
4043
static void startContainer() {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright 2023 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.integration.jdbc.store.channel;
18+
19+
import javax.sql.DataSource;
20+
21+
import org.apache.commons.dbcp2.BasicDataSource;
22+
23+
import org.springframework.context.annotation.Bean;
24+
import org.springframework.context.annotation.Configuration;
25+
import org.springframework.integration.jdbc.channel.PostgresContainerTest;
26+
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
27+
import org.springframework.test.context.ContextConfiguration;
28+
import org.springframework.transaction.PlatformTransactionManager;
29+
30+
/**
31+
* @author Johannes Edmeier
32+
* @author Artem Bilan
33+
*
34+
* @since 6.2
35+
*/
36+
@ContextConfiguration
37+
public class PostgresJdbcChannelMessageStoreTests extends AbstractJdbcChannelMessageStoreTests
38+
implements PostgresContainerTest {
39+
40+
@Configuration
41+
public static class Config {
42+
43+
@Bean
44+
public DataSource dataSource() {
45+
BasicDataSource dataSource = new BasicDataSource();
46+
dataSource.setUrl(PostgresContainerTest.getJdbcUrl());
47+
dataSource.setUsername(PostgresContainerTest.getUsername());
48+
dataSource.setPassword(PostgresContainerTest.getPassword());
49+
return dataSource;
50+
}
51+
52+
@Bean
53+
PlatformTransactionManager transactionManager(DataSource dataSource) {
54+
return new DataSourceTransactionManager(dataSource);
55+
}
56+
57+
@Bean
58+
PostgresChannelMessageStoreQueryProvider queryProvider() {
59+
return new PostgresChannelMessageStoreQueryProvider();
60+
}
61+
62+
}
63+
64+
}

Diff for: src/reference/antora/modules/ROOT/pages/jdbc/message-store.adoc

+3
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ If your database is not listed, you can implement the `ChannelMessageStoreQueryP
8181

8282
Version 4.0 added the `MESSAGE_SEQUENCE` column to the table to ensure first-in-first-out (FIFO) queueing even when messages are stored in the same millisecond.
8383

84+
Starting with version 6.2, `ChannelMessageStoreQueryProvider` exposes a `isSingleStatementForPoll` flag, where the `PostgresChannelMessageStoreQueryProvider` returns `true` and its queries for polls are now based on a single `DELETE...RETURNING` statement.
85+
The `JdbcChannelMessageStore` consults with the `isSingleStatementForPoll` option and skips a separate `DELETE` statement if only single poll statement is supported.
86+
8487
[[custom-message-insertion]]
8588
=== Custom Message Insertion
8689

Diff for: src/reference/antora/modules/ROOT/pages/whats-new.adoc

+3-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,9 @@ See xref:kafka.adoc#kafka-inbound-pollable[Kafka Inbound Channel Adapter] for mo
6262
=== JDBC Support Changes
6363

6464
The `JdbcMessageStore`, `JdbcChannelMessageStore`, `JdbcMetadataStore`, and `DefaultLockRepository` implement `SmartLifecycle` and perform a `SELECT COUNT` query, on their respective tables, in the `start()` method to ensure that the required table (according to the provided prefix) is present in the target database.
65-
See xref:jdbc/message-store.adoc#jdbc-db-init[Initializing the Database] for more information.
65+
The `PostgresChannelMessageStoreQueryProvider` now provides single `DELETE...RETURNING` statement for polling queries.
66+
For this purpose the `ChannelMessageStoreQueryProvider` exposes `isSingleStatementForPoll` option which is consulted from the `JdbcChannelMessageStore`.
67+
See xref:jdbc/message-store.adoc[JDBC Message Store] for more information.
6668

6769
[[x6.2-mongodb]]
6870
=== MongoDB Support Changes

0 commit comments

Comments
 (0)