|
18 | 18 | package org.apache.flink.agents.runtime.operator; |
19 | 19 |
|
20 | 20 | import org.apache.flink.agents.api.InputEvent; |
| 21 | +import org.apache.flink.agents.plan.AgentPlan; |
21 | 22 | import org.apache.flink.agents.plan.actions.Action; |
| 23 | +import org.apache.flink.agents.runtime.ResourceCache; |
| 24 | +import org.apache.flink.agents.runtime.actionstate.ActionState; |
| 25 | +import org.apache.flink.agents.runtime.actionstate.InMemoryActionStateStore; |
22 | 26 | import org.apache.flink.agents.runtime.async.ContinuationContext; |
| 27 | +import org.apache.flink.agents.runtime.context.JavaRunnerContextImpl; |
| 28 | +import org.apache.flink.agents.runtime.context.RunnerContextImpl; |
| 29 | +import org.apache.flink.agents.runtime.memory.MemoryObjectImpl; |
| 30 | +import org.apache.flink.agents.runtime.metrics.FlinkAgentsMetricGroupImpl; |
| 31 | +import org.apache.flink.api.common.state.MapState; |
23 | 32 | import org.junit.jupiter.api.Test; |
24 | 33 |
|
| 34 | +import java.util.HashMap; |
| 35 | + |
25 | 36 | import static org.assertj.core.api.Assertions.assertThat; |
26 | 37 | import static org.assertj.core.api.Assertions.assertThatThrownBy; |
| 38 | +import static org.mockito.ArgumentMatchers.any; |
| 39 | +import static org.mockito.ArgumentMatchers.eq; |
| 40 | +import static org.mockito.Mockito.RETURNS_DEEP_STUBS; |
| 41 | +import static org.mockito.Mockito.mock; |
| 42 | +import static org.mockito.Mockito.spy; |
| 43 | +import static org.mockito.Mockito.verify; |
27 | 44 |
|
28 | 45 | /** Contract tests for {@link ActionTaskContextManager}. */ |
29 | 46 | class ActionTaskContextManagerTest { |
@@ -73,4 +90,166 @@ void createOrGetRunnerContextThrowsWhenPythonContextRequestedButNull() throws Ex |
73 | 90 | .hasMessageContaining("PythonRunnerContextImpl has not been initialized"); |
74 | 91 | } |
75 | 92 | } |
| 93 | + |
| 94 | + @Test |
| 95 | + void createAndSetRunnerContextBuildsFreshMemoryContextOnFirstCall() throws Exception { |
| 96 | + try (ActionTaskContextManager mgr = new ActionTaskContextManager(1)) { |
| 97 | + ActionTask t = new JavaActionTask("k", new InputEvent(1L), TestActions.noopAction()); |
| 98 | + invokeCreateAndSetRunnerContext(mgr, t); |
| 99 | + |
| 100 | + // Production path: createAndSetRunnerContext at ActionTaskContextManager.java:210-218 |
| 101 | + // — the else branch builds a fresh MemoryContext when the map has no entry. |
| 102 | + assertThat(t.getRunnerContext()).isInstanceOf(JavaRunnerContextImpl.class); |
| 103 | + assertThat(t.getRunnerContext().getMemoryContext()).isNotNull(); |
| 104 | + } |
| 105 | + } |
| 106 | + |
| 107 | + @Test |
| 108 | + void createAndSetRunnerContextReusesExistingMemoryContext() throws Exception { |
| 109 | + try (ActionTaskContextManager mgr = new ActionTaskContextManager(1)) { |
| 110 | + Action action = TestActions.noopAction(); |
| 111 | + ActionTask from = new JavaActionTask("k", new InputEvent(1L), action); |
| 112 | + ActionTask to = new JavaActionTask("k", new InputEvent(2L), action); |
| 113 | + |
| 114 | + // Step 1: createAndSetRunnerContext(from) — runner context now carries a fresh |
| 115 | + // MemoryContext, but the map (actionTaskMemoryContexts) is still empty (production |
| 116 | + // code at lines 210-218 only reads from the map, never writes). |
| 117 | + invokeCreateAndSetRunnerContext(mgr, from); |
| 118 | + RunnerContextImpl.MemoryContext fromMemCtx = from.getRunnerContext().getMemoryContext(); |
| 119 | + assertThat(fromMemCtx).isNotNull(); |
| 120 | + |
| 121 | + // Step 2: transferContexts populates the map entry for `to` via the private |
| 122 | + // putMemoryContext (ActionTaskContextManager.java:266-286). DEM null is OK because |
| 123 | + // from has no DurableExecutionContext. |
| 124 | + mgr.transferContexts(from, to, new DurableExecutionManager(null)); |
| 125 | + |
| 126 | + // Step 3: createAndSetRunnerContext(to) — production code at lines 211-212 reads |
| 127 | + // the map for `to` and reuses fromMemCtx (the if-branch of the reuse check). |
| 128 | + invokeCreateAndSetRunnerContext(mgr, to); |
| 129 | + |
| 130 | + // The runner context is shared (single Java JavaRunnerContextImpl instance), but |
| 131 | + // its memoryContext was switched to the entry that was in the map for `to`. Verify |
| 132 | + // the same MemoryContext instance is now wired on the runner context. |
| 133 | + assertThat(to.getRunnerContext().getMemoryContext()).isSameAs(fromMemCtx); |
| 134 | + } |
| 135 | + } |
| 136 | + |
| 137 | + @Test |
| 138 | + void transferContextsCopiesMemoryAndContinuationToNewTask() throws Exception { |
| 139 | + try (ActionTaskContextManager mgr = new ActionTaskContextManager(1)) { |
| 140 | + Action action = TestActions.noopAction(); |
| 141 | + ActionTask from = new JavaActionTask("k", new InputEvent(1L), action); |
| 142 | + ActionTask to = new JavaActionTask("k", new InputEvent(2L), action); |
| 143 | + |
| 144 | + // Populate `from`'s runner context with a MemoryContext and ContinuationContext. |
| 145 | + invokeCreateAndSetRunnerContext(mgr, from); |
| 146 | + RunnerContextImpl.MemoryContext fromMemCtx = from.getRunnerContext().getMemoryContext(); |
| 147 | + assertThat(fromMemCtx).isNotNull(); |
| 148 | + |
| 149 | + // transferContexts (ActionTaskContextManager.java:266-286) copies but does NOT |
| 150 | + // remove from source. The from-side continuation map is never populated (the |
| 151 | + // continuation lives on from's runner context until transfer copies it over for |
| 152 | + // `to`). Operator-side cleanup of `from`'s entries is the operator's |
| 153 | + // responsibility — see ActionExecutionOperator.java:366-369. |
| 154 | + mgr.transferContexts(from, to, new DurableExecutionManager(null)); |
| 155 | + |
| 156 | + // (a) The memory context entry for `to` is the same instance fromTask holds. |
| 157 | + RunnerContextImpl.MemoryContext toMemCtx = mgr.removeMemoryContext(to); |
| 158 | + assertThat(toMemCtx).isSameAs(fromMemCtx); |
| 159 | + |
| 160 | + // After remove, the map no longer has `to`'s entry. |
| 161 | + assertThat(mgr.removeMemoryContext(to)).isNull(); |
| 162 | + |
| 163 | + // (b) Continuation context routed to `to`. |
| 164 | + assertThat(mgr.hasContinuationContext(to)).isTrue(); |
| 165 | + |
| 166 | + // (c) The `from`-side continuation map entry was never populated by the transfer |
| 167 | + // — the source carries its continuation on its runner context, not on the |
| 168 | + // manager's map. |
| 169 | + assertThat(mgr.hasContinuationContext(from)).isFalse(); |
| 170 | + } |
| 171 | + } |
| 172 | + |
| 173 | + @Test |
| 174 | + void transferContextsRoutesDurableContextThroughManager() throws Exception { |
| 175 | + try (ActionTaskContextManager mgr = new ActionTaskContextManager(1)) { |
| 176 | + Action action = TestActions.noopAction(); |
| 177 | + InputEvent event = new InputEvent(1L); |
| 178 | + ActionTask from = new JavaActionTask("k", event, action); |
| 179 | + ActionTask to = new JavaActionTask("k", new InputEvent(2L), action); |
| 180 | + |
| 181 | + invokeCreateAndSetRunnerContext(mgr, from); |
| 182 | + |
| 183 | + // Spy on DEM backed by a real InMemoryActionStateStore so spied internals don't |
| 184 | + // NPE. The store doesn't really need to be exercised — we only verify the |
| 185 | + // putDurableContext call site at ActionTaskContextManager.java:271-273. |
| 186 | + DurableExecutionManager spyDem = |
| 187 | + spy(new DurableExecutionManager(new InMemoryActionStateStore(false))); |
| 188 | + |
| 189 | + // Attach a DurableExecutionContext to `from`'s runner context. The persister is |
| 190 | + // the DEM itself (DurableExecutionManager implements ActionStatePersister at |
| 191 | + // DurableExecutionManager.java:78). ActionState ctor needs an Event so getCallResults() |
| 192 | + // returns a non-null list inside the DurableExecutionContext ctor. |
| 193 | + ActionState actionState = new ActionState(event); |
| 194 | + RunnerContextImpl.DurableExecutionContext durableCtx = |
| 195 | + new RunnerContextImpl.DurableExecutionContext( |
| 196 | + "k", 0L, action, event, actionState, spyDem); |
| 197 | + from.getRunnerContext().setDurableExecutionContext(durableCtx); |
| 198 | + |
| 199 | + mgr.transferContexts(from, to, spyDem); |
| 200 | + |
| 201 | + // The durable-context branch routes via the DEM's putDurableContext, satisfying |
| 202 | + // the no-manager-to-manager-references design constraint (DEM passed as a |
| 203 | + // parameter, not held as a field). |
| 204 | + verify(spyDem) |
| 205 | + .putDurableContext( |
| 206 | + eq(to), any(RunnerContextImpl.DurableExecutionContext.class)); |
| 207 | + } |
| 208 | + } |
| 209 | + |
| 210 | + @Test |
| 211 | + void closeIsIdempotent() throws Exception { |
| 212 | + // Not using try-with-resources here because we want to call close() explicitly twice. |
| 213 | + ActionTaskContextManager mgr = new ActionTaskContextManager(1); |
| 214 | + ActionTask t = new JavaActionTask("k", new InputEvent(1L), TestActions.noopAction()); |
| 215 | + invokeCreateAndSetRunnerContext(mgr, t); |
| 216 | + |
| 217 | + // First close() shuts down the runner context and the continuation executor |
| 218 | + // (ActionTaskContextManager.java:319-330). The second close() must be a no-op: |
| 219 | + // runnerContext is nulled and ContinuationActionExecutor.close() is backed by |
| 220 | + // ExecutorService.shutdownNow() which is itself idempotent. |
| 221 | + mgr.close(); |
| 222 | + mgr.close(); |
| 223 | + } |
| 224 | + |
| 225 | + /** |
| 226 | + * Shared helper: install a runner context on {@code task} using mocked collaborators. Used by |
| 227 | + * tests that need a fully wired runner context but do not care about the collaborator details. |
| 228 | + */ |
| 229 | + @SuppressWarnings("unchecked") |
| 230 | + private static void invokeCreateAndSetRunnerContext( |
| 231 | + ActionTaskContextManager mgr, ActionTask task) { |
| 232 | + AgentPlan plan = newEmptyAgentPlan(); |
| 233 | + ResourceCache cache = mock(ResourceCache.class); |
| 234 | + FlinkAgentsMetricGroupImpl metricGroup = |
| 235 | + mock(FlinkAgentsMetricGroupImpl.class, RETURNS_DEEP_STUBS); |
| 236 | + MapState<String, MemoryObjectImpl.MemoryItem> sensoryMem = mock(MapState.class); |
| 237 | + MapState<String, MemoryObjectImpl.MemoryItem> shortTermMem = mock(MapState.class); |
| 238 | + mgr.createAndSetRunnerContext( |
| 239 | + task, |
| 240 | + "k", |
| 241 | + plan, |
| 242 | + cache, |
| 243 | + metricGroup, |
| 244 | + "job", |
| 245 | + () -> {}, |
| 246 | + sensoryMem, |
| 247 | + shortTermMem, |
| 248 | + /* pythonRunnerContext */ null, |
| 249 | + /* longTermMemory */ null); |
| 250 | + } |
| 251 | + |
| 252 | + private static AgentPlan newEmptyAgentPlan() { |
| 253 | + return new AgentPlan(new HashMap<>(), new HashMap<>()); |
| 254 | + } |
76 | 255 | } |
0 commit comments