feat: schedule due processing from next dueAt instead of polling
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+40
-1
@@ -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)) {
|
||||
|
||||
+5
@@ -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();
|
||||
}
|
||||
|
||||
+1
@@ -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]);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user