This is an automated email from the ASF dual-hosted git repository.
snuyanzin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/flink.git
The following commit(s) were added to refs/heads/master by this push:
new d6b9ff65e29 [FLINK-39730][runtime] Honor `maxExceptions` on
`ApplicationExceptionsHandler`
d6b9ff65e29 is described below
commit d6b9ff65e29e1c7acbcfba450a5f3e005f0ec84f
Author: Jubin Soni <[email protected]>
AuthorDate: Fri Jun 19 03:41:36 2026 -0700
[FLINK-39730][runtime] Honor `maxExceptions` on
`ApplicationExceptionsHandler`
---
.../shortcodes/generated/rest_v1_dispatcher.html | 10 ++
docs/static/generated/rest_v1_dispatcher.yml | 9 ++
.../src/test/resources/rest_api_v1.snapshot | 5 +-
.../application/ApplicationExceptionsHandler.java | 22 +++-
.../ApplicationExceptionsInfoWithHistory.java | 11 +-
.../application/ApplicationExceptionsHeaders.java | 7 +-
.../ApplicationExceptionsHandlerTest.java | 141 ++++++++++++++++++++-
7 files changed, 190 insertions(+), 15 deletions(-)
diff --git a/docs/layouts/shortcodes/generated/rest_v1_dispatcher.html
b/docs/layouts/shortcodes/generated/rest_v1_dispatcher.html
index b7ab854eb98..98c02d7a418 100644
--- a/docs/layouts/shortcodes/generated/rest_v1_dispatcher.html
+++ b/docs/layouts/shortcodes/generated/rest_v1_dispatcher.html
@@ -251,6 +251,16 @@
</ul>
</td>
</tr>
+ <tr>
+ <td colspan="2">Query parameters</td>
+ </tr>
+ <tr>
+ <td colspan="2">
+ <ul>
+<li><code>maxExceptions</code> (optional): Comma-separated list of integer
values that specifies the upper limit of exceptions to return.</li>
+ </ul>
+ </td>
+ </tr>
<tr>
<td colspan="2">
<label>
diff --git a/docs/static/generated/rest_v1_dispatcher.yml
b/docs/static/generated/rest_v1_dispatcher.yml
index ada573ef5df..24ecb76ed2e 100644
--- a/docs/static/generated/rest_v1_dispatcher.yml
+++ b/docs/static/generated/rest_v1_dispatcher.yml
@@ -62,6 +62,15 @@ paths:
required: true
schema:
$ref: "#/components/schemas/ApplicationID"
+ - name: maxExceptions
+ in: query
+ description: Comma-separated list of integer values that specifies the
upper
+ limit of exceptions to return.
+ required: false
+ style: form
+ schema:
+ type: integer
+ format: int32
responses:
"200":
description: The request was successful.
diff --git a/flink-runtime-web/src/test/resources/rest_api_v1.snapshot
b/flink-runtime-web/src/test/resources/rest_api_v1.snapshot
index f282db00ea9..890ac91ef3c 100644
--- a/flink-runtime-web/src/test/resources/rest_api_v1.snapshot
+++ b/flink-runtime-web/src/test/resources/rest_api_v1.snapshot
@@ -180,7 +180,10 @@
} ]
},
"query-parameters" : {
- "queryParameters" : [ ]
+ "queryParameters" : [ {
+ "key" : "maxExceptions",
+ "mandatory" : false
+ } ]
},
"request" : {
"type" : "object",
diff --git
a/flink-runtime/src/main/java/org/apache/flink/runtime/rest/handler/application/ApplicationExceptionsHandler.java
b/flink-runtime/src/main/java/org/apache/flink/runtime/rest/handler/application/ApplicationExceptionsHandler.java
index 6dc04cd6d88..3fb29687d56 100644
---
a/flink-runtime/src/main/java/org/apache/flink/runtime/rest/handler/application/ApplicationExceptionsHandler.java
+++
b/flink-runtime/src/main/java/org/apache/flink/runtime/rest/handler/application/ApplicationExceptionsHandler.java
@@ -24,9 +24,10 @@ import
org.apache.flink.runtime.rest.handler.AbstractRestHandler;
import org.apache.flink.runtime.rest.handler.HandlerRequest;
import
org.apache.flink.runtime.rest.messages.ApplicationExceptionsInfoWithHistory;
import org.apache.flink.runtime.rest.messages.ApplicationIDPathParameter;
-import org.apache.flink.runtime.rest.messages.ApplicationMessageParameters;
import org.apache.flink.runtime.rest.messages.EmptyRequestBody;
import org.apache.flink.runtime.rest.messages.MessageHeaders;
+import
org.apache.flink.runtime.rest.messages.application.ApplicationExceptionsMessageParameters;
+import org.apache.flink.runtime.rest.messages.job.UpperLimitExceptionParameter;
import org.apache.flink.runtime.webmonitor.RestfulGateway;
import org.apache.flink.runtime.webmonitor.history.ApplicationJsonArchivist;
import org.apache.flink.runtime.webmonitor.history.ArchivedJson;
@@ -38,6 +39,7 @@ import java.io.IOException;
import java.time.Duration;
import java.util.Collection;
import java.util.Collections;
+import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
@@ -47,9 +49,11 @@ public class ApplicationExceptionsHandler
RestfulGateway,
EmptyRequestBody,
ApplicationExceptionsInfoWithHistory,
- ApplicationMessageParameters>
+ ApplicationExceptionsMessageParameters>
implements ApplicationJsonArchivist {
+ static final int MAX_NUMBER_EXCEPTION_TO_REPORT = 20;
+
public ApplicationExceptionsHandler(
GatewayRetriever<? extends RestfulGateway> leaderRetriever,
Duration timeout,
@@ -57,7 +61,7 @@ public class ApplicationExceptionsHandler
MessageHeaders<
EmptyRequestBody,
ApplicationExceptionsInfoWithHistory,
- ApplicationMessageParameters>
+ ApplicationExceptionsMessageParameters>
messageHeaders) {
super(leaderRetriever, timeout, responseHeaders, messageHeaders);
}
@@ -66,13 +70,20 @@ public class ApplicationExceptionsHandler
public CompletableFuture<ApplicationExceptionsInfoWithHistory>
handleRequest(
@Nonnull HandlerRequest<EmptyRequestBody> request, @Nonnull
RestfulGateway gateway) {
ApplicationID applicationId =
request.getPathParameter(ApplicationIDPathParameter.class);
+ final List<Integer> exceptionToReportMaxSizes =
+ request.getQueryParameter(UpperLimitExceptionParameter.class);
+ final int exceptionToReportMaxSize =
+ exceptionToReportMaxSizes.size() > 0
+ ? exceptionToReportMaxSizes.get(0)
+ : MAX_NUMBER_EXCEPTION_TO_REPORT;
return gateway.requestApplication(applicationId, timeout)
.thenApply(
archivedApplication ->
ApplicationExceptionsInfoWithHistory
.fromApplicationExceptionHistory(
-
archivedApplication.getExceptionHistory()));
+
archivedApplication.getExceptionHistory(),
+ exceptionToReportMaxSize));
}
@Override
@@ -88,6 +99,7 @@ public class ApplicationExceptionsHandler
new ArchivedJson(
path,
ApplicationExceptionsInfoWithHistory.fromApplicationExceptionHistory(
- archivedApplication.getExceptionHistory())));
+ archivedApplication.getExceptionHistory(),
+ MAX_NUMBER_EXCEPTION_TO_REPORT)));
}
}
diff --git
a/flink-runtime/src/main/java/org/apache/flink/runtime/rest/messages/ApplicationExceptionsInfoWithHistory.java
b/flink-runtime/src/main/java/org/apache/flink/runtime/rest/messages/ApplicationExceptionsInfoWithHistory.java
index aa4d36425d8..82f97c2013e 100644
---
a/flink-runtime/src/main/java/org/apache/flink/runtime/rest/messages/ApplicationExceptionsInfoWithHistory.java
+++
b/flink-runtime/src/main/java/org/apache/flink/runtime/rest/messages/ApplicationExceptionsInfoWithHistory.java
@@ -33,7 +33,9 @@ import
org.apache.flink.shaded.jackson2.com.fasterxml.jackson.databind.annotatio
import javax.annotation.Nullable;
+import java.util.ArrayList;
import java.util.Collection;
+import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.StringJoiner;
@@ -91,10 +93,15 @@ public class ApplicationExceptionsInfoWithHistory
implements ResponseBody {
}
public static ApplicationExceptionsInfoWithHistory
fromApplicationExceptionHistory(
- Collection<ApplicationExceptionHistoryEntry> exceptions) {
+ Collection<ApplicationExceptionHistoryEntry> exceptions, int
maxSize) {
+ // reverse so the newest entries are returned first when truncating by
maxSize,
+ // matching JobExceptionsHandler#createJobExceptionHistory semantics
+ final List<ApplicationExceptionHistoryEntry> reversed = new
ArrayList<>(exceptions);
+ Collections.reverse(reversed);
return new ApplicationExceptionsInfoWithHistory(
new ApplicationExceptionHistory(
- exceptions.stream()
+ reversed.stream()
+ .limit(maxSize)
.map(
exception ->
new ApplicationExceptionInfo(
diff --git
a/flink-runtime/src/main/java/org/apache/flink/runtime/rest/messages/application/ApplicationExceptionsHeaders.java
b/flink-runtime/src/main/java/org/apache/flink/runtime/rest/messages/application/ApplicationExceptionsHeaders.java
index 22a0ad90e03..0f9c1b6c523 100644
---
a/flink-runtime/src/main/java/org/apache/flink/runtime/rest/messages/application/ApplicationExceptionsHeaders.java
+++
b/flink-runtime/src/main/java/org/apache/flink/runtime/rest/messages/application/ApplicationExceptionsHeaders.java
@@ -22,7 +22,6 @@ import org.apache.flink.runtime.rest.HttpMethodWrapper;
import
org.apache.flink.runtime.rest.handler.application.ApplicationExceptionsHandler;
import
org.apache.flink.runtime.rest.messages.ApplicationExceptionsInfoWithHistory;
import org.apache.flink.runtime.rest.messages.ApplicationIDPathParameter;
-import org.apache.flink.runtime.rest.messages.ApplicationMessageParameters;
import org.apache.flink.runtime.rest.messages.EmptyRequestBody;
import org.apache.flink.runtime.rest.messages.RuntimeMessageHeaders;
@@ -33,7 +32,7 @@ public class ApplicationExceptionsHeaders
implements RuntimeMessageHeaders<
EmptyRequestBody,
ApplicationExceptionsInfoWithHistory,
- ApplicationMessageParameters> {
+ ApplicationExceptionsMessageParameters> {
private static final ApplicationExceptionsHeaders INSTANCE = new
ApplicationExceptionsHeaders();
@@ -58,8 +57,8 @@ public class ApplicationExceptionsHeaders
}
@Override
- public ApplicationMessageParameters getUnresolvedMessageParameters() {
- return new ApplicationMessageParameters();
+ public ApplicationExceptionsMessageParameters
getUnresolvedMessageParameters() {
+ return new ApplicationExceptionsMessageParameters();
}
@Override
diff --git
a/flink-runtime/src/test/java/org/apache/flink/runtime/rest/handler/application/ApplicationExceptionsHandlerTest.java
b/flink-runtime/src/test/java/org/apache/flink/runtime/rest/handler/application/ApplicationExceptionsHandlerTest.java
index 26c11a60409..9f26e84d1f6 100644
---
a/flink-runtime/src/test/java/org/apache/flink/runtime/rest/handler/application/ApplicationExceptionsHandlerTest.java
+++
b/flink-runtime/src/test/java/org/apache/flink/runtime/rest/handler/application/ApplicationExceptionsHandlerTest.java
@@ -27,9 +27,10 @@ import org.apache.flink.runtime.rest.handler.HandlerRequest;
import org.apache.flink.runtime.rest.handler.HandlerRequestException;
import
org.apache.flink.runtime.rest.messages.ApplicationExceptionsInfoWithHistory;
import org.apache.flink.runtime.rest.messages.ApplicationIDPathParameter;
-import org.apache.flink.runtime.rest.messages.ApplicationMessageParameters;
import org.apache.flink.runtime.rest.messages.EmptyRequestBody;
import
org.apache.flink.runtime.rest.messages.application.ApplicationExceptionsHeaders;
+import
org.apache.flink.runtime.rest.messages.application.ApplicationExceptionsMessageParameters;
+import org.apache.flink.runtime.rest.messages.job.UpperLimitExceptionParameter;
import org.apache.flink.runtime.webmonitor.RestfulGateway;
import org.apache.flink.runtime.webmonitor.TestingRestfulGateway;
import org.apache.flink.runtime.webmonitor.retriever.GatewayRetriever;
@@ -57,13 +58,28 @@ class ApplicationExceptionsHandlerTest {
private static HandlerRequest<EmptyRequestBody>
createRequest(ApplicationID applicationId)
throws HandlerRequestException {
+ return createRequest(applicationId, Collections.emptyMap());
+ }
+
+ private static HandlerRequest<EmptyRequestBody> createRequest(
+ ApplicationID applicationId, int maxExceptions) throws
HandlerRequestException {
+ Map<String, List<String>> queryParameters = new HashMap<>();
+ queryParameters.put(
+ UpperLimitExceptionParameter.KEY,
+ Collections.singletonList(Integer.toString(maxExceptions)));
+ return createRequest(applicationId, queryParameters);
+ }
+
+ private static HandlerRequest<EmptyRequestBody> createRequest(
+ ApplicationID applicationId, Map<String, List<String>>
queryParameters)
+ throws HandlerRequestException {
Map<String, String> pathParameters = new HashMap<>();
pathParameters.put(ApplicationIDPathParameter.KEY,
applicationId.toString());
return HandlerRequest.resolveParametersAndCreate(
EmptyRequestBody.getInstance(),
- new ApplicationMessageParameters(),
+ new ApplicationExceptionsMessageParameters(),
pathParameters,
- Collections.emptyMap(),
+ queryParameters,
Collections.emptyList());
}
@@ -143,6 +159,125 @@ class ApplicationExceptionsHandlerTest {
assertThat(exceptionInfo.getJobId()).isEqualTo(jobId);
}
+ @Test
+ void testMaxExceptionsLimitsHistorySize() throws Exception {
+ final int historySize = 5;
+ final int maxExceptions = 2;
+ final long baseTimestamp = System.currentTimeMillis();
+ final List<ApplicationExceptionHistoryEntry> exceptionHistory = new
ArrayList<>();
+ for (int i = 0; i < historySize; i++) {
+ exceptionHistory.add(
+ new ApplicationExceptionHistoryEntry(
+ new RuntimeException("exception #" + i),
baseTimestamp + i, null));
+ }
+
+ final ArchivedApplication applicationWithExceptions =
+ new ArchivedApplication(
+ archivedApplication.getApplicationId(),
+ archivedApplication.getApplicationName(),
+ ApplicationState.FAILED,
+ new long[] {1L, 1L, 1L, 1L, 1L, 1L, 1L},
+ Collections.emptyMap(),
+ exceptionHistory);
+
+ testingRestfulGateway =
+ new TestingRestfulGateway.Builder()
+ .setRequestApplicationFunction(
+ applicationId ->
+ CompletableFuture.completedFuture(
+ applicationWithExceptions))
+ .build();
+
+ final HandlerRequest<EmptyRequestBody> limitedRequest =
+ createRequest(archivedApplication.getApplicationId(),
maxExceptions);
+
+ final ApplicationExceptionsInfoWithHistory response =
+ handler.handleRequest(limitedRequest,
testingRestfulGateway).get();
+
+ // newest entries are returned first when truncating, so with
historySize=5 and
+ // maxExceptions=2 we expect entries #4 and #3 (in that order)
+ assertThat(response.getExceptionHistory().getEntries())
+ .hasSize(maxExceptions)
+ .extracting(
+
ApplicationExceptionsInfoWithHistory.ApplicationExceptionInfo::getTimestamp)
+ .containsExactly(baseTimestamp + historySize - 1,
baseTimestamp + historySize - 2);
+ }
+
+ @Test
+ void testDefaultCapAppliedWhenMaxExceptionsNotProvided() throws Exception {
+ final int totalExceptions =
ApplicationExceptionsHandler.MAX_NUMBER_EXCEPTION_TO_REPORT + 5;
+ final List<ApplicationExceptionHistoryEntry> exceptionHistory = new
ArrayList<>();
+ for (int i = 0; i < totalExceptions; i++) {
+ exceptionHistory.add(
+ new ApplicationExceptionHistoryEntry(
+ new RuntimeException("exception #" + i),
+ System.currentTimeMillis(),
+ null));
+ }
+
+ final ArchivedApplication applicationWithExceptions =
+ new ArchivedApplication(
+ archivedApplication.getApplicationId(),
+ archivedApplication.getApplicationName(),
+ ApplicationState.FAILED,
+ new long[] {1L, 1L, 1L, 1L, 1L, 1L, 1L},
+ Collections.emptyMap(),
+ exceptionHistory);
+
+ testingRestfulGateway =
+ new TestingRestfulGateway.Builder()
+ .setRequestApplicationFunction(
+ applicationId ->
+ CompletableFuture.completedFuture(
+ applicationWithExceptions))
+ .build();
+
+ final ApplicationExceptionsInfoWithHistory response =
+ handler.handleRequest(handlerRequest,
testingRestfulGateway).get();
+
+ assertThat(response.getExceptionHistory().getEntries())
+
.hasSize(ApplicationExceptionsHandler.MAX_NUMBER_EXCEPTION_TO_REPORT);
+ }
+
+ @Test
+ void testMaxExceptionsLargerThanHistorySizeReturnsAllEntries() throws
Exception {
+ final int historySize = 3;
+ final int oversizedMaxExceptions = historySize + 7;
+ final List<ApplicationExceptionHistoryEntry> exceptionHistory = new
ArrayList<>();
+ for (int i = 0; i < historySize; i++) {
+ exceptionHistory.add(
+ new ApplicationExceptionHistoryEntry(
+ new RuntimeException("exception #" + i),
+ System.currentTimeMillis(),
+ null));
+ }
+
+ final ArchivedApplication applicationWithExceptions =
+ new ArchivedApplication(
+ archivedApplication.getApplicationId(),
+ archivedApplication.getApplicationName(),
+ ApplicationState.FAILED,
+ new long[] {1L, 1L, 1L, 1L, 1L, 1L, 1L},
+ Collections.emptyMap(),
+ exceptionHistory);
+
+ testingRestfulGateway =
+ new TestingRestfulGateway.Builder()
+ .setRequestApplicationFunction(
+ applicationId ->
+ CompletableFuture.completedFuture(
+ applicationWithExceptions))
+ .build();
+
+ final HandlerRequest<EmptyRequestBody> oversizedRequest =
+ createRequest(archivedApplication.getApplicationId(),
oversizedMaxExceptions);
+
+ final ApplicationExceptionsInfoWithHistory response =
+ handler.handleRequest(oversizedRequest,
testingRestfulGateway).get();
+
+
assertThat(response.getExceptionHistory().getEntries()).hasSize(historySize);
+ }
+
@Test
void testExceptionWithoutJobId() throws Exception {
final RuntimeException rootCause = new RuntimeException("exception
#0");