Skip to content

[runtime] Add comprehensive tests for ActionExecutionOperator manager classes - #659

Merged
wenjin272 merged 8 commits into
apache:mainfrom
weiqingy:646-impl
May 14, 2026
Merged

wenjin272 merged 8 commits into
apache:mainfrom
weiqingy:646-impl

Conversation

@weiqingy

Copy link
Copy Markdown
Collaborator

Linked issue: #646

Purpose of change

Closes the heavier test coverage explicitly deferred at PR #546's review for the manager classes that decomposed ActionExecutionOperator. PR #546 added 4 contract test files (7 tests, 295 lines) using mocks and stubs; heavier coverage — snapshot/recovery cycle, listener notification, watermark draining order, memory-context lifecycle — was deferred and tracked in #646. This PR closes that gap.

Scope (matches issue #646's deferred-coverage table):

Manager New coverage
DurableExecutionManager Full snapshot + recovery cycle; notifyCheckpointComplete pruning behaviour (3 new tests)
EventRouter Listener notification on event processed; watermark draining order under concurrent keys (5 new tests)
ActionTaskContextManager Full memory-context lifecycle: create → transfer → complete → cleanup (5 new tests)

OperatorStateManager is dropped from this PR — its observable contracts are already covered by ActionExecutionOperatorTest.java:176–231 (rescale + recovery) and :254–306 (durable state recovery). Adding a manager-scoped harness test would duplicate without adding coverage.

PythonBridgeManager is out of scope per the issue body's deferred-coverage table.

Commit structure (4 commits)

  1. [runtime] Add @VisibleForTesting seams for manager tests — three narrow seams in production code, all following the existing DurableExecutionManager.getActionStateStore() precedent:
    • DurableExecutionManager.getCheckpointIdToSeqNums() accessor
    • EventRouter.addEventListener(EventListener) accessor
    • EventRouter 3-arg @VisibleForTesting constructor for EventLogger injection (2-arg constructor delegates; production signature unchanged)
  2. [runtime] Add comprehensive tests for DurableExecutionManager — 3 new tests
  3. [runtime] Add comprehensive tests for EventRouter — 5 new tests
  4. [runtime] Add comprehensive tests for ActionTaskContextManager — 5 new tests

Tests

  • 13 new tests added; 6 existing manager tests untouched. After this PR: 5 + 7 + 7 + 1 = 20 tests across the four manager test classes (was 7).
  • Full runtime module: 262 tests, 0 failures, 0 errors, 0 skipped.
  • Spotless format check: ### FORMAT CHECK PASSED ###.
  • License check: clean (no tracked files missing headers).

Verification commands:

mvn -pl runtime -am test -Dsurefire.failIfNoSpecifiedTests=false
./tools/lint.sh -c

Final diff: 5 files, +464 / -2 (2 production-code files for the seams; 3 test files for the new tests).

Test design notes (each test catches a distinct mutation)

  • DurableExecutionManagerTest.notifyPrunesNotifiedCheckpointAndPreservesOthers is a single consolidated test that exercises BOTH the multi-key loop body (two keys under one checkpoint, both must be pruned) AND multi-checkpoint isolation (a third key under a different checkpoint must survive). Strictly stronger than two separate tests.
  • DurableExecutionManagerTest.handleRecoveryCallsRebuildState verifies the call contract; mutation observability for the rebuildState side effect is covered by operator-level integration tests against real stores (InMemoryActionStateStore.rebuildState is a no-op).
  • ActionTaskContextManagerTest.transferContextsCopiesMemoryAndContinuationToNewTask covers the Java path; the Python path requires Pemja and is covered by ActionExecutionOperatorTest's Python flows.
  • EventRouterTest.processEligibleWatermarksDrainsInArrivalOrder drives SegmentedQueue across two segments and removes keys in completion order that differs from arrival; ordered assertion containsExactly(WM1, WM2) catches both wrong-order draining and skipped watermarks.

API

No public API changes. Three new @VisibleForTesting package-private members in production code (one accessor on DurableExecutionManager, one accessor + one constructor overload on EventRouter). The existing EventRouter(AgentPlan, boolean) constructor signature is unchanged; it delegates to the new 3-arg form. createEventLogger is converted from instance to static (required by Java for chained this(...) constructors; the original body uses only its parameter).

Documentation

  • doc-needed
  • doc-not-needed
  • doc-included

@github-actions github-actions Bot added doc-not-needed Your PR changes do not impact docs fixVersion/0.3.0 The feature or bug should be implemented/fixed in the 0.3.0 version. priority/major Default priority of the PR or issue. labels May 12, 2026
weiqingy added 6 commits May 12, 2026 21:05
Adds three narrow seams to production code so that follow-up commits
covering DurableExecutionManager, EventRouter, and
ActionTaskContextManager have observable targets for their contract
assertions. No behaviour change for production callers.

* DurableExecutionManager.getCheckpointIdToSeqNums() accessor —
  exposes the in-heap snapshot map used by snapshotState and
  notifyCheckpointComplete. Follows the existing
  getActionStateStore() pattern.
* EventRouter.addEventListener(EventListener) accessor — current
  eventListeners list has no public registration path; required by
  listener-notification tests.
* EventRouter constructor refactor — existing 2-arg constructor
  delegates to a new 3-arg @VisibleForTesting constructor that
  accepts a pre-built EventLogger. Allows tests to inject a
  mock(EventLogger.class) and avoid file I/O. createEventLogger is
  converted from instance method to static (required by Java for
  use in a chained this(...) call).

Part of apache#646.
Closes the snapshot/recovery + pruning coverage gap deferred from
PR apache#546 (issue apache#646, row 2 of the deferred-coverage table).

* snapshotCapturesKeySequenceNumbers — mocks
  KeyedStateBackend.applyToAllKeys via Mockito doAnswer and verifies
  the in-heap checkpointIdToSeqNums map is populated per-key.
* notifyPrunesNotifiedCheckpointAndPreservesOthers — populates two
  checkpoints in the map ({1000L: {k1, k2}} and {1001L: {k3}}),
  notifies completion of 1000L, and asserts: k1 and k2 are pruned,
  k3 survives (selectivity holds), the 1000-entry is removed, the
  1001-entry remains. Combines multi-key (loop body) and
  multi-checkpoint (isolation) concerns into one strictly stronger
  assertion than two separate tests.
* handleRecoveryCallsRebuildState — spy on InMemoryActionStateStore;
  mock OperatorStateBackend.getUnionListState to return a marker
  list; verify rebuildState(markers) is invoked with the expected
  payload.

The handleRecovery test verifies the call contract; mutation
observability for the rebuildState side effect is covered by
operator-level integration tests against real stores
(InMemoryActionStateStore.rebuildState is a no-op).

Uses the @VisibleForTesting getCheckpointIdToSeqNums() accessor
added in the previous commit. No production-code changes.

Part of apache#646.
Closes the listener-notification + watermark-draining coverage gap
deferred from PR apache#546 (issue apache#646, row 3 of the deferred-coverage
table).

