diff --git a/src/main/java/it/cnr/isti/workflow/manager/controllers/ExecutionsController.java b/src/main/java/it/cnr/isti/workflow/manager/controllers/ExecutionsController.java index 60902d8..8800284 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/controllers/ExecutionsController.java +++ b/src/main/java/it/cnr/isti/workflow/manager/controllers/ExecutionsController.java @@ -219,6 +219,21 @@ public class ExecutionsController { .toList(); } + @GetMapping(path = "{id}/node/{stepId}/iterations") + @Operation(summary = "Retrieves container iterations", + description = "Retrieves every child execution created for a container step (one per Loop/Iterator " + + "iteration, or one for a GenericContainer run), ordered by iteration index. The step itself " + + "only exposes the currently active child; this is the only way to see past iterations.") + public List getContainerIterations(@PathVariable String id, @PathVariable String stepId, + @AuthenticationPrincipal LoginEntity userDetails) { + if (userDetails == null) { + throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Authentication required"); + } + return executionService.getContainerIterationsByOwner(id, stepId, userDetails.getUsername()).stream() + .map(ExecutionView::fromExecution) + .toList(); + } + /** * Deletes an execution with the specified ID. * diff --git a/src/main/java/it/cnr/isti/workflow/manager/executions/ExecutionsService.java b/src/main/java/it/cnr/isti/workflow/manager/executions/ExecutionsService.java index 8c10362..1d76e20 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/executions/ExecutionsService.java +++ b/src/main/java/it/cnr/isti/workflow/manager/executions/ExecutionsService.java @@ -367,6 +367,34 @@ public class ExecutionsService { public record ExecutionGroup(String groupId, List executions) { } + /** + * Lists every child execution created for a container step, one per + * iteration (Loop/Iterator) or run (GenericContainer), ordered by + * iteration index. The step only ever tracks the currently active child + * via its {@code ContainerContinuationSnapshot}; this method is the only + * way to see the full history, since past iterations' children are never + * deleted but aren't otherwise enumerable. + */ + @Transactional(readOnly = true) + public List getContainerIterationsByOwner(String executionId, String stepId, String owner) { + ExecutionObject parent = getExecutionByOwner(executionId, owner); + Step step = parent.getContext().getSteps().get(stepId); + if (step == null) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Step with id " + stepId + " not found"); + } + if (!(step.getNode() instanceof Container)) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Step with id " + stepId + " is not a container"); + } + List children = executionRepository + .findByParentExecutionIdAndParentStepIdOrderByParentIterationIndexAscCreationTimeAsc(executionId, stepId); + List loaded = children.stream() + .map(entity -> executions.computeIfAbsent(entity.getId(), ignored -> rebuildExecution(entity))) + .toList(); + loaded.forEach(execution -> touchExecution(execution.getId())); + evictIfNeeded(); + return loaded; + } + @Transactional public ExecutionObject rerunExecution(String id, String owner) { ExecutionObject source = getExecutionByOwner(id, owner); diff --git a/src/main/java/it/cnr/isti/workflow/manager/executions/repo/ExecutionRepository.java b/src/main/java/it/cnr/isti/workflow/manager/executions/repo/ExecutionRepository.java index ef847c0..5ecf4d2 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/executions/repo/ExecutionRepository.java +++ b/src/main/java/it/cnr/isti/workflow/manager/executions/repo/ExecutionRepository.java @@ -21,5 +21,7 @@ public interface ExecutionRepository extends JpaRepository findBySourceFlowIdAndOwnerAndExecutionKindOrderByRunNumberAscCreationTimeAsc( String sourceFlowId, String owner, ExecutionKind executionKind); List findByParentExecutionIdOrderByCreationTimeAsc(String parentExecutionId); + List findByParentExecutionIdAndParentStepIdOrderByParentIterationIndexAscCreationTimeAsc( + String parentExecutionId, String parentStepId); void deleteByOwner(String owner); } diff --git a/src/test/java/it/cnr/isti/workflow/manager/executions/ExecutionTest.java b/src/test/java/it/cnr/isti/workflow/manager/executions/ExecutionTest.java index d17f188..dd70c90 100644 --- a/src/test/java/it/cnr/isti/workflow/manager/executions/ExecutionTest.java +++ b/src/test/java/it/cnr/isti/workflow/manager/executions/ExecutionTest.java @@ -1671,6 +1671,43 @@ public class ExecutionTest { && event.getDetails().get("iterationIndex") != null)); } + @Test + public void getContainerIterationsByOwnerListsOneChildPerIterationInOrder() { + Block internalBlock = llmBlockFactory.create(LLMBlockConfiguration.builder() + .name("Internal LLM") + .llmDescriptor(llmBrick) + .prompt("Hello, ${{name}}!") + .build()); + + Container container = iteratorContainerFactory.create(IteratorContainerConfiguration.builder() + .name("Iterator") + .subFlow(FlowData.builder().block(internalBlock).build()) + .iterationInput("name") + .build()); + + FlowData flow = FlowData.builder().container(container).build(); + ExecutionObject execObject = executionsService.createExecution("Iterator flow", flow, "iterations-owner"); + + executionsService.prepareInput(execObject.getId(), container.getId(), "name", List.of("Alice", "Bob", "Carol")); + execObject = executionsService.startExecution(execObject.getId()); + execObject = awaitNonBlockingContainerCompletion(execObject); + assertEquals(ExecutionStatus.SUCCESS, execObject.getContext().getStatus()); + String parentExecutionId = execObject.getId(); + + List iterations = executionsService.getContainerIterationsByOwner( + parentExecutionId, container.getId(), "iterations-owner"); + + assertEquals(3, iterations.size()); + assertEquals(List.of(1, 2, 3), iterations.stream().map(ExecutionObject::getParentIterationIndex).toList()); + assertTrue(iterations.stream().allMatch( + iteration -> iteration.getContext().getStatus() == ExecutionStatus.SUCCESS)); + assertEquals(3, iterations.stream().map(ExecutionObject::getId).distinct().count()); + assertTrue(iterations.stream().allMatch( + iteration -> iteration.getExecutionKind() == ExecutionKind.SUBFLOW + && container.getId().equals(iteration.getParentStepId()) + && parentExecutionId.equals(iteration.getParentExecutionId()))); + } + @Test public void containerOutputRejectedByDownstreamInputFailsCleanlyInsteadOfHanging() { Block internalBlock = llmBlockFactory.create(LLMBlockConfiguration.builder()