|
29 | 29 | import org.apache.flink.agents.plan.JavaFunction; |
30 | 30 | import org.apache.flink.agents.plan.actions.Action; |
31 | 31 | import org.apache.flink.agents.runtime.actionstate.ActionState; |
| 32 | +import org.apache.flink.agents.runtime.actionstate.CallResult; |
32 | 33 | import org.apache.flink.agents.runtime.actionstate.InMemoryActionStateStore; |
33 | 34 | import org.apache.flink.agents.runtime.eventlog.FileEventLogger; |
34 | 35 | import org.apache.flink.api.common.typeinfo.TypeInformation; |
@@ -297,6 +298,62 @@ agentPlanWithStateStore, true, new InMemoryActionStateStore(false)), |
297 | 298 | } |
298 | 299 | } |
299 | 300 |
|
| 301 | + @Test |
| 302 | + void testDoesNotPruneBeforeCheckpointComplete() throws Exception { |
| 303 | + AgentPlan agentPlanWithStateStore = TestAgent.getAgentPlan(false); |
| 304 | + RecordingActionStateStore actionStateStore = new RecordingActionStateStore(); |
| 305 | + |
| 306 | + try (KeyedOneInputStreamOperatorTestHarness<Long, Long, Object> testHarness = |
| 307 | + new KeyedOneInputStreamOperatorTestHarness<>( |
| 308 | + new ActionExecutionOperatorFactory<>( |
| 309 | + agentPlanWithStateStore, true, actionStateStore), |
| 310 | + (KeySelector<Long, Long>) value -> value, |
| 311 | + TypeInformation.of(Long.class))) { |
| 312 | + testHarness.open(); |
| 313 | + ActionExecutionOperator<Long, Object> operator = |
| 314 | + (ActionExecutionOperator<Long, Object>) testHarness.getOperator(); |
| 315 | + |
| 316 | + testHarness.processElement(new StreamRecord<>(5L)); |
| 317 | + operator.waitInFlightEventsFinished(); |
| 318 | + assertThat(actionStateStore.getPrunedSeqNums()).isEmpty(); |
| 319 | + |
| 320 | + testHarness.snapshot(1L, 1L); |
| 321 | + assertThat(actionStateStore.getPrunedSeqNums()).isEmpty(); |
| 322 | + testHarness.notifyOfCompletedCheckpoint(1L); |
| 323 | + |
| 324 | + assertThat(actionStateStore.getPrunedSeqNums()).containsExactly(0L); |
| 325 | + } |
| 326 | + } |
| 327 | + |
| 328 | + @Test |
| 329 | + void testDoesNotPruneSeqsInFlight() throws Exception { |
| 330 | + AgentPlan agentPlanWithStateStore = TestAgent.getAgentPlan(false); |
| 331 | + RecordingActionStateStore actionStateStore = new RecordingActionStateStore(); |
| 332 | + |
| 333 | + try (KeyedOneInputStreamOperatorTestHarness<Long, Long, Object> testHarness = |
| 334 | + new KeyedOneInputStreamOperatorTestHarness<>( |
| 335 | + new ActionExecutionOperatorFactory<>( |
| 336 | + agentPlanWithStateStore, true, actionStateStore), |
| 337 | + (KeySelector<Long, Long>) value -> value, |
| 338 | + TypeInformation.of(Long.class))) { |
| 339 | + testHarness.open(); |
| 340 | + ActionExecutionOperator<Long, Object> operator = |
| 341 | + (ActionExecutionOperator<Long, Object>) testHarness.getOperator(); |
| 342 | + |
| 343 | + testHarness.processElement(new StreamRecord<>(5L)); |
| 344 | + operator.waitInFlightEventsFinished(); |
| 345 | + actionStateStore.clearPruneCalls(); |
| 346 | + |
| 347 | + testHarness.processElement(new StreamRecord<>(5L)); |
| 348 | + assertThat(testHarness.getTaskMailbox().size()).isEqualTo(1); |
| 349 | + |
| 350 | + testHarness.snapshot(1L, 1L); |
| 351 | + testHarness.notifyOfCompletedCheckpoint(1L); |
| 352 | + |
| 353 | + assertThat(actionStateStore.getPrunedSeqNums()).containsExactly(0L); |
| 354 | + } |
| 355 | + } |
| 356 | + |
300 | 357 | @Test |
301 | 358 | void testEventLogBaseDirFromAgentConfig() throws Exception { |
302 | 359 | String baseLogDir = "/tmp/flink-agents-test"; |
@@ -461,7 +518,7 @@ agentPlanWithStateStore, true, new InMemoryActionStateStore(false)), |
461 | 518 | } |
462 | 519 |
|
463 | 520 | @Test |
464 | | - void testActionStateStoreCleanupAfterOutputEvent() throws Exception { |
| 521 | + void testActionStateStoreCleanupAfterCheckpointComplete() throws Exception { |
465 | 522 | AgentPlan agentPlanWithStateStore = TestAgent.getAgentPlan(false); |
466 | 523 |
|
467 | 524 | try (KeyedOneInputStreamOperatorTestHarness<Long, Long, Object> testHarness = |
@@ -496,10 +553,66 @@ agentPlanWithStateStore, true, new InMemoryActionStateStore(true)), |
496 | 553 | actionStateStoreField.setAccessible(true); |
497 | 554 | InMemoryActionStateStore actionStateStore = |
498 | 555 | (InMemoryActionStateStore) actionStateStoreField.get(operator); |
| 556 | + assertThat(actionStateStore.getKeyedActionStates()).isNotEmpty(); |
| 557 | + |
| 558 | + testHarness.snapshot(1L, 1L); |
| 559 | + testHarness.notifyOfCompletedCheckpoint(1L); |
| 560 | + |
499 | 561 | assertThat(actionStateStore.getKeyedActionStates()).isEmpty(); |
500 | 562 | } |
501 | 563 | } |
502 | 564 |
|
| 565 | + @Test |
| 566 | + void testEarlierCheckpointReplayKeepsDurableState() throws Exception { |
| 567 | + AgentPlan agentPlan = TestAgent.getDurableSyncAgentPlan(); |
| 568 | + InMemoryActionStateStore actionStateStore = new InMemoryActionStateStore(true); |
| 569 | + OperatorSubtaskState snapshot; |
| 570 | + |
| 571 | + TestAgent.DURABLE_CALL_COUNTER.set(0); |
| 572 | + |
| 573 | + try (KeyedOneInputStreamOperatorTestHarness<Long, Long, Object> testHarness = |
| 574 | + new KeyedOneInputStreamOperatorTestHarness<>( |
| 575 | + new ActionExecutionOperatorFactory<>(agentPlan, true, actionStateStore), |
| 576 | + (KeySelector<Long, Long>) value -> value, |
| 577 | + TypeInformation.of(Long.class))) { |
| 578 | + testHarness.open(); |
| 579 | + ActionExecutionOperator<Long, Object> operator = |
| 580 | + (ActionExecutionOperator<Long, Object>) testHarness.getOperator(); |
| 581 | + |
| 582 | + // Simulate failure recovery from a checkpoint taken before this input was processed. |
| 583 | + snapshot = testHarness.snapshot(1L, 1L); |
| 584 | + |
| 585 | + testHarness.processElement(new StreamRecord<>(7L)); |
| 586 | + operator.waitInFlightEventsFinished(); |
| 587 | + |
| 588 | + assertThat(TestAgent.DURABLE_CALL_COUNTER.get()).isEqualTo(1); |
| 589 | + assertThat(actionStateStore.getKeyedActionStates()).isNotEmpty(); |
| 590 | + } |
| 591 | + |
| 592 | + try (KeyedOneInputStreamOperatorTestHarness<Long, Long, Object> testHarness = |
| 593 | + new KeyedOneInputStreamOperatorTestHarness<>( |
| 594 | + new ActionExecutionOperatorFactory<>(agentPlan, true, actionStateStore), |
| 595 | + (KeySelector<Long, Long>) value -> value, |
| 596 | + TypeInformation.of(Long.class))) { |
| 597 | + testHarness.initializeState(snapshot); |
| 598 | + testHarness.open(); |
| 599 | + ActionExecutionOperator<Long, Object> operator = |
| 600 | + (ActionExecutionOperator<Long, Object>) testHarness.getOperator(); |
| 601 | + |
| 602 | + // Replay the same input after restoring from the earlier checkpoint. |
| 603 | + testHarness.processElement(new StreamRecord<>(7L)); |
| 604 | + operator.waitInFlightEventsFinished(); |
| 605 | + |
| 606 | + List<StreamRecord<Object>> recordOutput = |
| 607 | + (List<StreamRecord<Object>>) testHarness.getRecordOutput(); |
| 608 | + assertThat(recordOutput).hasSize(1); |
| 609 | + assertThat(recordOutput.get(0).getValue()).isEqualTo(21L); |
| 610 | + assertThat(TestAgent.DURABLE_CALL_COUNTER.get()) |
| 611 | + .as("Durable supplier should not be re-executed during replay") |
| 612 | + .isEqualTo(1); |
| 613 | + } |
| 614 | + } |
| 615 | + |
503 | 616 | @Test |
504 | 617 | void testActionStateStoreReplayIncurNoFunctionCall() throws Exception { |
505 | 618 | AgentPlan agentPlanWithStateStore = TestAgent.getAgentPlan(false); |
@@ -1524,6 +1637,60 @@ public static AgentPlan getDurableAsyncExceptionAgentPlan() { |
1524 | 1637 | } |
1525 | 1638 | } |
1526 | 1639 |
|
| 1640 | + private static ActionState actionStateWithCallResults(CallResult... callResults) { |
| 1641 | + ActionState actionState = new ActionState(null); |
| 1642 | + for (CallResult callResult : callResults) { |
| 1643 | + actionState.addCallResult(callResult); |
| 1644 | + } |
| 1645 | + return actionState; |
| 1646 | + } |
| 1647 | + |
| 1648 | + private static void seedActionState( |
| 1649 | + InMemoryActionStateStore actionStateStore, |
| 1650 | + long key, |
| 1651 | + long input, |
| 1652 | + AgentPlan agentPlan, |
| 1653 | + String actionName, |
| 1654 | + ActionState actionState) |
| 1655 | + throws Exception { |
| 1656 | + InputEvent event = new InputEvent(input); |
| 1657 | + Action action = agentPlan.getActions().get(actionName); |
| 1658 | + actionStateStore.put(key, 0L, action, event, actionState); |
| 1659 | + } |
| 1660 | + |
| 1661 | + private static ActionState getStoredActionState( |
| 1662 | + InMemoryActionStateStore actionStateStore, |
| 1663 | + long key, |
| 1664 | + long input, |
| 1665 | + AgentPlan agentPlan, |
| 1666 | + String actionName) |
| 1667 | + throws Exception { |
| 1668 | + InputEvent event = new InputEvent(input); |
| 1669 | + Action action = agentPlan.getActions().get(actionName); |
| 1670 | + return actionStateStore.get(key, 0L, action, event); |
| 1671 | + } |
| 1672 | + |
| 1673 | + private static class RecordingActionStateStore extends InMemoryActionStateStore { |
| 1674 | + private final List<Long> prunedSeqNums = new java.util.ArrayList<>(); |
| 1675 | + |
| 1676 | + private RecordingActionStateStore() { |
| 1677 | + super(false); |
| 1678 | + } |
| 1679 | + |
| 1680 | + @Override |
| 1681 | + public void pruneState(Object key, long seqNum) { |
| 1682 | + prunedSeqNums.add(seqNum); |
| 1683 | + } |
| 1684 | + |
| 1685 | + private void clearPruneCalls() { |
| 1686 | + prunedSeqNums.clear(); |
| 1687 | + } |
| 1688 | + |
| 1689 | + private List<Long> getPrunedSeqNums() { |
| 1690 | + return prunedSeqNums; |
| 1691 | + } |
| 1692 | + } |
| 1693 | + |
1527 | 1694 | private static void assertMailboxSizeAndRun(TaskMailbox mailbox, int expectedSize) |
1528 | 1695 | throws Exception { |
1529 | 1696 | assertThat(mailbox.size()).isEqualTo(expectedSize); |
|
0 commit comments