Skip to content

Commit e012fcb

Browse files
committed
optimizing rds module for lib + addressing pr comments
1 parent 50ecd8d commit e012fcb

14 files changed

Lines changed: 134 additions & 141 deletions

idempotent-rds/README.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,19 +43,19 @@ See main [README](../README.md) for general idempotent configuration.
4343

4444
* Table Name
4545

46-
Property: `idempotent.rds.table.name`
46+
Property: `idempotent.rds.table-name`
4747
Default Value: `idempotent`
4848
Description: The name of the database table to use.
4949

5050
* Cleanup Schedule
5151

52-
Property: `idempotent.rds.cleanup.fixedDelay`
52+
Property: `idempotent.rds.cleanup.fixed-delay`
5353
Default Value: `60000` (1 minute)
5454
Description: Fixed delay in milliseconds for the cleanup task that removes expired keys.
5555

5656
* Cleanup Batch Size
5757

58-
Property: `idempotent.rds.cleanup.batch.size`
58+
Property: `idempotent.rds.cleanup.batch-size`
5959
Default Value: `1000`
6060
Description: Number of expired keys to delete in each batch to prevent long-running database locks.
6161

@@ -70,7 +70,10 @@ Unlike Redis or DynamoDb, RDS does not support native TTL for records. This modu
7070

7171
## Dependencies
7272

73-
This module relies on `spring-boot-starter-jdbc`. You must configure your `DataSource` as per standard Spring Boot configuration (e.g., `spring.datasource.url`, etc.).
73+
This module provides the core JDBC logic but does **not** bundle a connection pool or database drivers. You must:
74+
1. Add a JDBC driver for your database (e.g., `mysql-connector-j` or `postgresql`).
75+
2. Provide a `DataSource` bean (typically by adding `spring-boot-starter-jdbc` to your application).
76+
3. Configure your `DataSource` as per standard Spring Boot configuration (e.g., `spring.datasource.url`).
7477

7578
## Performance Tuning
7679

idempotent-rds/pom.xml

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,18 @@
1818
<groupId>io.github.arun0009</groupId>
1919
<artifactId>idempotent-core</artifactId>
2020
</dependency>
21+
<dependency>
22+
<groupId>org.springframework</groupId>
23+
<artifactId>spring-jdbc</artifactId>
24+
</dependency>
25+
<dependency>
26+
<groupId>org.springframework</groupId>
27+
<artifactId>spring-tx</artifactId>
28+
</dependency>
2129
<dependency>
2230
<groupId>org.springframework.boot</groupId>
23-
<artifactId>spring-boot-starter-jdbc</artifactId>
31+
<artifactId>spring-boot-autoconfigure</artifactId>
32+
<scope>provided</scope>
2433
</dependency>
2534
<dependency>
2635
<groupId>org.springframework.boot</groupId>
@@ -34,11 +43,15 @@
3443
<artifactId>spring-boot-starter-test</artifactId>
3544
<scope>test</scope>
3645
</dependency>
46+
<dependency>
47+
<groupId>com.zaxxer</groupId>
48+
<artifactId>HikariCP</artifactId>
49+
<scope>test</scope>
50+
</dependency>
3751
<!-- TestContainers for real database testing -->
3852
<dependency>
3953
<groupId>org.testcontainers</groupId>
4054
<artifactId>junit-jupiter</artifactId>
41-
<version>${testcontainers.version}</version>
4255
<scope>test</scope>
4356
</dependency>
4457
<dependency>

idempotent-rds/src/main/java/io/github/arun0009/idempotent/rds/RdsConfig.java renamed to idempotent-rds/src/main/java/io/github/arun0009/idempotent/rds/RdsAutoConfiguration.java

Lines changed: 39 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,17 @@
44
import io.github.arun0009.idempotent.core.persistence.IdempotentStore;
55
import org.slf4j.Logger;
66
import org.slf4j.LoggerFactory;
7-
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
7+
import org.springframework.boot.autoconfigure.AutoConfiguration;
88
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
9+
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
910
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
1011
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
1112
import org.springframework.boot.context.properties.EnableConfigurationProperties;
1213
import org.springframework.context.annotation.Bean;
13-
import org.springframework.context.annotation.Configuration;
1414
import org.springframework.jdbc.core.JdbcTemplate;
1515
import org.springframework.scheduling.TaskScheduler;
1616
import org.springframework.scheduling.concurrent.SimpleAsyncTaskScheduler;
17+
import org.springframework.util.Assert;
1718
import tools.jackson.databind.DefaultTyping;
1819
import tools.jackson.databind.json.JsonMapper;
1920
import tools.jackson.databind.jsontype.BasicPolymorphicTypeValidator;
@@ -22,63 +23,71 @@
2223
import javax.sql.DataSource;
2324
import java.time.Duration;
2425

