diff --git a/asciidoc/src/main/docs/asciidoc/index.adoc b/asciidoc/src/main/docs/asciidoc/index.adoc
index ff6b5197..fbf1cab4 100644
--- a/asciidoc/src/main/docs/asciidoc/index.adoc
+++ b/asciidoc/src/main/docs/asciidoc/index.adoc
@@ -9,6 +9,10 @@ Release date: `21 Apr 2024`
* http://bucket4j.com/{revnumber}/toc.html[{revnumber} Reference]
* http://bucket4j.com/commercial/java8.html[Instructions to get the builds for Java 8]
+== How to support
+https://app.lava.top/ru/2716741203?donate=open[Make donate] to increase motivation of project maintainer to develop new features,
+write documentation and answer user questions.
+
== Documentation archive
If you are looking for documentation related to a previous version then http://bucket4j.com/previos-releases.html[ see there]
@@ -17,7 +21,7 @@ Vladimir Bukhtoyarov::
Lead Java developer at vk.com +
Saint-Petersburg, Russia +
jsecoder@mail.ru +
-Role: maintainer, future vision, architect +
+Role: maintainer, future vision, code owner +
image:images/photo.jpg[80,80] +
Maxim Bartkov::
diff --git a/bucket4j-core/src/main/java/io/github/bucket4j/distributed/proxy/ExpiredEntriesCleaner.java b/bucket4j-core/src/main/java/io/github/bucket4j/distributed/proxy/ExpiredEntriesCleaner.java
index 04ce3a48..d9f687e9 100644
--- a/bucket4j-core/src/main/java/io/github/bucket4j/distributed/proxy/ExpiredEntriesCleaner.java
+++ b/bucket4j-core/src/main/java/io/github/bucket4j/distributed/proxy/ExpiredEntriesCleaner.java
@@ -19,8 +19,6 @@
*/
package io.github.bucket4j.distributed.proxy;
-import java.util.Collection;
-
/**
* Interface used by particular {@link ProxyManager} implementations to indicate
* that they support the manual(triggered by user) removing of expired bucket entries.
@@ -41,21 +39,21 @@ public interface ExpiredEntriesCleaner {
*
Example of usage:
*
* {@code
- * private static final int CLEANUP_INTERVAL_MILLIS = 10_000;
- * private static final int MAX_KEYS_TO_REMOVE_IN_ONE_TRANSACTION = 100;
- * private static final int THRESHOLD_TO_CONTINUE_REMOVING = 20;
+ * private static final int MAX_TO_REMOVE_IN_ONE_TRANSACTION = 1_000;
+ * private static final int THRESHOLD_TO_CONTINUE_REMOVING = 50;
*
- * @Scheduled(fixedDelay = CLEANUP_INTERVAL_MILLIS)
+ * // once per day at 4:30 morning
+ * @Scheduled(cron = "0 30 4 * * *")
* public void scheduleFixedDelayTask() {
* int removedKeysCount;
* do {
- * removedKeysCount = proxyManager.removedKeys(MAX_KEYS_TO_REMOVE_IN_ONE_TRANSACTION);
+ * removedCount = proxyManager.removeExpired(MAX_TO_REMOVE_IN_ONE_TRANSACTION);
* if (removedKeysCount > 0) {
- * logger.info("Removed {} expired buckets", removedKeysCount);
+ * logger.info("Removed {} expired buckets", removedCount);
* } else {
* logger.info("There are no expired buckets to remove");
* }
- * } while (removedKeysCount > THRESHOLD_TO_CONTINUE_REMOVING)
+ * } while (removedCount > THRESHOLD_TO_CONTINUE_REMOVING)
* }
* }
*
diff --git a/bucket4j-mysql/pom.xml b/bucket4j-mysql/pom.xml
index b8535df8..802c5d1d 100644
--- a/bucket4j-mysql/pom.xml
+++ b/bucket4j-mysql/pom.xml
@@ -41,19 +41,19 @@
org.testcontainers
mysql
- 1.16.3
+ 1.19.7
test
mysql
mysql-connector-java
- 8.0.28
+ 8.0.33
provided
com.zaxxer
- HikariCP-java6
- 2.3.8
+ HikariCP
+ 5.1.0
test
diff --git a/bucket4j-mysql/src/main/java/io/github/bucket4j/mysql/Bucket4jMySQL.java b/bucket4j-mysql/src/main/java/io/github/bucket4j/mysql/Bucket4jMySQL.java
index 4cc92d97..22b0ac89 100644
--- a/bucket4j-mysql/src/main/java/io/github/bucket4j/mysql/Bucket4jMySQL.java
+++ b/bucket4j-mysql/src/main/java/io/github/bucket4j/mysql/Bucket4jMySQL.java
@@ -64,6 +64,11 @@ public MySQLSelectForUpdateBasedProxyManagerBuilder primaryKeyMapper(Pr
super.primaryKeyMapper = (PrimaryKeyMapper) Objects.requireNonNull(primaryKeyMapper);
return (MySQLSelectForUpdateBasedProxyManagerBuilder) this;
}
+
+ @Override
+ public boolean isExpireAfterWriteSupported() {
+ return true;
+ }
}
}
diff --git a/bucket4j-mysql/src/main/java/io/github/bucket4j/mysql/MySQLSelectForUpdateBasedProxyManager.java b/bucket4j-mysql/src/main/java/io/github/bucket4j/mysql/MySQLSelectForUpdateBasedProxyManager.java
index f4f4a4b4..9a617b44 100644
--- a/bucket4j-mysql/src/main/java/io/github/bucket4j/mysql/MySQLSelectForUpdateBasedProxyManager.java
+++ b/bucket4j-mysql/src/main/java/io/github/bucket4j/mysql/MySQLSelectForUpdateBasedProxyManager.java
@@ -21,8 +21,10 @@
import com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException;
import io.github.bucket4j.BucketExceptions;
+import io.github.bucket4j.distributed.jdbc.CustomColumnProvider;
import io.github.bucket4j.distributed.jdbc.PrimaryKeyMapper;
import io.github.bucket4j.distributed.jdbc.SQLProxyConfiguration;
+import io.github.bucket4j.distributed.proxy.ExpiredEntriesCleaner;
import io.github.bucket4j.distributed.proxy.generic.select_for_update.AbstractSelectForUpdateBasedProxyManager;
import io.github.bucket4j.distributed.proxy.generic.select_for_update.LockAndGetResult;
import io.github.bucket4j.distributed.proxy.generic.select_for_update.SelectForUpdateBasedTransaction;
@@ -35,6 +37,8 @@
import java.sql.ResultSet;
import java.sql.SQLException;
import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Objects;
import java.util.Optional;
@@ -45,7 +49,7 @@
*
* @param type of primary key
*/
-public class MySQLSelectForUpdateBasedProxyManager extends AbstractSelectForUpdateBasedProxyManager {
+public class MySQLSelectForUpdateBasedProxyManager extends AbstractSelectForUpdateBasedProxyManager implements ExpiredEntriesCleaner {
private final DataSource dataSource;
private final PrimaryKeyMapper primaryKeyMapper;
@@ -53,16 +57,35 @@ public class MySQLSelectForUpdateBasedProxyManager extends AbstractSelectForU
private final String updateSqlQuery;
private final String insertSqlQuery;
private final String selectSqlQuery;
+ private final String clearExpiredSqlQuery;
+ private final List> customColumns = new ArrayList<>();
MySQLSelectForUpdateBasedProxyManager(MySQLSelectForUpdateBasedProxyManagerBuilder builder) {
super(builder.getClientSideConfig());
this.dataSource = builder.getDataSource();
this.primaryKeyMapper = builder.getPrimaryKeyMapper();
this.removeSqlQuery = MessageFormat.format("DELETE FROM {0} WHERE {1} = ?", builder.getTableName(), builder.getIdColumnName());
- updateSqlQuery = MessageFormat.format("UPDATE {0} SET {1}=? WHERE {2}=?", builder.getTableName(), builder.getStateColumnName(), builder.getIdColumnName());
insertSqlQuery = MessageFormat.format("INSERT IGNORE INTO {0}({1}, {2}) VALUES(?, null)",
builder.getTableName(), builder.getIdColumnName(), builder.getStateColumnName());
selectSqlQuery = MessageFormat.format("SELECT {0} as state FROM {1} WHERE {2} = ? FOR UPDATE", builder.getStateColumnName(), builder.getTableName(), builder.getIdColumnName());
+ this.customColumns.addAll(builder.getCustomColumns());
+ getClientSideConfig().getExpirationAfterWriteStrategy().ifPresent(expiration -> {
+ this.customColumns.add(CustomColumnProvider.createExpiresInColumnProvider(builder.getExpiresAtColumnName(), expiration));
+ });
+ if (customColumns.isEmpty()) {
+ this.updateSqlQuery = MessageFormat.format("UPDATE {0} SET {1}=? WHERE {2}=?", builder.getTableName(), builder.getStateColumnName(), builder.getIdColumnName());
+ } else {
+ String customPartInUpdate = String.join(",", customColumns.stream().map(column -> column.getCustomFieldName() + "=?").toList());
+ this.updateSqlQuery = MessageFormat.format("UPDATE {0} SET {1}=?,{2} WHERE {3}=?", builder.getTableName(), builder.getStateColumnName(), customPartInUpdate, builder.getIdColumnName());
+ }
+ // https://stackoverflow.com/questions/12810346/alternative-to-using-limit-keyword-in-a-subquery-in-mysql
+ this.clearExpiredSqlQuery = MessageFormat.format(
+ """
+ DELETE FROM {0} WHERE
+ {2} < ? AND
+ {1} IN(SELECT * FROM (SELECT {1} FROM {0} WHERE {2} < ? LIMIT ? FOR UPDATE SKIP LOCKED) as subquery)
+ """, builder.getTableName(), builder.getIdColumnName(), builder.getExpiresAtColumnName()
+ );
}
/**
@@ -71,6 +94,7 @@ public class MySQLSelectForUpdateBasedProxyManager extends AbstractSelectForU
@Deprecated
public MySQLSelectForUpdateBasedProxyManager(SQLProxyConfiguration configuration) {
super(configuration.getClientSideConfig());
+ this.clearExpiredSqlQuery = null;
this.dataSource = Objects.requireNonNull(configuration.getDataSource());
this.primaryKeyMapper = configuration.getPrimaryKeyMapper();
this.removeSqlQuery = MessageFormat.format("DELETE FROM {0} WHERE {1} = ?", configuration.getTableName(), configuration.getIdName());
@@ -78,6 +102,9 @@ public MySQLSelectForUpdateBasedProxyManager(SQLProxyConfiguration configurat
insertSqlQuery = MessageFormat.format("INSERT IGNORE INTO {0}({1}, {2}) VALUES(?, null)",
configuration.getTableName(), configuration.getIdName(), configuration.getStateName());
selectSqlQuery = MessageFormat.format("SELECT {0} as state FROM {1} WHERE {2} = ? FOR UPDATE", configuration.getStateName(), configuration.getTableName(), configuration.getIdName());
+ if (getClientSideConfig().getExpirationAfterWriteStrategy().isPresent()) {
+ throw new IllegalArgumentException();
+ }
}
@Override
@@ -104,8 +131,12 @@ public void update(byte[] data, RemoteBucketState newState, Optional reque
try {
try (PreparedStatement updateStatement = connection.prepareStatement(updateSqlQuery)) {
applyTimeout(updateStatement, requestTimeoutNanos);
- updateStatement.setBytes(1, data);
- primaryKeyMapper.set(updateStatement, 2, key);
+ int i = 0;
+ updateStatement.setBytes(++i, data);
+ for (CustomColumnProvider column : customColumns) {
+ column.setCustomField(key, ++i, updateStatement, newState, currentTimeNanos());
+ }
+ primaryKeyMapper.set(updateStatement, ++i, key);
updateStatement.executeUpdate();
}
} catch (SQLException e) {
@@ -187,4 +218,25 @@ public void removeProxy(K key) {
}
}
+ @Override
+ public boolean isExpireAfterWriteSupported() {
+ return true;
+ }
+
+
+ @Override
+ public int removeExpired(int batchSize) {
+ try (Connection connection = dataSource.getConnection()) {
+ long currentTimeMillis = System.currentTimeMillis();
+ try(PreparedStatement clearStatement = connection.prepareStatement(clearExpiredSqlQuery)) {
+ clearStatement.setLong(1, currentTimeMillis);
+ clearStatement.setLong(2, currentTimeMillis);
+ clearStatement.setInt(3, batchSize);
+ return clearStatement.executeUpdate();
+ }
+ } catch (SQLException e) {
+ throw new BucketExceptions.BucketExecutionException(e);
+ }
+ }
+
}
diff --git a/bucket4j-mysql/src/test/java/io/github/bucket4j/mysql/MySQLSelectForUpdateLockBasedTransactionTest.java b/bucket4j-mysql/src/test/java/io/github/bucket4j/mysql/MySQLSelectForUpdateLockBasedTransactionTest.java
index 562b63b3..93f32559 100644
--- a/bucket4j-mysql/src/test/java/io/github/bucket4j/mysql/MySQLSelectForUpdateLockBasedTransactionTest.java
+++ b/bucket4j-mysql/src/test/java/io/github/bucket4j/mysql/MySQLSelectForUpdateLockBasedTransactionTest.java
@@ -9,6 +9,7 @@
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.testcontainers.containers.MySQLContainer;
+import org.testcontainers.utility.DockerImageName;
import javax.sql.DataSource;
import java.sql.Connection;
@@ -28,7 +29,7 @@ public static void initializeInstance() throws SQLException {
container = startMySQLContainer();
dataSource = createJdbcDataSource(container);
BucketTableSettings tableSettings = BucketTableSettings.customSettings("test.bucket", "id", "state");
- final String INIT_TABLE_SCRIPT = "CREATE TABLE IF NOT EXISTS {0}({1} BIGINT PRIMARY KEY, {2} BLOB)";
+ final String INIT_TABLE_SCRIPT = "CREATE TABLE IF NOT EXISTS {0}({1} BIGINT PRIMARY KEY, {2} BLOB, expires_at BIGINT)";
try (Connection connection = dataSource.getConnection()) {
try (Statement statement = connection.createStatement()) {
String query = MessageFormat.format(INIT_TABLE_SCRIPT, tableSettings.getTableName(), tableSettings.getIdName(), tableSettings.getStateName());
@@ -44,7 +45,7 @@ public static void initializeInstance() throws SQLException {
.table("test.bucket")
.idColumn("id")
.stateColumn("state")
- )
+ ).checkExpiration()
);
}
@@ -56,17 +57,16 @@ public static void shutdown() {
}
private static DataSource createJdbcDataSource(MySQLContainer container) {
- HikariConfig hikariConfig = new HikariConfig();
- hikariConfig.setJdbcUrl(container.getJdbcUrl());
- hikariConfig.setUsername(container.getUsername());
- hikariConfig.setPassword(container.getPassword());
- hikariConfig.setDriverClassName(container.getDriverClassName());
- hikariConfig.setMaximumPoolSize(100);
- return new HikariDataSource(hikariConfig);
+ HikariConfig config = new HikariConfig();
+ config.setJdbcUrl(container.getJdbcUrl());
+ config.setUsername(container.getUsername());
+ config.setPassword(container.getPassword());
+ config.setMaximumPoolSize(10);
+ return new HikariDataSource(config);
}
private static MySQLContainer startMySQLContainer() {
- MySQLContainer container = new MySQLContainer();
+ MySQLContainer container = new MySQLContainer(DockerImageName.parse("mysql:8.0.36"));
container.start();
return container;
}