feat: add endpoint to list all iterations of a container step
A Loop/Iterator container step only ever exposes activeInnerExecutionId (the
current iteration's child), overwritten on every iteration. Past iterations'
child executions are never deleted from the DB, but there was no way to
enumerate them - only individually fetchable by id if a caller already had
it (e.g. from the event log).
Adds GET /executions/{id}/node/{stepId}/iterations, returning every child
execution created for that container step (one per iteration for
Loop/Iterator, one per run for GenericContainer), ordered by iteration
index. Backed by a new repository query
(findByParentExecutionIdAndParentStepIdOrderByParentIterationIndexAscCreationTimeAsc)
and ExecutionsService.getContainerIterationsByOwner(), which validates the
step exists and is actually a container before querying.
Verified end to end against the running service on the "test cv ranking
iterator" flow: the endpoint lists all 3 iterations in order with distinct
ids and SUCCESS status, returns 400 for a non-container step and 404 for an
unknown step id, and each listed iteration remains individually fetchable
via the existing GET /executions/{id}.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
c6efa32c79
commit
f28fe0312e
|
|
@ -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<ExecutionView> 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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -367,6 +367,34 @@ public class ExecutionsService {
|
|||
public record ExecutionGroup(String groupId, List<ExecutionObject> 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<ExecutionObject> 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<ExecutionEntity> children = executionRepository
|
||||
.findByParentExecutionIdAndParentStepIdOrderByParentIterationIndexAscCreationTimeAsc(executionId, stepId);
|
||||
List<ExecutionObject> 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);
|
||||
|
|
|
|||
|
|
@ -21,5 +21,7 @@ public interface ExecutionRepository extends JpaRepository<ExecutionEntity, Stri
|
|||
List<ExecutionEntity> findBySourceFlowIdAndOwnerAndExecutionKindOrderByRunNumberAscCreationTimeAsc(
|
||||
String sourceFlowId, String owner, ExecutionKind executionKind);
|
||||
List<ExecutionEntity> findByParentExecutionIdOrderByCreationTimeAsc(String parentExecutionId);
|
||||
List<ExecutionEntity> findByParentExecutionIdAndParentStepIdOrderByParentIterationIndexAscCreationTimeAsc(
|
||||
String parentExecutionId, String parentStepId);
|
||||
void deleteByOwner(String owner);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1671,6 +1671,43 @@ public class ExecutionTest {
|
|||
&& event.getDetails().get("iterationIndex") != null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getContainerIterationsByOwnerListsOneChildPerIterationInOrder() {
|
||||
Block<LLMBlockType> internalBlock = llmBlockFactory.create(LLMBlockConfiguration.builder()
|
||||
.name("Internal LLM")
|
||||
.llmDescriptor(llmBrick)
|
||||
.prompt("Hello, ${{name}}!")
|
||||
.build());
|
||||
|
||||
Container<IteratorContainerType> 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<ExecutionObject> 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<LLMBlockType> internalBlock = llmBlockFactory.create(LLMBlockConfiguration.builder()
|
||||
|
|
|
|||
Loading…
Reference in New Issue