* notifyEventProcessedNotifiesAllListeners — multi-listener fan-out
  via the @VisibleForTesting addEventListener seam.
* notifyEventProcessedIncrementsMetric — spy on BuiltInMetrics
  verifies markEventProcessed() is invoked once per event.
* notifyEventProcessedAppendsAndFlushesLogger — Mockito InOrder
  verifies append() is called before flush(); logger injected via
  the 3-arg @VisibleForTesting constructor as a mock(EventLogger).
* processEligibleWatermarksDrainsInArrivalOrder — drives
  SegmentedQueue across two segments and removes keys in completion
  order that differs from arrival; asserts ordered emit via
  containsExactly(WM1, WM2).
* processEligibleWatermarksBlocksWhileSegmentHasKeys — confirms no
  watermark is emitted while a key in its segment is still in flight,
  then verifies emit after the key is removed.

Extracts a private static makeMetrics() helper for the 5 tests that
need BuiltInMetrics. Uses the addEventListener and 3-arg constructor
seams added in commit 1. No production-code changes.

Part of apache#646.
Closes the memory-context lifecycle coverage gap deferred from
PR apache#546 (issue apache#646, row 4 of the deferred-coverage table). Maps
the issue body's four phases (create → transfer → complete →
cleanup) to concrete code points.

