feat: escalate overdue approval tasks via processDue
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+53
-4
@@ -13,6 +13,7 @@ import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Timestamp;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
@@ -21,10 +22,10 @@ import java.util.Optional;
|
||||
/** JDBC implementation of the task storage port and its pending-task indexes. */
|
||||
public final class JdbcApprovalTaskRepository implements ApprovalTaskRepository {
|
||||
private static final String TASK_COLUMNS =
|
||||
"id, instance_id, step_id, task_name, assignee, status, created_at, completed_at, action_actor, action_comment, action_at";
|
||||
"id, instance_id, step_id, task_name, assignee, status, created_at, completed_at, action_actor, action_comment, action_at, due_at";
|
||||
private static final String INSERT_TASK =
|
||||
"INSERT INTO ordo_approval_task (id, instance_id, step_id, task_name, assignee, status, created_at)"
|
||||
+ " VALUES (?, ?, ?, ?, ?, ?, ?)";
|
||||
"INSERT INTO ordo_approval_task (id, instance_id, step_id, task_name, assignee, status, created_at, due_at)"
|
||||
+ " VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
|
||||
private static final String COMPLETE_IF_PENDING =
|
||||
"UPDATE ordo_approval_task SET status = ?, completed_at = ?, action_actor = ?, action_comment = ?, action_at = ?"
|
||||
+ " WHERE id = ? AND status = 'PENDING' AND assignee = ?";
|
||||
@@ -43,9 +44,15 @@ public final class JdbcApprovalTaskRepository implements ApprovalTaskRepository
|
||||
private static final String SELECT_PENDING_BY_INSTANCE =
|
||||
"SELECT " + TASK_COLUMNS + " FROM ordo_approval_task WHERE status = 'PENDING' AND instance_id = ?"
|
||||
+ " ORDER BY created_at, id";
|
||||
private static final String CLAIM_IF_DUE =
|
||||
"UPDATE ordo_approval_task SET due_at = NULL WHERE id = ? AND status = 'PENDING' AND assignee = ?"
|
||||
+ " AND due_at IS NOT NULL AND due_at <= ?";
|
||||
private static final String SELECT_DUE_PENDING =
|
||||
"SELECT " + TASK_COLUMNS + " FROM ordo_approval_task WHERE status = 'PENDING' AND due_at IS NOT NULL"
|
||||
+ " AND due_at <= ? ORDER BY due_at, id LIMIT ?";
|
||||
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.action_actor, t.action_comment, t.action_at, t.due_at";
|
||||
|
||||
private final JdbcConnectionProvider connectionProvider;
|
||||
|
||||
@@ -139,6 +146,48 @@ public final class JdbcApprovalTaskRepository implements ApprovalTaskRepository
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ApprovalTask> findDuePending(Instant now, int limit) {
|
||||
Objects.requireNonNull(now, "now must not be null");
|
||||
if (limit <= 0) {
|
||||
throw new IllegalArgumentException("limit must be positive");
|
||||
}
|
||||
Connection connection = connectionProvider.getConnection();
|
||||
try (PreparedStatement select = connection.prepareStatement(SELECT_DUE_PENDING)) {
|
||||
select.setTimestamp(1, Timestamp.from(now));
|
||||
select.setInt(2, limit);
|
||||
try (ResultSet resultSet = select.executeQuery()) {
|
||||
List<ApprovalTask> tasks = new ArrayList<>();
|
||||
while (resultSet.next()) {
|
||||
tasks.add(ApprovalTaskMapper.read(resultSet));
|
||||
}
|
||||
return tasks;
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
throw new JdbcStorageException("failed to query due tasks", e);
|
||||
} finally {
|
||||
connectionProvider.close(connection);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean claimIfDue(String taskId, String expectedAssignee, Instant now) {
|
||||
Objects.requireNonNull(taskId, "taskId must not be null");
|
||||
Objects.requireNonNull(expectedAssignee, "expectedAssignee must not be null");
|
||||
Objects.requireNonNull(now, "now must not be null");
|
||||
Connection connection = connectionProvider.getConnection();
|
||||
try (PreparedStatement update = connection.prepareStatement(CLAIM_IF_DUE)) {
|
||||
update.setString(1, taskId);
|
||||
update.setString(2, expectedAssignee);
|
||||
update.setTimestamp(3, Timestamp.from(now));
|
||||
return update.executeUpdate() == 1;
|
||||
} catch (SQLException e) {
|
||||
throw new JdbcStorageException("failed to claim due task: " + taskId, e);
|
||||
} finally {
|
||||
connectionProvider.close(connection);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Page<ApprovalTask> query(TaskQuery query, PageRequest pageRequest) {
|
||||
Objects.requireNonNull(query, "query must not be null");
|
||||
|
||||
+16
-4
@@ -27,8 +27,8 @@ public final class JdbcProcessDefinitionRepository implements ProcessDefinitionR
|
||||
private static final String INSERT_DEFINITION =
|
||||
"INSERT INTO ordo_process_definition (id, name) VALUES (?, ?)";
|
||||
private static final String INSERT_STEP =
|
||||
"INSERT INTO ordo_approval_step (definition_id, step_id, step_name, policy, step_order, kind, action_key)"
|
||||
+ " VALUES (?, ?, ?, ?, ?, ?, ?)";
|
||||
"INSERT INTO ordo_approval_step (definition_id, step_id, step_name, policy, step_order, kind, action_key,"
|
||||
+ " due_after, due_then, due_to, due_action) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
|
||||
private static final String INSERT_CANDIDATE =
|
||||
"INSERT INTO ordo_step_candidate (definition_id, step_id, candidate, candidate_order) VALUES (?, ?, ?, ?)";
|
||||
private static final String INSERT_TRANSITION =
|
||||
@@ -44,7 +44,8 @@ public final class JdbcProcessDefinitionRepository implements ProcessDefinitionR
|
||||
private static final String SELECT_DEFINITION =
|
||||
"SELECT id, name FROM ordo_process_definition WHERE id = ?";
|
||||
private static final String SELECT_STEPS =
|
||||
"SELECT step_id, step_name, policy, kind, action_key FROM ordo_approval_step"
|
||||
"SELECT step_id, step_name, policy, kind, action_key, due_after, due_then, due_to, due_action"
|
||||
+ " FROM ordo_approval_step"
|
||||
+ " WHERE definition_id = ? ORDER BY step_order";
|
||||
private static final String SELECT_CANDIDATES =
|
||||
"SELECT step_id, candidate FROM ordo_step_candidate WHERE definition_id = ? ORDER BY step_id, candidate_order";
|
||||
@@ -176,7 +177,7 @@ public final class JdbcProcessDefinitionRepository implements ProcessDefinitionR
|
||||
for (StepRow row : stepRows) {
|
||||
List<String> candidates = candidatesByStep.getOrDefault(row.stepId(), List.of());
|
||||
steps.add(new ApprovalStep(row.stepId(), row.stepName(), candidates, row.policy(), row.kind(),
|
||||
row.actionKey()));
|
||||
row.actionKey(), ApprovalStepMapper.toDue(row)));
|
||||
}
|
||||
List<StepTransition> transitions = new ArrayList<>();
|
||||
try (PreparedStatement selectTransitions = connection.prepareStatement(SELECT_TRANSITIONS)) {
|
||||
@@ -249,6 +250,17 @@ public final class JdbcProcessDefinitionRepository implements ProcessDefinitionR
|
||||
insertStep.setInt(5, stepOrder++);
|
||||
insertStep.setString(6, step.kind().name());
|
||||
insertStep.setString(7, step.actionKey());
|
||||
if (step.due() == null) {
|
||||
insertStep.setString(8, null);
|
||||
insertStep.setString(9, null);
|
||||
insertStep.setString(10, null);
|
||||
insertStep.setString(11, null);
|
||||
} else {
|
||||
insertStep.setString(8, step.due().after().toString());
|
||||
insertStep.setString(9, step.due().then().name());
|
||||
insertStep.setString(10, step.due().to());
|
||||
insertStep.setString(11, step.due().action());
|
||||
}
|
||||
insertStep.executeUpdate();
|
||||
}
|
||||
int candidateOrder = 0;
|
||||
|
||||
+18
-2
@@ -1,10 +1,13 @@
|
||||
package com.jetlumen.ordo.storage.jdbc.mapper;
|
||||
|
||||
import com.jetlumen.ordo.api.ApprovalPolicy;
|
||||
import com.jetlumen.ordo.api.DueThen;
|
||||
import com.jetlumen.ordo.api.StepDue;
|
||||
import com.jetlumen.ordo.api.StepKind;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.time.Duration;
|
||||
|
||||
/**
|
||||
* Maps rows of {@code ordo_approval_step}. A step's candidate list lives in the separate
|
||||
@@ -19,9 +22,22 @@ public final class ApprovalStepMapper {
|
||||
return new StepRow(resultSet.getString("step_id"), resultSet.getString("step_name"),
|
||||
ApprovalPolicy.valueOf(resultSet.getString("policy")),
|
||||
StepKind.valueOf(resultSet.getString("kind")),
|
||||
resultSet.getString("action_key"));
|
||||
resultSet.getString("action_key"),
|
||||
resultSet.getString("due_after"),
|
||||
resultSet.getString("due_then"),
|
||||
resultSet.getString("due_to"),
|
||||
resultSet.getString("due_action"));
|
||||
}
|
||||
|
||||
public record StepRow(String stepId, String stepName, ApprovalPolicy policy, StepKind kind, String actionKey) {
|
||||
public static StepDue toDue(StepRow row) {
|
||||
if (row.dueAfter() == null || row.dueAfter().isBlank()) {
|
||||
return null;
|
||||
}
|
||||
return new StepDue(Duration.parse(row.dueAfter()), DueThen.valueOf(row.dueThen()), row.dueTo(),
|
||||
row.dueAction());
|
||||
}
|
||||
|
||||
public record StepRow(String stepId, String stepName, ApprovalPolicy policy, StepKind kind, String actionKey,
|
||||
String dueAfter, String dueThen, String dueTo, String dueAction) {
|
||||
}
|
||||
}
|
||||
|
||||
+8
-1
@@ -23,6 +23,11 @@ public final class ApprovalTaskMapper {
|
||||
statement.setString(5, task.assignee());
|
||||
statement.setString(6, task.status().name());
|
||||
statement.setTimestamp(7, Timestamp.from(task.createdAt()));
|
||||
if (task.dueAt() == null) {
|
||||
statement.setNull(8, Types.TIMESTAMP);
|
||||
} else {
|
||||
statement.setTimestamp(8, Timestamp.from(task.dueAt()));
|
||||
}
|
||||
}
|
||||
|
||||
public static void bindComplete(PreparedStatement statement, ApprovalTask completedTask) throws SQLException {
|
||||
@@ -49,6 +54,7 @@ public final class ApprovalTaskMapper {
|
||||
TaskAction action = actionActor == null ? null
|
||||
: new TaskAction(actionActor, resultSet.getString("action_comment"), actionAt.toInstant());
|
||||
Timestamp completedAt = resultSet.getTimestamp("completed_at");
|
||||
Timestamp dueAt = resultSet.getTimestamp("due_at");
|
||||
return new ApprovalTask(
|
||||
resultSet.getString("id"),
|
||||
resultSet.getString("instance_id"),
|
||||
@@ -58,6 +64,7 @@ public final class ApprovalTaskMapper {
|
||||
TaskStatus.valueOf(resultSet.getString("status")),
|
||||
resultSet.getTimestamp("created_at").toInstant(),
|
||||
completedAt == null ? null : completedAt.toInstant(),
|
||||
action);
|
||||
action,
|
||||
dueAt == null ? null : dueAt.toInstant());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
-- SLA / due escalation: task due_at plus step due policy columns.
|
||||
ALTER TABLE ordo_approval_task ADD COLUMN due_at TIMESTAMP;
|
||||
CREATE INDEX idx_approval_task_status_due ON ordo_approval_task (status, due_at);
|
||||
|
||||
ALTER TABLE ordo_approval_step ADD COLUMN due_after VARCHAR(32);
|
||||
ALTER TABLE ordo_approval_step ADD COLUMN due_then VARCHAR(16);
|
||||
ALTER TABLE ordo_approval_step ADD COLUMN due_to VARCHAR(255);
|
||||
ALTER TABLE ordo_approval_step ADD COLUMN due_action VARCHAR(255);
|
||||
Reference in New Issue
Block a user