Skip to content

Commit 0d55ef4

Browse files
address feedback from self review
1 parent f1fcf0f commit 0d55ef4

7 files changed

Lines changed: 29 additions & 123 deletions

File tree

.github/workflows/ci.yml

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -149,13 +149,7 @@ jobs:
149149
uses: mikepenz/action-junit-report@v6
150150
with:
151151
report_paths: '**/build/test-results/test/TEST-*.xml'
152-
# Distinct per suite: the action defaults to 'JUnit Test Report' for every caller, so
153-
# this report and the ones published by integration-tests.yml all landed on a single
154-
# check run and overwrote each other.
155-
check_name: Unit Test Report
156-
152+
157153
- name: Check Tests Status
158154
if: steps.tests.outcome == 'failure'
159-
run: |
160-
echo "::error::Tests failed. See the 'Run Tests' step above for the Gradle/test output, and the 'Publish Test Report' step's JUnit summary for which test(s) failed."
161-
exit 1
155+
run: exit 1

conductor-client/src/main/java/com/netflix/conductor/sdk/workflow/executor/WorkflowExecutor.java

Lines changed: 7 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -77,17 +77,9 @@ public class WorkflowExecutor {
7777
private final TypeReference<List<TaskDef>> listOfTaskDefs = new TypeReference<>() {
7878
};
7979

80-
/** Zero, i.e. never give up. See {@link #setMonitorFailureGiveUpMillis(long)}. */
81-
private static final long DEFAULT_MONITOR_FAILURE_GIVE_UP_MILLIS = 0;
82-
8380
private final Map<String, CompletableFuture<Workflow>> runningWorkflowFutures =
8481
new ConcurrentHashMap<>();
8582

86-
/** When the current run of consecutive polling failures started, per workflow id. */
87-
private final Map<String, Long> monitorFailingSince = new ConcurrentHashMap<>();
88-
89-
private volatile long monitorFailureGiveUpMillis = DEFAULT_MONITOR_FAILURE_GIVE_UP_MILLIS;
90-
9183
private final ObjectMapper objectMapper = new ObjectMapperProvider().getObjectMapper();
9284

9385
private final TaskClient taskClient;
@@ -184,18 +176,19 @@ private void initMonitor() {
184176
CompletableFuture<Workflow> future = entry.getValue();
185177
try {
186178
Workflow workflow = workflowClient.getWorkflow(workflowId, true);
187-
monitorFailingSince.remove(workflowId);
188179
if (workflow.getStatus().isTerminal()) {
189180
future.complete(workflow);
190181
runningWorkflowFutures.remove(workflowId);
191182
}
192183
} catch (Exception e) {
193184
// scheduleAtFixedRate silently kills all future ticks on any uncaught
194-
// exception, so one transient error here would otherwise stop completion
195-
// tracking for every workflow, forever. Catch, but do not retry forever:
196-
// a workflow id that never becomes resolvable would spin at the polling
197-
// interval indefinitely while its caller blocks with no signal.
198-
handleMonitorFailure(workflowId, future, e);
185+
// exception, so this must not escape: one failed poll would otherwise
186+
// stop completion tracking for every workflow, forever. Stop tracking
187+
// this one and let its caller see the failure.
188+
LOGGER.error("Error polling workflow {} for completion; completing its "
189+
+ "future exceptionally", workflowId, e);
190+
runningWorkflowFutures.remove(workflowId);
191+
future.completeExceptionally(e);
199192
}
200193
}
201194
},
@@ -204,55 +197,6 @@ private void initMonitor() {
204197
TimeUnit.MILLISECONDS);
205198
}
206199

207-
private void handleMonitorFailure(String workflowId, CompletableFuture<Workflow> future, Exception e) {
208-
long now = System.currentTimeMillis();
209-
Long failingSince = monitorFailingSince.putIfAbsent(workflowId, now);
210-
211-
if (failingSince == null) {
212-
LOGGER.warn("Error polling workflow {} for completion; will retry on the next tick",
213-
workflowId, e);
214-
return;
215-
}
216-
217-
long giveUpMillis = monitorFailureGiveUpMillis;
218-
long failingForMillis = now - failingSince;
219-
if (giveUpMillis <= 0 || failingForMillis < giveUpMillis) {
220-
// Already warned once for this run of failures. Staying at DEBUG keeps a persistently
221-
// unresolvable workflow from emitting a stack trace on every tick.
222-
LOGGER.debug("Still failing to poll workflow {} for completion ({} ms so far)",
223-
workflowId, failingForMillis, e);
224-
return;
225-
}
226-
227-
LOGGER.error("Giving up polling workflow {} for completion after {} ms of consecutive "
228-
+ "failures; completing its future exceptionally", workflowId, failingForMillis, e);
229-
monitorFailingSince.remove(workflowId);
230-
runningWorkflowFutures.remove(workflowId);
231-
future.completeExceptionally(e);
232-
}
233-
234-
/**
235-
* How long the completion monitor keeps retrying a workflow whose status cannot be fetched
236-
* before giving up on it.
237-
*
238-
* @param monitorFailureGiveUpMillis zero or negative (the default) to never give up: the
239-
* monitor retries such a workflow for as long as this executor lives, so a server outage
240-
* longer than any fixed budget — a rolling restart, a failover — does not strand futures
241-
* that would otherwise have completed once the server came back. A positive value bounds
242-
* that: once a workflow has failed to poll continuously for this long, the monitor stops
243-
* tracking it and completes its future exceptionally, so a caller blocked in
244-
* {@code executeWorkflow(...).get()} sees an {@link java.util.concurrent.ExecutionException}
245-
* rather than blocking indefinitely on a workflow id that will never resolve.
246-
*/
247-
public void setMonitorFailureGiveUpMillis(long monitorFailureGiveUpMillis) {
248-
this.monitorFailureGiveUpMillis = monitorFailureGiveUpMillis;
249-
}
250-
251-
/** @see #setMonitorFailureGiveUpMillis(long) */
252-
public long getMonitorFailureGiveUpMillis() {
253-
return monitorFailureGiveUpMillis;
254-
}
255-
256200
public void initWorkers(String... packagesToScan) {
257201
annotatedWorkerExecutor.initWorkers(packagesToScan);
258202
}

