Skip to content

Commit fa57962

Browse files
committed
Add recovery test: replaying debouncer workflow must not restart user workflow
1 parent a25a54a commit fa57962

1 file changed

Lines changed: 50 additions & 0 deletions

File tree

transact/src/test/java/dev/dbos/transact/workflow/DebouncerTest.java

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,17 @@
55
import static org.junit.jupiter.api.Assertions.assertNotNull;
66
import static org.junit.jupiter.api.Assertions.assertTrue;
77

8+
import dev.dbos.transact.Constants;
89
import dev.dbos.transact.DBOS;
10+
import dev.dbos.transact.DBOSTestAccess;
911
import dev.dbos.transact.config.DBOSConfig;
1012
import dev.dbos.transact.utils.PgContainer;
1113

14+
import java.sql.Connection;
15+
import java.sql.PreparedStatement;
16+
import java.sql.SQLException;
1217
import java.time.Duration;
18+
import java.time.Instant;
1319
import java.util.List;
1420
import java.util.concurrent.ConcurrentLinkedQueue;
1521
import java.util.concurrent.CountDownLatch;
@@ -359,4 +365,48 @@ public void reDebounceAfterWindowCloses() throws Exception {
359365
// Each window produces an independent user workflow.
360366
assertNotEquals(h1.workflowId(), h2.workflowId());
361367
}
368+
369+
// Recovering/replaying the internal debouncer workflow must be idempotent: it reuses the
370+
// pre-assigned user workflow id and must not start a second user workflow execution.
371+
@Test
372+
public void recoveryDoesNotRestartUserWorkflow() throws Exception {
373+
DebouncedService svc = dbos.registerProxy(DebouncedService.class, serviceImpl);
374+
dbos.launch();
375+
376+
var handle =
377+
dbos.<String>debouncer().debounce("rec-key", Duration.ofMillis(300), () -> svc.process("v1"));
378+
String userWorkflowId = handle.workflowId();
379+
assertEquals("result:v1", handle.getResult());
380+
assertEquals(1, serviceImpl.callCount());
381+
382+
// Simulate a crash where the debouncer ran but did not durably record completion: flip only
383+
// the debouncer workflow back to PENDING (the user workflow stays SUCCESS) and recover it.
384+
var executor = DBOSTestAccess.getDbosExecutor(dbos);
385+
markDebouncerPending();
386+
387+
var recovered = executor.recoverPendingWorkflows(List.of(executor.executorId()));
388+
assertEquals(1, recovered.size());
389+
for (var h : recovered) {
390+
h.getResult();
391+
}
392+
393+
// Replay reused the same user workflow id and did not run the user workflow again.
394+
assertEquals(1, serviceImpl.callCount());
395+
assertEquals(List.of("v1"), serviceImpl.callArgs());
396+
WorkflowHandle<String, Exception> userHandle = dbos.retrieveWorkflow(userWorkflowId);
397+
assertEquals("result:v1", userHandle.getResult());
398+
assertEquals(WorkflowState.SUCCESS, userHandle.getStatus().status());
399+
}
400+
401+
private void markDebouncerPending() throws SQLException {
402+
var sql =
403+
"UPDATE dbos.workflow_status SET status = ?, queue_name = NULL, updated_at = ? WHERE name = ?";
404+
try (Connection conn = pgContainer.dataSource().getConnection();
405+
PreparedStatement stmt = conn.prepareStatement(sql)) {
406+
stmt.setString(1, WorkflowState.PENDING.name());
407+
stmt.setLong(2, Instant.now().toEpochMilli());
408+
stmt.setString(3, Constants.DEBOUNCER_WORKFLOW_NAME);
409+
stmt.executeUpdate();
410+
}
411+
}
362412
}

0 commit comments

Comments
 (0)