feat: let the current assignee reassign a pending task
Keep the same task id, move the pending inbox, and record TASK_REASSIGNED without advancing the step. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+21
-1
@@ -27,7 +27,9 @@ public final class JdbcApprovalTaskRepository implements ApprovalTaskRepository
|
||||
+ " 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'";
|
||||
+ " WHERE id = ? AND status = 'PENDING' AND assignee = ?";
|
||||
private static final String REASSIGN_IF_PENDING =
|
||||
"UPDATE ordo_approval_task SET assignee = ? WHERE id = ? AND status = 'PENDING' AND assignee = ?";
|
||||
private static final String SELECT_TASK =
|
||||
"SELECT " + TASK_COLUMNS + " FROM ordo_approval_task WHERE id = ?";
|
||||
private static final String SELECT_BY_INSTANCE =
|
||||
@@ -119,6 +121,24 @@ public final class JdbcApprovalTaskRepository implements ApprovalTaskRepository
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean reassignIfPending(String taskId, String expectedAssignee, String newAssignee) {
|
||||
Objects.requireNonNull(taskId, "taskId must not be null");
|
||||
Objects.requireNonNull(expectedAssignee, "expectedAssignee must not be null");
|
||||
Objects.requireNonNull(newAssignee, "newAssignee must not be null");
|
||||
Connection connection = connectionProvider.getConnection();
|
||||
try (PreparedStatement update = connection.prepareStatement(REASSIGN_IF_PENDING)) {
|
||||
update.setString(1, newAssignee);
|
||||
update.setString(2, taskId);
|
||||
update.setString(3, expectedAssignee);
|
||||
return update.executeUpdate() == 1;
|
||||
} catch (SQLException e) {
|
||||
throw new JdbcStorageException("failed to reassign task: " + taskId, e);
|
||||
} finally {
|
||||
connectionProvider.close(connection);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Page<ApprovalTask> query(TaskQuery query, PageRequest pageRequest) {
|
||||
Objects.requireNonNull(query, "query must not be null");
|
||||
|
||||
+1
@@ -40,6 +40,7 @@ public final class ApprovalTaskMapper {
|
||||
statement.setTimestamp(5, Timestamp.from(action.operatedAt()));
|
||||
}
|
||||
statement.setString(6, completedTask.id());
|
||||
statement.setString(7, completedTask.assignee());
|
||||
}
|
||||
|
||||
public static ApprovalTask read(ResultSet resultSet) throws SQLException {
|
||||
|
||||
+64
@@ -154,6 +154,70 @@ class JdbcApprovalTaskRepositoryTest {
|
||||
assertTrue(Set.of("first", "second").contains(stored.action().comment()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void reassignIfPendingUpdatesAssigneeAndFailsWhenNotPending() {
|
||||
ApprovalTask pending = pendingTask("task-1", "inst-1", "manager", "maria", CREATED_AT);
|
||||
repository.save(pending);
|
||||
|
||||
assertTrue(repository.reassignIfPending("task-1", "maria", "diana"));
|
||||
assertEquals("diana", repository.findById("task-1").orElseThrow().assignee());
|
||||
assertEquals(List.of(repository.findById("task-1").orElseThrow()), repository.findPendingByAssignee("diana"));
|
||||
assertTrue(repository.findPendingByAssignee("maria").isEmpty());
|
||||
assertFalse(repository.reassignIfPending("task-1", "maria", "henry"));
|
||||
|
||||
assertTrue(repository.completeIfPending(completedTask(repository.findById("task-1").orElseThrow(), "ok")));
|
||||
assertFalse(repository.reassignIfPending("task-1", "diana", "henry"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void onlyOneOfConcurrentReassignAndCompletionWins() throws Exception {
|
||||
ApprovalTask pending = pendingTask("task-1", "inst-1", "manager", "maria", CREATED_AT);
|
||||
repository.save(pending);
|
||||
|
||||
CountDownLatch start = new CountDownLatch(1);
|
||||
CountDownLatch done = new CountDownLatch(2);
|
||||
AtomicInteger completeWins = new AtomicInteger();
|
||||
AtomicInteger reassignWins = new AtomicInteger();
|
||||
Thread completer = new Thread(() -> {
|
||||
try {
|
||||
start.await();
|
||||
if (repository.completeIfPending(completedTask(pending, "ok"))) {
|
||||
completeWins.incrementAndGet();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
} finally {
|
||||
done.countDown();
|
||||
}
|
||||
});
|
||||
Thread reassigner = new Thread(() -> {
|
||||
try {
|
||||
start.await();
|
||||
if (repository.reassignIfPending("task-1", "maria", "diana")) {
|
||||
reassignWins.incrementAndGet();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
} finally {
|
||||
done.countDown();
|
||||
}
|
||||
});
|
||||
completer.start();
|
||||
reassigner.start();
|
||||
start.countDown();
|
||||
|
||||
assertTrue(done.await(10, TimeUnit.SECONDS));
|
||||
assertEquals(1, completeWins.get() + reassignWins.get());
|
||||
ApprovalTask stored = repository.findById("task-1").orElseThrow();
|
||||
if (completeWins.get() == 1) {
|
||||
assertEquals(TaskStatus.APPROVED, stored.status());
|
||||
assertEquals("maria", stored.assignee());
|
||||
} else {
|
||||
assertEquals(TaskStatus.PENDING, stored.status());
|
||||
assertEquals("diana", stored.assignee());
|
||||
}
|
||||
}
|
||||
|
||||
private void attempt(CountDownLatch start, CountDownLatch done, AtomicInteger wins, ApprovalTask completed) {
|
||||
try {
|
||||
start.await();
|
||||
|
||||
+21
@@ -97,6 +97,27 @@ class JdbcOrdoEngineIntegrationTest {
|
||||
assertEquals("hr", engine.findPendingTasksByInstanceId(instance.id()).get(0).stepId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void reassignsAPendingTaskAndRecordsHistory() {
|
||||
ProcessInstance instance = engine.start("leave", "alice");
|
||||
ApprovalTask task = engine.findPendingTasksByInstanceId(instance.id()).get(0);
|
||||
|
||||
ApprovalTask reassigned = engine.reassign(task.id(), "maria", "diana");
|
||||
|
||||
assertEquals("diana", reassigned.assignee());
|
||||
assertEquals(TaskStatus.PENDING, reassigned.status());
|
||||
assertTrue(engine.findPendingTasksByAssignee("maria").isEmpty());
|
||||
assertEquals("diana", engine.findPendingTasksByAssignee("diana").get(0).assignee());
|
||||
List<ProcessEvent> history = engine.queryHistory(instance.id(), new PageRequest(0, 20)).content();
|
||||
ProcessEvent reassignedEvent = history.stream()
|
||||
.filter(event -> event.type() == ProcessEventType.TASK_REASSIGNED)
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
assertEquals("maria", reassignedEvent.actor());
|
||||
assertEquals("diana", reassignedEvent.detail());
|
||||
assertEquals(task.id(), reassignedEvent.taskId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rollsBackTheWholeApprovalWhenTheNextStepCannotBeCreated() {
|
||||
OrdoEngine failingEngine = newEngine((candidate, step, context) -> {
|
||||
|
||||
Reference in New Issue
Block a user