conductor-client/src/test/java/com/netflix/conductor/sdk/workflow/executor/WorkflowExecutorMonitorTests.java

Lines changed: 12 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
import java.util.concurrent.CompletableFuture;
1717
import java.util.concurrent.ExecutionException;
1818
import java.util.concurrent.TimeUnit;
19-
import java.util.concurrent.TimeoutException;
2019

2120
import org.junit.jupiter.api.DisplayName;
2221
import org.junit.jupiter.api.Test;
@@ -58,59 +57,30 @@ private WorkflowExecutor executorFor(WorkflowClient workflowClient) {
5857
}
5958

6059
@Test
61-
@DisplayName("the monitor should keep polling after a transient getWorkflow failure")
62-
void monitorSurvivesTransientPollFailure() throws Exception {
63-
Workflow completed = new Workflow();
64-
completed.setStatus(Workflow.WorkflowStatus.COMPLETED);
65-
66-
WorkflowClient workflowClient = mock(WorkflowClient.class);
67-
when(workflowClient.startWorkflow(any(StartWorkflowRequest.class))).thenReturn(WORKFLOW_ID);
68-
when(workflowClient.getWorkflow(anyString(), anyBoolean()))
69-
.thenThrow(new RuntimeException("transient failure"))
70-
.thenReturn(completed);
71-
72-
WorkflowExecutor executor = executorFor(workflowClient);
73-
try {
74-
CompletableFuture<Workflow> future = executor.executeWorkflow("wf", 1, Map.of());
75-
76-
Workflow result = future.get(5, TimeUnit.SECONDS);
77-
78-
assertEquals(Workflow.WorkflowStatus.COMPLETED, result.getStatus());
79-
} finally {
80-
executor.shutdown();
81-
}
82-
}
83-
84-
@Test
85-
@DisplayName("the monitor should give up and fail the future once the failure budget is spent")
86-
void monitorGivesUpOnPersistentPollFailure() {
60+
@DisplayName("a failed poll should complete that workflow's future exceptionally")
61+
void monitorFailsTheFutureOnPollFailure() {
8762
WorkflowClient workflowClient = mock(WorkflowClient.class);
8863
when(workflowClient.startWorkflow(any(StartWorkflowRequest.class))).thenReturn(WORKFLOW_ID);
8964
when(workflowClient.getWorkflow(anyString(), anyBoolean()))
90-
.thenThrow(new RuntimeException("permanent failure"));
65+
.thenThrow(new RuntimeException("poll failure"));
9166

9267
WorkflowExecutor executor = executorFor(workflowClient);
9368
try {
94-
// 1ms budget: the monitor ticks every 100ms, so the second consecutive failure is
95-
// already past it. Keeps the test off the wall clock while still exercising a real
96-
// (positive, opt-in) budget rather than the never-give-up default.
97-
executor.setMonitorFailureGiveUpMillis(1);
98-
9969
CompletableFuture<Workflow> future = executor.executeWorkflow("wf", 1, Map.of());
10070

10171
ExecutionException thrown = assertThrows(
10272
ExecutionException.class, () -> future.get(5, TimeUnit.SECONDS));
10373

10474
assertInstanceOf(RuntimeException.class, thrown.getCause());
105-
assertEquals("permanent failure", thrown.getCause().getMessage());
75+
assertEquals("poll failure", thrown.getCause().getMessage());
10676
} finally {
10777
executor.shutdown();
10878
}
10979
}
11080

11181
@Test
112-
@DisplayName("by default the monitor should never give up, and one bad workflow should not stall the rest")
113-
void monitorDoesNotGiveUpByDefault() throws Exception {
82+
@DisplayName("one workflow's poll failure should not stop the monitor tracking the rest")
83+
void monitorKeepsTrackingOtherWorkflowsAfterAFailure() throws Exception {
11484
Workflow completed = new Workflow();
11585
completed.setStatus(Workflow.WorkflowStatus.COMPLETED);
11686

@@ -119,24 +89,20 @@ void monitorDoesNotGiveUpByDefault() throws Exception {
11989
.thenReturn(WORKFLOW_ID)
12090
.thenReturn(OTHER_WORKFLOW_ID);
12191
when(workflowClient.getWorkflow(eq(WORKFLOW_ID), anyBoolean()))
122-
.thenThrow(new RuntimeException("permanent failure"));
92+
.thenThrow(new RuntimeException("poll failure"));
12393
when(workflowClient.getWorkflow(eq(OTHER_WORKFLOW_ID), anyBoolean()))
12494
.thenReturn(completed);
12595

12696
WorkflowExecutor executor = executorFor(workflowClient);
12797
try {
128-
assertEquals(0, executor.getMonitorFailureGiveUpMillis(), "give-up should be off by default");
129-
13098
CompletableFuture<Workflow> failing = executor.executeWorkflow("wf", 1, Map.of());
131-
CompletableFuture<Workflow> healthy = executor.executeWorkflow("wf", 1, Map.of());
99+
assertThrows(ExecutionException.class, () -> failing.get(5, TimeUnit.SECONDS));
132100

133-
// The unresolvable workflow must not take the monitor down with it: a sibling
134-
// registered on the same tick loop still completes.
101+
// Registered only after the failure has already happened, so it can complete at all
102+
// only if the tick loop survived it -- scheduleAtFixedRate would have cancelled every
103+
// future tick had the exception been allowed to escape.
104+
CompletableFuture<Workflow> healthy = executor.executeWorkflow("wf", 1, Map.of());
135105
assertEquals(Workflow.WorkflowStatus.COMPLETED, healthy.get(5, TimeUnit.SECONDS).getStatus());
136-
137-
// ...and the unresolvable one stays pending rather than being failed, which is the
138-
// pre-bounded-retry behavior callers depend on across a server restart.
139-
assertThrows(TimeoutException.class, () -> failing.get(1, TimeUnit.SECONDS));
140106
} finally {
141107
executor.shutdown();
142108
}

scripts/docker-compose-oss.yaml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
# Shared by scripts/run-integration-oss.sh and the integration-tests-oss job in
33
# .github/workflows/integration-tests.yml.
44
#
5-
# OSS_CONDUCTOR_VERSION defaults to `latest` for local runs; CI pins it via the
6-
# E2E_TEST_OSS_CONDUCTOR_VERSION org variable (or a workflow_dispatch input).
5+
# OSS_CONDUCTOR_VERSION defaults to `latest` for local runs. CI resolves it from the
6+
# E2E_TEST_OSS_CONDUCTOR_VERSION org variable (or a workflow_dispatch input); that variable is
7+
# currently set to `latest` too, so CI tracks whatever `latest` resolves to at run time rather
8+
# than a fixed version. Set the org variable to a real tag if the job needs to be deterministic.
79

810
services:
911
conductor-server:

tests/src/test/java/io/orkes/conductor/client/http/EventClientTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ void testEventHandler() {
3838
} catch (ConductorClientException e) {
3939
// Best-effort cleanup: tolerate "doesn't exist" in whichever shape the server
4040
// we're running against actually reports it.
41-
TestUtil.assertNotFoundOrRethrow(e, "not found");
41+
TestUtil.tolerateNotFound(e, "EventHandler with name");
4242
}
4343
EventHandler eventHandler = getEventHandler();
4444
eventClient.registerEventHandler(eventHandler);

tests/src/test/java/io/orkes/conductor/client/http/MetadataClientTests.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ void taskDefinition() {
4141
} catch (ConductorClientException e) {
4242
// Best-effort cleanup: tolerate "doesn't exist" in whichever shape the server
4343
// we're running against actually reports it.
44-
TestUtil.assertNotFoundOrRethrow(e, "No such task definition");
44+
TestUtil.tolerateNotFound(e, "No such task definition");
4545
}
4646
TaskDef taskDef = Commons.getTaskDef();
4747
metadataClient.registerTaskDefs(List.of(taskDef));
@@ -57,7 +57,7 @@ void workflow() {
5757
} catch (ConductorClientException e) {
5858
// Best-effort cleanup: tolerate "doesn't exist" in whichever shape the server
5959
// we're running against actually reports it.
60-
TestUtil.assertNotFoundOrRethrow(e, "No such workflow definition");
60+
TestUtil.tolerateNotFound(e, "No such workflow definition");
6161
}
6262
metadataClient.registerTaskDefs(List.of(Commons.getTaskDef()));
6363
WorkflowDef workflowDef = WorkflowUtil.getWorkflowDef();

tests/src/test/java/io/orkes/conductor/client/util/TestUtil.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ private static boolean isTerminalFailure(Workflow workflow) {
174174
* OSS with it deliberately unset, and OSS returning a correct 404 for one of these endpoints
175175
* should not start failing the suite.
176176
*/
177-
public static void assertNotFoundOrRethrow(ConductorClientException e, String ossMessageSubstring) {
177+
public static void tolerateNotFound(ConductorClientException e, String ossMessageSubstring) {
178178
if (e.getStatus() == 404) {
179179
return;
180180
}

0 commit comments

Comments
 (0)