|
10 | 10 | import io.kestra.core.queues.TestQueueFactory; |
11 | 11 | import io.kestra.core.repositories.ExecutionRepositoryInterface; |
12 | 12 | import io.kestra.core.runners.TestRunner; |
| 13 | +import io.kestra.core.utils.Await; |
13 | 14 | import io.kestra.core.utils.ListUtils; |
14 | 15 | import io.kestra.core.utils.TestsUtils; |
15 | 16 | import io.micronaut.inject.qualifiers.Qualifiers; |
16 | 17 | import io.micronaut.test.annotation.MicronautTestValue; |
| 18 | +import io.micronaut.test.context.TestContext; |
17 | 19 | import io.micronaut.test.extensions.junit5.MicronautJunit5Extension; |
18 | 20 | import lombok.extern.slf4j.Slf4j; |
19 | 21 | import org.junit.jupiter.api.extension.ExtensionContext; |
20 | 22 | import org.junit.platform.commons.support.AnnotationSupport; |
21 | 23 |
|
22 | | -import java.util.Collections; |
23 | | -import java.util.ConcurrentModificationException; |
24 | | -import java.util.List; |
25 | | -import java.util.Optional; |
| 24 | +import java.time.Duration; |
| 25 | +import java.util.*; |
| 26 | +import java.util.concurrent.TimeoutException; |
26 | 27 |
|
27 | 28 | @Slf4j |
28 | 29 | public class KestraTestExtension extends MicronautJunit5Extension { |
@@ -75,58 +76,96 @@ public void beforeAll(ExtensionContext extensionContext) throws Exception { |
75 | 76 | } |
76 | 77 | } |
77 | 78 |
|
| 79 | + @Override |
| 80 | + public void beforeTestExecution(ExtensionContext context) throws Exception { |
| 81 | + super.beforeTestExecution(context); |
| 82 | + |
| 83 | + log.error("THREAD NAME IN BEFOREEACH: {}", Thread.currentThread().getName()); |
| 84 | + TestQueueFactory.testExecutions.set(new ArrayList<>()); |
| 85 | + } |
| 86 | + |
78 | 87 | @Override |
79 | 88 | public void afterTestExecution(ExtensionContext context) throws Exception { |
80 | 89 | super.afterTestExecution(context); |
81 | 90 |
|
82 | 91 | TestsUtils.queueConsumersCleanup(); |
83 | 92 |
|
84 | | - KestraTest kestraTest = context.getTestClass() |
85 | | - .orElseThrow() |
86 | | - .getAnnotation(KestraTest.class); |
87 | | - Optional<TestQueueFactory> testQueueFactory = Optional.of(applicationContext.containsBean(TestQueueFactory.class)).flatMap(contains -> contains ? Optional.of(applicationContext.getBean(TestQueueFactory.class)) : Optional.empty()); |
88 | | - List<Execution> testExecutions = testQueueFactory.map(TestQueueFactory::getTestExecutions).orElse(Collections.emptyList()); |
89 | | - if (!testExecutions.isEmpty() |
| 93 | + log.error("THREAD NAME IN AFTEREACH: {}", Thread.currentThread().getName()); |
| 94 | + List<Execution> executionsToKill = TestQueueFactory.testExecutions.get(); |
| 95 | + if (!executionsToKill.isEmpty() |
90 | 96 | && applicationContext.containsBean(ExecutionRepositoryInterface.class) |
91 | 97 | && applicationContext.containsBean(QueueInterface.class, Qualifiers.byName(QueueFactoryInterface.KILL_NAMED))) { |
92 | 98 | ExecutionRepositoryInterface executionRepository = applicationContext.getBean(ExecutionRepositoryInterface.class); |
93 | 99 | QueueInterface<ExecutionKilled> killQueue = applicationContext.getBean(QueueInterface.class, Qualifiers.byName(QueueFactoryInterface.KILL_NAMED)); |
94 | 100 |
|
95 | | - retryingExecutionKill(testExecutions, executionRepository, killQueue, 10); |
| 101 | + KestraTest kestraTest = context.getTestClass() |
| 102 | + .orElseThrow() |
| 103 | + .getAnnotation(KestraTest.class); |
| 104 | + // We only wait for KILLED state if the runner is started, otherwise we just emit the kill event and it may be processed upon starting a test with a runner |
| 105 | + List<Execution> killedExecutions = retryingExecutionKill(executionsToKill, executionRepository, killQueue, 10, kestraTest.startRunner()); |
96 | 106 |
|
97 | | - testExecutions.clear(); |
| 107 | + executionsToKill.removeIf(execution -> killedExecutions.stream().anyMatch(killedExecution -> |
| 108 | + Objects.equals(execution.getTenantId(), killedExecution.getTenantId()) |
| 109 | + && Objects.equals(execution.getId(), killedExecution.getId()) |
| 110 | + )); |
98 | 111 | } |
99 | 112 | } |
100 | 113 |
|
101 | 114 |
|
102 | | - private void retryingExecutionKill(List<Execution> testExecutions, ExecutionRepositoryInterface executionRepository, QueueInterface<ExecutionKilled> killQueue, int retriesLeft) throws InterruptedException { |
| 115 | + private List<Execution> retryingExecutionKill(List<Execution> testExecutions, ExecutionRepositoryInterface executionRepository, QueueInterface<ExecutionKilled> killQueue, int retriesLeft, boolean shouldWaitForKill) throws InterruptedException { |
103 | 116 | try { |
104 | | - ListUtils.distinctByKey( |
105 | | - testExecutions.stream().flatMap(launchedExecution -> executionRepository.findById(launchedExecution.getTenantId(), launchedExecution.getId()).stream()).toList(), |
106 | | - Execution::getId |
107 | | - ).stream().filter(inRepository -> inRepository.getState().isRunning() || inRepository.getState().isPaused() || inRepository.getState().isQueued()) |
108 | | - .forEach(inRepository -> { |
109 | | - log.warn("Execution {} is still running after test execution, killing it", inRepository.getId()); |
110 | | - try { |
111 | | - killQueue.emit(ExecutionKilledExecution.builder() |
112 | | - .tenantId(inRepository.getTenantId()) |
113 | | - .executionId(inRepository.getId()) |
114 | | - .state(ExecutionKilled.State.REQUESTED) |
115 | | - .isOnKillCascade(true) |
116 | | - .build() |
117 | | - ); |
118 | | - } catch (QueueException e) { |
119 | | - log.warn("Couldn't kill execution {} after test execution", inRepository.getId(), e); |
120 | | - } |
121 | | - }); |
| 117 | + List<Execution> runningExecutions = ListUtils.distinctByKey( |
| 118 | + testExecutions.stream().flatMap(launchedExecution -> executionRepository.findById(launchedExecution.getTenantId(), launchedExecution.getId()).stream()).toList(), |
| 119 | + Execution::getId |
| 120 | + ).stream().filter(inRepository -> !inRepository.getState().isTerminated()).toList(); |
| 121 | + |
| 122 | + runningExecutions.forEach(inRepository -> emitKillMessage(killQueue, inRepository)); |
| 123 | + |
| 124 | + if (shouldWaitForKill) { |
| 125 | + try { |
| 126 | + waitForKilled(executionRepository, runningExecutions); |
| 127 | + } catch (TimeoutException e) { |
| 128 | + log.warn("Some executions remained in KILLING", e); |
| 129 | + } |
| 130 | + } |
| 131 | + return runningExecutions; |
122 | 132 | } catch (ConcurrentModificationException e) { |
123 | 133 | // We intentionally don't use a CopyOnWriteArrayList to retry on concurrent modification exceptions to make sure to get rid of flakiness due to overflowing executions |
124 | 134 | if (retriesLeft <= 0) { |
125 | 135 | log.warn("Couldn't kill executions after test execution, due to concurrent modifications, this could impact further tests", e); |
126 | | - return; |
| 136 | + return Collections.emptyList(); |
127 | 137 | } |
128 | 138 | Thread.sleep(100); |
129 | | - retryingExecutionKill(testExecutions, executionRepository, killQueue, retriesLeft - 1); |
| 139 | + return retryingExecutionKill(testExecutions, executionRepository, killQueue, retriesLeft - 1, shouldWaitForKill); |
130 | 140 | } |
131 | 141 | } |
| 142 | + |
| 143 | + private void emitKillMessage(QueueInterface<ExecutionKilled> killQueue, Execution inRepository) { |
| 144 | + log.warn("Execution {} is still running after test execution, killing it", inRepository.getId()); |
| 145 | + try { |
| 146 | + killQueue.emit(ExecutionKilledExecution.builder() |
| 147 | + .tenantId(inRepository.getTenantId()) |
| 148 | + .executionId(inRepository.getId()) |
| 149 | + .state(ExecutionKilled.State.REQUESTED) |
| 150 | + .isOnKillCascade(true) |
| 151 | + .build() |
| 152 | + ); |
| 153 | + } catch (QueueException e) { |
| 154 | + log.warn("Couldn't kill execution {} after test execution", inRepository.getId(), e); |
| 155 | + } |
| 156 | + } |
| 157 | + |
| 158 | + private void waitForKilled(ExecutionRepositoryInterface executionRepository, List<Execution> runningExecutions) throws TimeoutException { |
| 159 | + Await.until(() -> runningExecutions.stream() |
| 160 | + .map(execution -> executionRepository.findById(execution.getTenantId(), execution.getId())) |
| 161 | + .allMatch(maybeExecution -> maybeExecution.map(inRepository -> { |
| 162 | + boolean terminated = inRepository.getState().isTerminated(); |
| 163 | + if (!terminated) { |
| 164 | + log.warn("Execution {} has a pending KILL request but is still in state {} ", inRepository.getId(), inRepository.getState().getCurrent()); |
| 165 | + } |
| 166 | + return terminated; |
| 167 | + }) |
| 168 | + .orElse(true)) |
| 169 | + , Duration.ofMillis(50), Duration.ofSeconds(10)); |
| 170 | + } |
132 | 171 | } |
0 commit comments