feat: schedule due processing from next dueAt instead of polling

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
0264408
2026-09-20 16:54:13 +08:00
co-authored by Cursor
parent 8d08a2840b
commit 02557a9b86
15 changed files with 183 additions and 12 deletions
@@ -11,6 +11,7 @@ import com.jetlumen.ordo.storage.jdbc.dialect.SqlDialects;
import com.jetlumen.ordo.storage.jdbc.mapper.ApprovalTaskMapper;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
@@ -18,6 +19,7 @@ import java.sql.Timestamp;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
@@ -52,6 +54,8 @@ public final class JdbcApprovalTaskRepository implements ApprovalTaskRepository
private static final String SELECT_DUE_PENDING_BASE =
"SELECT " + TASK_COLUMNS + " FROM ordo_approval_task WHERE status = 'PENDING' AND due_at IS NOT NULL"
+ " AND due_at <= ? ORDER BY due_at, id";
private static final String SELECT_NEXT_DUE_AT =
"SELECT MIN(due_at) FROM ordo_approval_task WHERE status = 'PENDING' AND due_at IS NOT NULL";
private static final String TASK_COLUMNS_QUALIFIED =
"t.id, t.instance_id, t.step_id, t.task_name, t.assignee, t.status, t.created_at, t.completed_at,"
+ " t.action_actor, t.action_comment, t.action_at, t.due_at";
@@ -67,7 +71,7 @@ public final class JdbcApprovalTaskRepository implements ApprovalTaskRepository
public JdbcApprovalTaskRepository(JdbcConnectionProvider connectionProvider, SqlDialect dialect) {
this.connectionProvider = Objects.requireNonNull(connectionProvider, "connectionProvider must not be null");
this.dialect = Objects.requireNonNull(dialect, "dialect must not be null");
this.selectDuePending = dialect.limit(SELECT_DUE_PENDING_BASE, false);
this.selectDuePending = duePendingSql(this.connectionProvider, this.dialect);
}
@Override
@@ -180,6 +184,23 @@ public final class JdbcApprovalTaskRepository implements ApprovalTaskRepository
}
}
@Override
public Optional<Instant> findNextDueAt() {
Connection connection = connectionProvider.getConnection();
try (PreparedStatement select = connection.prepareStatement(SELECT_NEXT_DUE_AT);
ResultSet resultSet = select.executeQuery()) {
if (!resultSet.next()) {
return Optional.empty();
}
Timestamp timestamp = resultSet.getTimestamp(1);
return timestamp == null ? Optional.empty() : Optional.of(timestamp.toInstant());
} catch (SQLException e) {
throw new JdbcStorageException("failed to query next due at", e);
} finally {
connectionProvider.close(connection);
}
}
@Override
public boolean claimIfDue(String taskId, String expectedAssignee, Instant now) {
Objects.requireNonNull(taskId, "taskId must not be null");
@@ -275,6 +296,24 @@ public final class JdbcApprovalTaskRepository implements ApprovalTaskRepository
}
}
private static String duePendingSql(JdbcConnectionProvider connectionProvider, SqlDialect dialect) {
String limited = dialect.limit(SELECT_DUE_PENDING_BASE, false);
return supportsSkipLocked(connectionProvider) ? dialect.forUpdateSkipLocked(limited) : limited;
}
private static boolean supportsSkipLocked(JdbcConnectionProvider connectionProvider) {
Connection connection = connectionProvider.getConnection();
try {
DatabaseMetaData metaData = connection.getMetaData();
String product = metaData.getDatabaseProductName();
return product != null && !product.toLowerCase(Locale.ROOT).contains("h2");
} catch (SQLException e) {
return false;
} finally {
connectionProvider.close(connection);
}
}
private List<ApprovalTask> findAll(String sql, String parameter) {
Connection connection = connectionProvider.getConnection();
try (PreparedStatement select = connection.prepareStatement(sql)) {
@@ -16,4 +16,9 @@ abstract class LimitOffsetSqlDialect implements SqlDialect {
public final String forUpdate(String sql) {
return sql + " FOR UPDATE";
}
@Override
public final String forUpdateSkipLocked(String sql) {
return sql + " FOR UPDATE SKIP LOCKED";
}
}
@@ -25,5 +25,8 @@ public interface SqlDialect {
String forUpdate(String sql);
/** Appends {@code FOR UPDATE SKIP LOCKED}. */
String forUpdateSkipLocked(String sql);
String[] flywayLocations();
}
@@ -54,6 +54,7 @@ class SqlDialectsTest {
assertEquals("SELECT 1 LIMIT ? OFFSET ?", dialect.limit("SELECT 1"));
assertEquals("SELECT 1 LIMIT ?", dialect.limit("SELECT 1", false));
assertEquals("SELECT 1 FOR UPDATE", dialect.forUpdate("SELECT 1"));
assertEquals("SELECT 1 FOR UPDATE SKIP LOCKED", dialect.forUpdateSkipLocked("SELECT 1"));
assertEquals("classpath:db/postgresql/migration", dialect.flywayLocations()[0]);
}