Skip to content

Commit d35dc9c

Browse files
authored
Merge pull request #224 from Netflix/ad/step-fatal-failure-signal
Allow a step to be fatally failed via a RetryArtifact in its output data
2 parents a5e788b + 0318fe9 commit d35dc9c

8 files changed

Lines changed: 220 additions & 4 deletions

File tree

maestro-common/src/main/java/com/netflix/maestro/models/artifact/Artifact.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
@JsonSubTypes.Type(name = "DYNAMIC_OUTPUT", value = DynamicOutputArtifact.class),
3434
@JsonSubTypes.Type(name = "KUBERNETES", value = KubernetesArtifact.class),
3535
@JsonSubTypes.Type(name = "HTTP", value = HttpArtifact.class),
36+
@JsonSubTypes.Type(name = "RETRY", value = RetryArtifact.class),
3637
})
3738
@SuppressWarnings("PMD.ImplicitFunctionalInterface")
3839
public interface Artifact {
@@ -58,7 +59,9 @@ enum Type {
5859
/** kubernetes artifact. */
5960
KUBERNETES(Constants.MAESTRO_PREFIX + "kubernetes"),
6061
/** http artifact. */
61-
HTTP(Constants.MAESTRO_PREFIX + "http");
62+
HTTP(Constants.MAESTRO_PREFIX + "http"),
63+
/** retry artifact. */
64+
RETRY(Constants.MAESTRO_PREFIX + "retry");
6265

6366
private final String key;
6467

@@ -152,4 +155,13 @@ default KubernetesArtifact asKubernetes() {
152155
default HttpArtifact asHttp() {
153156
throw new MaestroInternalError("Artifact type [%s] cannot be used as HTTP", getType());
154157
}
158+
159+
/**
160+
* Get Retry type artifact.
161+
*
162+
* @return concrete artifact object.
163+
*/
164+
default RetryArtifact asRetry() {
165+
throw new MaestroInternalError("Artifact type [%s] cannot be used as RETRY", getType());
166+
}
155167
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2024 Netflix, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
5+
* the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10+
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11+
* specific language governing permissions and limitations under the License.
12+
*/
13+
package com.netflix.maestro.models.artifact;
14+
15+
import com.fasterxml.jackson.annotation.JsonIgnore;
16+
import com.fasterxml.jackson.annotation.JsonInclude;
17+
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
18+
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
19+
import com.fasterxml.jackson.databind.annotation.JsonNaming;
20+
import lombok.Data;
21+
22+
/** Retry artifact for a step to influence whether the system retries it on failure. */
23+
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
24+
@JsonInclude(JsonInclude.Include.NON_NULL)
25+
@JsonPropertyOrder(
26+
value = {"retryable"},
27+
alphabetic = true)
28+
@Data
29+
public class RetryArtifact implements Artifact {
30+
private boolean retryable = true; // whether the system should retry the step on failure
31+
32+
@JsonIgnore
33+
@Override
34+
public RetryArtifact asRetry() {
35+
return this;
36+
}
37+
38+
@Override
39+
public Type getType() {
40+
return Type.RETRY;
41+
}
42+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright 2024 Netflix, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
5+
* the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10+
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11+
* specific language governing permissions and limitations under the License.
12+
*/
13+
package com.netflix.maestro.models.artifact;
14+
15+
import static org.junit.Assert.assertEquals;
16+
import static org.junit.Assert.assertFalse;
17+
import static org.junit.Assert.assertTrue;
18+
19+
import com.netflix.maestro.MaestroBaseTest;
20+
import org.junit.Test;
21+
22+
public class RetryArtifactTest extends MaestroBaseTest {
23+
@Test
24+
public void testRoundTripSerde() throws Exception {
25+
RetryArtifact artifact =
26+
loadObject("fixtures/artifact/sample-retry-artifact.json", RetryArtifact.class);
27+
assertEquals(
28+
artifact, MAPPER.readValue(MAPPER.writeValueAsString(artifact), RetryArtifact.class));
29+
}
30+
31+
@Test
32+
public void testDeserializeRetryable() throws Exception {
33+
Artifact artifact =
34+
MAPPER.readValue(
35+
"""
36+
{"type": "RETRY", "retryable": false}
37+
""", Artifact.class);
38+
assertEquals(Artifact.Type.RETRY, artifact.getType());
39+
assertFalse(artifact.asRetry().isRetryable());
40+
}
41+
42+
@Test
43+
public void testDeserializeDefaultsToRetryable() throws Exception {
44+
Artifact artifact =
45+
MAPPER.readValue("""
46+
{"type": "RETRY"}
47+
""", Artifact.class);
48+
assertTrue(artifact.asRetry().isRetryable());
49+
}
50+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"retryable": false,
3+
"type": "RETRY"
4+
}

maestro-engine/src/main/java/com/netflix/maestro/engine/params/OutputDataManager.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,26 @@ public void validateAndMergeOutputParamsAndArtifacts(StepRuntimeSummary runtimeS
9191
}
9292
}
9393

94+
/**
95+
* Checks whether the step's output data marks a failed step as non-retryable by the system.
96+
*
97+
* @param runtimeSummary step runtime summary used to locate the output data
98+
* @return true if the output data marks the step non-retryable, false otherwise
99+
*/
100+
public boolean isStepNonRetryable(StepRuntimeSummary runtimeSummary) {
101+
Optional<String> externalJobId = extractExternalJobId(runtimeSummary);
102+
if (externalJobId.isEmpty()) {
103+
return false;
104+
}
105+
Optional<OutputData> outputDataOpt =
106+
outputDataDao.getOutputDataForExternalJob(externalJobId.get(), runtimeSummary.getType());
107+
return outputDataOpt
108+
.map(OutputData::getArtifacts)
109+
.map(artifacts -> artifacts.get(Artifact.Type.RETRY.key()))
110+
.map(artifact -> !artifact.asRetry().isRetryable())
111+
.orElse(false);
112+
}
113+
94114
private Optional<String> extractExternalJobId(StepRuntimeSummary runtimeSummary) {
95115
Map<String, Artifact> artifacts = runtimeSummary.getArtifacts();
96116
String jobId = null;

maestro-engine/src/main/java/com/netflix/maestro/engine/tasks/MaestroTask.java

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -750,7 +750,7 @@ private void handleTimeoutError(
750750
}
751751

752752
/** Executes the step instance. It returns true, if the task is in dummy run mode. */
753-
@SuppressWarnings("PMD.ExhaustiveSwitchHasDefault")
753+
@SuppressWarnings({"PMD.ExhaustiveSwitchHasDefault", "checkstyle:MethodLength"})
754754
private boolean doExecute(
755755
Flow flow,
756756
Task task,
@@ -847,6 +847,21 @@ private boolean doExecute(
847847
evaluateNextConditionParams(flow, stepDefinition, runtimeSummary);
848848
doneWithExecute = true;
849849
break;
850+
case USER_FAILED:
851+
case PLATFORM_FAILED:
852+
// A retryable failure is escalated to FATALLY_FAILED when the step's output data marks
853+
// it as non-retryable, then falls through to the FATALLY_FAILED handling.
854+
if (!outputDataManager.isStepNonRetryable(runtimeSummary)) {
855+
doneWithExecute = true;
856+
break;
857+
}
858+
StepInstance.Status failedStatus = runtimeSummary.getRuntimeState().getStatus();
859+
runtimeSummary.markTerminated(StepInstance.Status.FATALLY_FAILED, tracingManager);
860+
runtimeSummary.addTimeline(
861+
TimelineLogEvent.info(
862+
"Step failed with [%s] and its output data classified it as non-retryable.",
863+
failedStatus));
864+
// fall through, to apply failure mode handling
850865
case FATALLY_FAILED: // Failure mode only applies to FATALLY_FAILED
851866
if (!runtimeSummary.isIgnoreFailureMode()) {
852867
if (FailureMode.IGNORE_FAILURE == stepDefinition.getFailureMode()) {
@@ -864,8 +879,6 @@ private boolean doExecute(
864879
}
865880
// fall through, otherwise
866881
case INTERNALLY_FAILED: // Ignoring failure model as the error happens within Maestro
867-
case USER_FAILED:
868-
case PLATFORM_FAILED:
869882
case TIMEOUT_FAILED:
870883
case STOPPED:
871884
case TIMED_OUT:

maestro-engine/src/test/java/com/netflix/maestro/engine/params/OutputDataManagerTest.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import com.netflix.maestro.models.artifact.Artifact;
3232
import com.netflix.maestro.models.artifact.DynamicOutputArtifact;
3333
import com.netflix.maestro.models.artifact.KubernetesArtifact;
34+
import com.netflix.maestro.models.artifact.RetryArtifact;
3435
import com.netflix.maestro.models.artifact.TitusArtifact;
3536
import com.netflix.maestro.models.definition.StepType;
3637
import com.netflix.maestro.models.parameter.InternalParamMode;
@@ -110,6 +111,40 @@ public void testSaveOutputData() {
110111
Mockito.verify(outputDataDao, times(1)).insertOrUpdateOutputData(outputData);
111112
}
112113

114+
@Test
115+
public void testIsStepNonRetryableWhenMarkedNonRetryable() {
116+
RetryArtifact retryArtifact = new RetryArtifact();
117+
retryArtifact.setRetryable(false);
118+
OutputData output = new OutputData(null, Map.of(Artifact.Type.RETRY.key(), retryArtifact));
119+
when(outputDataDao.getOutputDataForExternalJob(TASK_ID, StepType.TITUS))
120+
.thenReturn(Optional.of(output));
121+
runtimeSummary = runtimeSummaryBuilder().type(StepType.TITUS).artifacts(artifacts).build();
122+
assertTrue(outputDataManager.isStepNonRetryable(runtimeSummary));
123+
}
124+
125+
@Test
126+
public void testIsStepNonRetryableWhenMarkedRetryable() {
127+
OutputData output =
128+
new OutputData(null, Map.of(Artifact.Type.RETRY.key(), new RetryArtifact()));
129+
when(outputDataDao.getOutputDataForExternalJob(TASK_ID, StepType.TITUS))
130+
.thenReturn(Optional.of(output));
131+
runtimeSummary = runtimeSummaryBuilder().type(StepType.TITUS).artifacts(artifacts).build();
132+
assertFalse(outputDataManager.isStepNonRetryable(runtimeSummary));
133+
}
134+
135+
@Test
136+
public void testIsStepNonRetryableWhenNoRetryArtifact() {
137+
setupOutputDataDao();
138+
runtimeSummary = runtimeSummaryBuilder().type(StepType.TITUS).artifacts(artifacts).build();
139+
assertFalse(outputDataManager.isStepNonRetryable(runtimeSummary));
140+
}
141+
142+
@Test
143+
public void testIsStepNonRetryableWhenNoOutputData() {
144+
runtimeSummary = runtimeSummaryBuilder().type(StepType.TITUS).artifacts(artifacts).build();
145+
assertFalse(outputDataManager.isStepNonRetryable(runtimeSummary));
146+
}
147+
113148
@Test
114149
public void testMissingJobIdArtifact() {
115150
outputDataManager.validateAndMergeOutputParamsAndArtifacts(runtimeSummary);
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"properties": {
3+
"owner": "tester",
4+
"run_strategy": "sequential"
5+
},
6+
"workflow": {
7+
"id": "sample-kubernetes-nonretryable-wf",
8+
"name": "Test kubernetes workflow that fails and is classified non-retryable via output data",
9+
"steps": [
10+
{
11+
"step": {
12+
"id": "job1",
13+
"type": "kubernetes",
14+
"retry_policy": {
15+
"error_retry_limit": 3
16+
},
17+
"params": {
18+
"kubernetes": {
19+
"value": {
20+
"image": {
21+
"value": "busybox",
22+
"type": "STRING"
23+
},
24+
"command": {
25+
"value": ["/bin/sh"],
26+
"type": "STRING_ARRAY"
27+
},
28+
"args": {
29+
"value": ["-c", "sleep 5 && echo $$MAESTRO_OUTPUT_START$1$$MAESTRO_OUTPUT_END && exit 1", "sh", "{\"artifacts\":{\"maestro_retry\":{\"type\":\"RETRY\",\"retryable\":false}}}"],
30+
"type": "STRING_ARRAY"
31+
}
32+
},
33+
"type": "MAP"
34+
}
35+
}
36+
}
37+
}
38+
]
39+
}
40+
}

0 commit comments

Comments
 (0)