* createAndSetRunnerContextBuildsFreshMemoryContextOnFirstCall —
  first call wires a JavaRunnerContextImpl and creates a fresh
  MemoryContext.
* createAndSetRunnerContextReusesExistingMemoryContext — second
  call reuses the map entry that transferContexts populated.
* transferContextsCopiesMemoryAndContinuationToNewTask — transfer
  populates the to-task and leaves the from-task's manager-map
  entries untouched. Asserts the remove-then-null contract for the
  memory context. Documents the shared-responsibility cleanup
  model: transferContexts is a pure copy; the operator drives
  from-task cleanup at ActionExecutionOperator.java:366-369.
* transferContextsRoutesDurableContextThroughManager — spy on
  DurableExecutionManager verifies the durable-context branch
  routes through putDurableContext(toTask, ...).
* closeIsIdempotent — close() can be called twice without
  exception, matching ContinuationActionExecutor.shutdownNow()
  semantics.

Extracts a private static invokeCreateAndSetRunnerContext helper
(4 use sites) to factor mocked-collaborator boilerplate. No
production-code changes; uses existing public/package-private API.

Part of apache#646.
@weiqingy

Copy link
Copy Markdown
Collaborator Author

Hey @wenjin272, this PR follows up on the review comment about adding more tests for #546. Could you please review it when you get a chance? Thanks!

@weiqingy

Copy link
Copy Markdown
Collaborator Author

The 1 remaining failing check is unrelated to this PR — it's an httpx.ReadTimeout against the external chat model service.

  • cross-language [python-3.12] [java-21]: ChatModelCrossLanguageTest.testChatModeIntegration — Caused by: pemja.core.PythonException: <class 'httpx.ReadTimeout'>: timed out. Pure network/HTTP-client timeout, no assertion failure, no code path touched by this PR. Evidence: job log.

The same flake hit this PR earlier (run 25716424884) and seems a known environmental flake in the cross-language integration suite. All other 22 checks pass.

@wenjin272 wenjin272 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@wenjin272
wenjin272 merged commit 6142e5e into apache:main May 14, 2026
23 checks passed
weiqingy added a commit to weiqingy/flink-agents that referenced this pull request May 15, 2026
…ntries

Issue apache#665. When Flink aborts a checkpoint, it calls notifyCheckpointAborted
instead of notifyCheckpointComplete. The DurableExecutionManager only handled
the complete path, so the per-checkpoint sequence-number entry recorded by
snapshotLastCompletedSequenceNumbers was never released for aborted
checkpoints. Under sustained abort pressure (timeouts, alignment failures,
backend pressure), checkpointIdToSeqNums grew unboundedly.

Changes:

- Add DurableExecutionManager.notifyCheckpointAborted(long): removes the
  entry from checkpointIdToSeqNums, guarded by the same actionStateStore
  != null check as notifyCheckpointComplete. Does NOT prune durable action
  state — the aborted checkpoint's writes were never committed, so the
  prior committed checkpoint's recovery state is still load-bearing and
  must not be pruned.

- Add ActionExecutionOperator.notifyCheckpointAborted(long): thin override
  that delegates to the manager and then calls super, mirroring the
  existing notifyCheckpointComplete override.

- Extend the symmetric-guard invariant javadoc on
  snapshotLastCompletedSequenceNumbers and notifyCheckpointComplete to
  name both release paths (complete OR abort). The actionStateStore !=
  null guard now lives on three methods; the cross-linked javadoc makes
  that explicit and cites issues apache#645 and apache#665.