25-
@Configuration
26-
@ConditionalOnBean(DataSource.class)
27-
@EnableConfigurationProperties(RdsIdempotentProperties.class)
28-
@AutoConfigureAfter(
29-
name = {
26+
@AutoConfiguration(
27+
afterName = {
3028
"org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration",
3129
"org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration"
3230
})
33-
public class RdsConfig {
31+
@ConditionalOnClass({DataSource.class, JdbcTemplate.class})
32+
@ConditionalOnBean(DataSource.class)
33+
@EnableConfigurationProperties(RdsIdempotentProperties.class)
34+
public class RdsAutoConfiguration {
3435

35-
private static final Logger log = LoggerFactory.getLogger(RdsConfig.class);
36+
private static final Logger log = LoggerFactory.getLogger(RdsAutoConfiguration.class);
3637

3738
@Bean
3839
@ConditionalOnMissingBean
39-
public JsonMapper rdsJsonMapper() {
40-
log.warn("Using an unrestricted polymorphic type validator for RDS idempotent store. "
41-
+ "Without restrictions of the PolymorphicTypeValidator, deserialization is "
42-
+ "vulnerable to arbitrary code execution when reading from untrusted sources.");
43-
BasicPolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator.builder()
44-
.allowIfBaseType(Object.class)
45-
.allowIfSubType((ctx, clazz) -> true)
46-
.build();
47-
return JsonMapper.builder()
48-
.polymorphicTypeValidator(ptv)
49-
.setDefaultTyping(new DefaultTypeResolverBuilder(
50-
ptv, DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY, JsonTypeInfo.Id.CLASS, "@class"))
51-
.build();
40+
public IdempotentStore idempotentStore(
41+
JdbcTemplate jdbcTemplate, RdsIdempotentProperties properties, RdsJacksonJsonBuilderCustomizer customizer) {
42+
Assert.hasText(properties.tableName(), "idempotent.rds.table-name must not be blank");
43+
44+
var jsonBuilder = JsonMapper.builder();
45+
customizer.customize(jsonBuilder);
46+
47+
return new RdsIdempotentStore(jdbcTemplate, properties.tableName(), jsonBuilder.build());
5248
}
5349

5450
@Bean
5551
@ConditionalOnMissingBean
56-
public IdempotentStore idempotentStore(
57-
JdbcTemplate jdbcTemplate, RdsIdempotentProperties properties, JsonMapper rdsJsonMapper) {
58-
return new RdsIdempotentStore(jdbcTemplate, properties.getTableName(), rdsJsonMapper);
52+
public RdsJacksonJsonBuilderCustomizer rdsJacksonJsonBuilderCustomizer() {
53+
return builder -> {
54+
log.warn("Using an unrestricted polymorphic type validator for RDS idempotent store. "
55+
+ "Without restrictions of the PolymorphicTypeValidator, deserialization is "
56+
+ "vulnerable to arbitrary code execution when reading from untrusted sources.");
57+
var ptv = BasicPolymorphicTypeValidator.builder()
58+
.allowIfBaseType(Object.class)
59+
.allowIfSubType((ctx, clazz) -> true)
60+
.build();
61+
builder.polymorphicTypeValidator(ptv)
62+
.setDefaultTyping(new DefaultTypeResolverBuilder(
63+
ptv, DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY, JsonTypeInfo.Id.CLASS, "@class"));
64+
};
5965
}
6066

6167
@Bean
6268
@ConditionalOnMissingBean
6369
@ConditionalOnProperty(prefix = "idempotent.rds.cleanup", name = "enabled", matchIfMissing = true)
6470
public RdsCleanupTask rdsCleanupTask(
6571
JdbcTemplate jdbcTemplate, RdsIdempotentProperties properties, TaskScheduler rdsCleanupTaskScheduler) {
72+
Assert.isTrue(properties.cleanup().batchSize() > 0, "idempotent.rds.cleanup.batch-size must be positive");
73+
Assert.isTrue(properties.cleanup().fixedDelay() > 0, "idempotent.rds.cleanup.fixed-delay must be positive");
74+
6675
RdsDialect dialect = RdsDialect.detect(jdbcTemplate);
67-
RdsCleanupTask cleanupTask = new RdsCleanupTask(
76+
var cleanupTask = new RdsCleanupTask(
6877
jdbcTemplate,
69-
properties.getTableName(),
78+
properties.tableName(),
7079
dialect,
71-
properties.getCleanup().getBatchSize());
80+
properties.cleanup().batchSize());
7281
rdsCleanupTaskScheduler.scheduleWithFixedDelay(
73-
cleanupTask::cleanup, Duration.ofMillis(properties.getCleanup().getFixedDelay()));
82+
cleanupTask::cleanup, Duration.ofMillis(properties.cleanup().fixedDelay()));
7483
return cleanupTask;
7584
}
7685

7786
@Bean
7887
@ConditionalOnMissingBean(name = "rdsCleanupTaskScheduler")
7988
@ConditionalOnProperty(prefix = "idempotent.rds.cleanup", name = "enabled", matchIfMissing = true)
8089
public TaskScheduler rdsCleanupTaskScheduler() {
81-
SimpleAsyncTaskScheduler scheduler = new SimpleAsyncTaskScheduler();
90+
var scheduler = new SimpleAsyncTaskScheduler();
8291
scheduler.setThreadNamePrefix("idempotent-rds-cleanup-");
8392
return scheduler;
8493
}

idempotent-rds/src/main/java/io/github/arun0009/idempotent/rds/RdsCleanupTask.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ public RdsCleanupTask(JdbcTemplate jdbcTemplate, String tableName, RdsDialect di
2020
}
2121

2222
public void cleanup() {
23-
long now = System.currentTimeMillis();
24-
int totalDeleted = 0;
23+
var now = System.currentTimeMillis();
24+
var totalDeleted = 0;
2525
int batchDeleted;
2626

2727
do {
Lines changed: 11 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,20 @@
11
package io.github.arun0009.idempotent.rds;
22

33
import org.springframework.boot.context.properties.ConfigurationProperties;
4+
import org.springframework.boot.context.properties.bind.DefaultValue;
45

56
@ConfigurationProperties(prefix = "idempotent.rds")
6-
public class RdsIdempotentProperties {
7+
public record RdsIdempotentProperties(
8+
/** Name of the database table for idempotent keys. */
9+
@DefaultValue("idempotent") String tableName, Cleanup cleanup) {
710

8-
/** Name of the database table for idempotent keys. */
9-
private String tableName = "idempotent";
11+
public record Cleanup(
12+
/** Whether the cleanup task is enabled. Set to false for CDS or AOT cache runs. */
13+
@DefaultValue("true") boolean enabled,
1014

11-
private final Cleanup cleanup = new Cleanup();
15+
/** Number of expired records to delete per batch. */
16+
@DefaultValue("1000") int batchSize,
1217

13-
public String getTableName() {
14-
return tableName;
15-
}
16-
17-
public void setTableName(String tableName) {
18-
this.tableName = tableName;
19-
}
20-
21-
public Cleanup getCleanup() {
22-
return cleanup;
23-
}
24-
25-
public static class Cleanup {
26-
27-
/**
28-
* Whether the cleanup task is enabled. Set to false for CDS or AOT cache runs.
29-
*/
30-
private boolean enabled = true;
31-
32-
/** Number of expired records to delete per batch. */
33-
private int batchSize = 1000;
34-
35-
/** Fixed delay in milliseconds between cleanup runs. */
36-
private long fixedDelay = 60000;
37-
38-
public boolean isEnabled() {
39-
return enabled;
40-
}
41-
42-
public void setEnabled(boolean enabled) {
43-
this.enabled = enabled;
44-
}
45-
46-
public int getBatchSize() {
47-
return batchSize;
48-
}
49-
50-
public void setBatchSize(int batchSize) {
51-
this.batchSize = batchSize;
52-
}
53-
54-
public long getFixedDelay() {
55-
return fixedDelay;
56-
}
57-
58-
public void setFixedDelay(long fixedDelay) {
59-
this.fixedDelay = fixedDelay;
60-
}
61-
}
18+
/** Fixed delay in milliseconds between cleanup runs. */
19+
@DefaultValue("60000") long fixedDelay) {}
6220
}

idempotent-rds/src/main/java/io/github/arun0009/idempotent/rds/RdsIdempotentStore.java

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public RdsIdempotentStore(JdbcTemplate jdbcTemplate, String tableName, JsonMappe
3131

3232
@Override
3333
public Value getValue(IdempotentKey key, Class<?> returnType) {
34-
String sql = """
34+
var sql = """
3535
SELECT status, expiration_time_millis, response FROM %s WHERE key_id = ? AND process_name = ?
3636
""".formatted(tableName);
3737
try {
@@ -67,7 +67,7 @@ public Value mapRow(ResultSet rs, int rowNum) throws SQLException {
6767
@Override
6868
public void store(IdempotentKey key, Value value) {
6969
try {
70-
String responseJson = value.response() != null ? jsonMapper.writeValueAsString(value.response()) : null;
70+
var responseJson = value.response() != null ? jsonMapper.writeValueAsString(value.response()) : null;
7171

7272
switch (dialect) {
7373
case POSTGRES -> storePostgres(key, value, responseJson);
@@ -81,7 +81,7 @@ public void store(IdempotentKey key, Value value) {
8181
}
8282

8383
private void storePostgres(IdempotentKey key, Value value, String responseJson) {
84-
String sql = """
84+
var sql = """
8585
INSERT INTO %s (key_id, process_name, status, expiration_time_millis, response)
8686
VALUES (?, ?, ?, ?, ?)
8787
""".formatted(tableName);
@@ -90,15 +90,15 @@ private void storePostgres(IdempotentKey key, Value value, String responseJson)
9090
}
9191

9292
private void storeMySQL(IdempotentKey key, Value value, String responseJson) {
93-
String sql = """
93+
var sql = """
9494
INSERT INTO %s (key_id, process_name, status, expiration_time_millis, response) VALUES (?, ?, ?, ?, ?)
9595
""".formatted(tableName);
9696
jdbcTemplate.update(
9797
sql, key.key(), key.processName(), value.status(), value.expirationTimeInMilliSeconds(), responseJson);
9898
}
9999

100100
private void storeGeneric(IdempotentKey key, Value value, String response) {
101-
String sql = """
101+
var sql = """
102102
INSERT INTO %s (key_id, process_name, status, expiration_time_millis, response) VALUES (?, ?, ?, ?, ?)
103103
""".formatted(tableName);
104104
jdbcTemplate.update(
@@ -107,7 +107,7 @@ private void storeGeneric(IdempotentKey key, Value value, String response) {
107107

108108
@Override
109109
public void remove(IdempotentKey key) {
110-
String sql = """
110+
var sql = """
111111
DELETE FROM %s WHERE key_id = ? AND process_name = ?
112112
""".formatted(tableName);
113113
jdbcTemplate.update(sql, key.key(), key.processName());
@@ -116,9 +116,9 @@ public void remove(IdempotentKey key) {
116116
@Override
117117
public void update(IdempotentKey key, Value value) {
118118
try {
119-
String responseJson = value.response() != null ? jsonMapper.writeValueAsString(value.response()) : null;
119+
var responseJson = value.response() != null ? jsonMapper.writeValueAsString(value.response()) : null;
120120
// Simple UPDATE - trust the caller's contract that this is for INPROGRESS keys
121-
String updateSql = """
121+
var updateSql = """
122122
UPDATE %s SET status = ?, expiration_time_millis = ?, response = ?
123123
WHERE key_id = ? AND process_name = ?
124124
""".formatted(tableName);
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package io.github.arun0009.idempotent.rds;
2+
3+
import tools.jackson.databind.json.JsonMapper;
4+
5+
/**
6+
* Customizer for the JsonMapper.Builder used in the RDS idempotent store.
7+
*/
8+
@FunctionalInterface
9+
public interface RdsJacksonJsonBuilderCustomizer {
10+
11+
/**
12+
* Customizes the provided {@link JsonMapper.Builder} instance.
13+
*
14+
* @param builder the {@link JsonMapper.Builder} instance to customize
15+
*/
16+
void customize(JsonMapper.Builder builder);
17+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
io.github.arun0009.idempotent.rds.RdsConfig
1+
io.github.arun0009.idempotent.rds.RdsAutoConfiguration

idempotent-rds/src/test/java/io/github/arun0009/idempotent/rds/RdsIdempotentStoreH2Test.java

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
package io.github.arun0009.idempotent.rds;
22

3+
import com.zaxxer.hikari.HikariDataSource;
34
import io.github.arun0009.idempotent.core.persistence.IdempotentStore;
45
import io.github.arun0009.idempotent.core.persistence.IdempotentStore.IdempotentKey;
56
import io.github.arun0009.idempotent.core.persistence.IdempotentStore.Value;
67
import org.junit.jupiter.api.BeforeEach;
78
import org.junit.jupiter.api.Test;
89
import org.junit.jupiter.api.extension.ExtendWith;
910
import org.springframework.beans.factory.annotation.Autowired;
10-
import org.springframework.boot.jdbc.DataSourceBuilder;
1111
import org.springframework.context.annotation.Bean;
1212
import org.springframework.context.annotation.Configuration;
1313
import org.springframework.dao.DuplicateKeyException;
@@ -46,12 +46,12 @@ public class RdsIdempotentStoreH2Test {
4646
static class H2TestConfig {
4747
@Bean
4848
public DataSource dataSource() {
49-
return DataSourceBuilder.create()
50-
.url("jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;MODE=MySQL")
51-
.driverClassName("org.h2.Driver")
52-
.username("sa")
53-
.password("")
54-
.build();
49+
HikariDataSource ds = new HikariDataSource();
50+
ds.setJdbcUrl("jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;MODE=MySQL");
51+
ds.setDriverClassName("org.h2.Driver");
52+
ds.setUsername("sa");
53+
ds.setPassword("");
54+
return ds;
5555
}
5656

5757
@Bean

0 commit comments

Comments
 (0)