@@ -61,13 +61,16 @@ public final class NodeExecutionState {
6161 // The parent's state when this run's output is also the parent's.
6262 private final @ Nullable NodeExecutionState outputParent ;
6363 private final String runId ;
64+ private final int attemptCount ;
6465 private final String nodePath ;
6566 private final ImmutableList <String > outputFor ;
6667 private final Map <String , @ Nullable Object > resumeInputs ;
67- private final ConcurrentMap <String , Integer > childRunCounts = new ConcurrentHashMap <>() ;
68- private final ConcurrentMap <String , SingleSubject <Context >> childRuns = new ConcurrentHashMap <>() ;
69- // The paths of the node runs started but not finished in this turn, shared under one root.
68+ private final ConcurrentMap <String , Integer > childRunCounts ;
69+ private final ConcurrentMap <String , SingleSubject <Context >> childRuns ;
70+ // Paths of runs started but not finished in this turn, shared under one root and its attempts .
7071 private final Set <String > unfinishedNodePaths ;
72+ // Paths of runs that failed this turn and have not finished cleanly since, shared the same way.
73+ private final Set <String > failedNodePaths ;
7174 private final Set <String > interruptIds = new CopyOnWriteArraySet <>();
7275 private final AtomicReference <OutputState > outputState = new AtomicReference <>(NO_OUTPUT );
7376 // Set once the node drops its own output because a useAsOutput child holds the output.
@@ -100,9 +103,19 @@ private NodeExecutionState(Builder builder) {
100103 ? ImmutableList .<String >builder ().add (nodePath ).addAll (outputParent .outputFor ).build ()
101104 : ImmutableList .of (nodePath );
102105 this .eventAuthor = parentState == null ? "" : parentState .eventAuthor ;
106+ NodeExecutionState previousAttempt = builder .previousAttempt ;
107+ this .attemptCount = previousAttempt == null ? 1 : previousAttempt .attemptCount + 1 ;
108+ NodeExecutionState turnRecords = parentState != null ? parentState : previousAttempt ;
103109 this .unfinishedNodePaths =
104- parentState == null ? ConcurrentHashMap .newKeySet () : parentState .unfinishedNodePaths ;
110+ turnRecords == null ? ConcurrentHashMap .newKeySet () : turnRecords .unfinishedNodePaths ;
111+ this .failedNodePaths =
112+ turnRecords == null ? ConcurrentHashMap .newKeySet () : turnRecords .failedNodePaths ;
105113 this .resumeInputs = Collections .unmodifiableMap (new LinkedHashMap <>(builder .resumeInputs ));
114+ // A retried workflow restarts its node numbering, so its finished nodes replay.
115+ boolean continuesChildRuns = previousAttempt != null && !(node instanceof Workflow );
116+ this .childRunCounts =
117+ continuesChildRuns ? previousAttempt .childRunCounts : new ConcurrentHashMap <>();
118+ this .childRuns = continuesChildRuns ? previousAttempt .childRuns : new ConcurrentHashMap <>();
106119 }
107120
108121 /** Returns a builder for the state of one node run. */
@@ -128,6 +141,11 @@ public String runId() {
128141 return runId ;
129142 }
130143
144+ /** Returns the number of this attempt at running the node, counting from 1. */
145+ public int attemptCount () {
146+ return attemptCount ;
147+ }
148+
131149 /** Returns the context of the node that ran this node, or empty if this is a root node. */
132150 public Optional <Context > parent () {
133151 return Optional .ofNullable (parent );
@@ -266,6 +284,11 @@ void markStarted() {
266284 /** Records that this run finished, with or without a result. */
267285 void markFinished () {
268286 unfinishedNodePaths .remove (nodePath );
287+ if (failure == null ) {
288+ failedNodePaths .remove (nodePath );
289+ } else {
290+ failedNodePaths .add (nodePath );
291+ }
269292 }
270293
271294 /**
@@ -277,6 +300,14 @@ boolean childCanceledMidRun(String childKey) {
277300 return unfinishedNodePaths .contains (nodePath + "/" + childKey );
278301 }
279302
303+ /**
304+ * Returns whether this run's child {@code childKey} failed in this turn and has not finished
305+ * cleanly since. A child that takes on its own child's failure leaves no error event to show it.
306+ */
307+ boolean childFailedThisTurn (String childKey ) {
308+ return failedNodePaths .contains (nodePath + "/" + childKey );
309+ }
310+
280311 /**
281312 * Returns the states of this node's children from earlier turns, keyed by {@code name@runId},
282313 * which the first call reconstructs from the events of {@code invocationContext}.
@@ -457,6 +488,7 @@ static final class Builder {
457488 private @ Nullable String runId ;
458489 private boolean useAsOutput ;
459490 private Map <String , @ Nullable Object > resumeInputs = ImmutableMap .of ();
491+ private @ Nullable NodeExecutionState previousAttempt ;
460492
461493 private Builder () {}
462494
@@ -505,6 +537,17 @@ Builder resumeInputs(Map<String, @Nullable Object> resumeInputs) {
505537 return this ;
506538 }
507539
540+ /**
541+ * Sets the state of the run's previous attempt, or null for a first attempt. A later attempt's
542+ * count is one higher. It shares the previous attempt's records of unfinished and failed runs
543+ * and, unless the node is a workflow, its child run IDs and child runs.
544+ */
545+ @ CanIgnoreReturnValue
546+ Builder previousAttempt (@ Nullable NodeExecutionState previousAttempt ) {
547+ this .previousAttempt = previousAttempt ;
548+ return this ;
549+ }
550+
508551 /**
509552 * Builds the state.
510553 *
0 commit comments