- Three new DurableExecutionManagerTest cases (using the existing
  getCheckpointIdToSeqNums() @VisibleForTesting accessor introduced in
  apache#659):
  * notifyAbortedRemovesEntryWithoutPruning — entry released, durable
    state untouched (verified against a real InMemoryActionStateStore so
    wrongful pruning would be observable).
  * completedAndAbortedInterleavedKeepsInFlightEntries — three in-flight
    checkpoints, one completes (state pruned), one aborts (state preserved),
    one remains.
  * noStoreModeNotifyCheckpointAbortedIsNoOp — symmetric null-store no-op
    coverage matching the existing notifyCheckpointComplete null-store case.
weiqingy added a commit to weiqingy/flink-agents that referenced this pull request May 15, 2026
…ntries

Issue apache#665. When Flink aborts a checkpoint, it calls notifyCheckpointAborted
instead of notifyCheckpointComplete. The DurableExecutionManager only handled
the complete path, so the per-checkpoint sequence-number entry recorded by
snapshotLastCompletedSequenceNumbers was never released for aborted
checkpoints. Under sustained abort pressure (timeouts, alignment failures,
backend pressure), checkpointIdToSeqNums grew unboundedly.

Changes:

- Add DurableExecutionManager.notifyCheckpointAborted(long): removes the
  entry from checkpointIdToSeqNums, guarded by the same actionStateStore
  != null check as notifyCheckpointComplete. Does NOT prune durable action
  state — the aborted checkpoint's writes were never committed, so the
  prior committed checkpoint's recovery state is still load-bearing and
  must not be pruned.

- Add ActionExecutionOperator.notifyCheckpointAborted(long): thin override
  that delegates to the manager and then calls super, mirroring the
  existing notifyCheckpointComplete override.

- Extend the symmetric-guard invariant javadoc on
  snapshotLastCompletedSequenceNumbers and notifyCheckpointComplete to
  name both release paths (complete OR abort). The actionStateStore !=
  null guard now lives on three methods; the cross-linked javadoc makes
  that explicit and cites issues apache#645 and apache#665.

- Three new DurableExecutionManagerTest cases (using the existing
  getCheckpointIdToSeqNums() @VisibleForTesting accessor introduced in
  apache#659):
  * notifyAbortedRemovesEntryWithoutPruning — entry released, durable
    state untouched (verified against a real InMemoryActionStateStore so
    wrongful pruning would be observable).
  * completedAndAbortedInterleavedKeepsInFlightEntries — three in-flight
    checkpoints, one completes (state pruned), one aborts (state preserved),
    one remains.
  * noStoreModeNotifyCheckpointAbortedIsNoOp — symmetric null-store no-op
    coverage matching the existing notifyCheckpointComplete null-store case.
wenjin272 pushed a commit that referenced this pull request May 16, 2026
…ntries (#667)

Issue #665. When Flink aborts a checkpoint, it calls notifyCheckpointAborted
instead of notifyCheckpointComplete. The DurableExecutionManager only handled
the complete path, so the per-checkpoint sequence-number entry recorded by
snapshotLastCompletedSequenceNumbers was never released for aborted
checkpoints. Under sustained abort pressure (timeouts, alignment failures,
backend pressure), checkpointIdToSeqNums grew unboundedly.

Changes:

- Add DurableExecutionManager.notifyCheckpointAborted(long): removes the
  entry from checkpointIdToSeqNums, guarded by the same actionStateStore
  != null check as notifyCheckpointComplete. Does NOT prune durable action
  state — the aborted checkpoint's writes were never committed, so the
  prior committed checkpoint's recovery state is still load-bearing and
  must not be pruned.

- Add ActionExecutionOperator.notifyCheckpointAborted(long): thin override
  that delegates to the manager and then calls super, mirroring the
  existing notifyCheckpointComplete override.

- Extend the symmetric-guard invariant javadoc on
  snapshotLastCompletedSequenceNumbers and notifyCheckpointComplete to
  name both release paths (complete OR abort). The actionStateStore !=
  null guard now lives on three methods; the cross-linked javadoc makes
  that explicit and cites issues #645 and #665.

- Three new DurableExecutionManagerTest cases (using the existing
  getCheckpointIdToSeqNums() @VisibleForTesting accessor introduced in
  #659):
  * notifyAbortedRemovesEntryWithoutPruning — entry released, durable
    state untouched (verified against a real InMemoryActionStateStore so
    wrongful pruning would be observable).
  * completedAndAbortedInterleavedKeepsInFlightEntries — three in-flight
    checkpoints, one completes (state pruned), one aborts (state preserved),
    one remains.
  * noStoreModeNotifyCheckpointAbortedIsNoOp — symmetric null-store no-op
    coverage matching the existing notifyCheckpointComplete null-store case.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

doc-not-needed Your PR changes do not impact docs fixVersion/0.3.0 The feature or bug should be implemented/fixed in the 0.3.0 version. priority/major Default priority of the PR or issue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants