This is an automated email from the ASF dual-hosted git repository.
kfaraz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/druid.git
The following commit(s) were added to refs/heads/master by this push:
new cf9a3bdc143 Fix up error handling in unusedSegments API. (#16206)
cf9a3bdc143 is described below
commit cf9a3bdc1439bbdcc71a598f1c2979f5bfe0b4c0
Author: Abhishek Radhakrishnan <[email protected]>
AuthorDate: Wed Mar 27 00:01:21 2024 -0700
Fix up error handling in unusedSegments API. (#16206)
Changes:
- Handle exceptions in the API and map them to a `Response` object with the
appropriate error code.
- Replace `AuthorizationUtils.filterAuthorizedResources()` with
`DatasourceResourceFilter`.
The endpoint is annotated consistent with other usages.
- Update `DatasourceResourceFilter` to remove the lambda and update
javadocs.
The usages information is self-evident with an IDE.
- Adjust the invalid interval exception message.
- Break up the large unit test `testGetUnusedSegmentsInDataSource()` into
smaller unit tests
for each test case. Also, validate the error codes.
---
.../apache/druid/java/util/common/Intervals.java | 2 +-
.../druid/server/http/DataSourcesResource.java | 6 +-
.../apache/druid/server/http/MetadataResource.java | 62 ++++---
.../http/security/DatasourceResourceFilter.java | 29 +--
.../druid/server/http/MetadataResourceTest.java | 203 +++++++++++++--------
5 files changed, 169 insertions(+), 133 deletions(-)
diff --git
a/processing/src/main/java/org/apache/druid/java/util/common/Intervals.java
b/processing/src/main/java/org/apache/druid/java/util/common/Intervals.java
index 4da87648f0e..4dfe09144e6 100644
--- a/processing/src/main/java/org/apache/druid/java/util/common/Intervals.java
+++ b/processing/src/main/java/org/apache/druid/java/util/common/Intervals.java
@@ -44,7 +44,7 @@ public final class Intervals
return new Interval(interval, ISOChronology.getInstanceUTC());
}
catch (IllegalArgumentException e) {
- throw InvalidInput.exception(e, "Bad Interval[%s]: [%s]", interval,
e.getMessage());
+ throw InvalidInput.exception(e, "Invalid interval[%s]: [%s]", interval,
e.getMessage());
}
}
diff --git
a/server/src/main/java/org/apache/druid/server/http/DataSourcesResource.java
b/server/src/main/java/org/apache/druid/server/http/DataSourcesResource.java
index 2f4334b36ac..1614cb69074 100644
--- a/server/src/main/java/org/apache/druid/server/http/DataSourcesResource.java
+++ b/server/src/main/java/org/apache/druid/server/http/DataSourcesResource.java
@@ -40,7 +40,6 @@ import org.apache.druid.client.ImmutableSegmentLoadInfo;
import org.apache.druid.client.SegmentLoadInfo;
import org.apache.druid.common.guava.FutureUtils;
import org.apache.druid.error.DruidException;
-import org.apache.druid.error.ErrorResponse;
import org.apache.druid.error.InvalidInput;
import org.apache.druid.guice.annotations.PublicApi;
import org.apache.druid.java.util.common.DateTimes;
@@ -312,10 +311,7 @@ public class DataSourcesResource
return Response.ok(ImmutableMap.of("numChangedSegments",
numChangedSegments)).build();
}
catch (DruidException e) {
- return Response
- .status(e.getStatusCode())
- .entity(new ErrorResponse(e))
- .build();
+ return ServletResourceUtils.buildErrorResponseFrom(e);
}
catch (Exception e) {
log.error(e, "Error occurred while updating segments for
datasource[%s]", dataSourceName);
diff --git
a/server/src/main/java/org/apache/druid/server/http/MetadataResource.java
b/server/src/main/java/org/apache/druid/server/http/MetadataResource.java
index c53a998e238..3e2b8d56406 100644
--- a/server/src/main/java/org/apache/druid/server/http/MetadataResource.java
+++ b/server/src/main/java/org/apache/druid/server/http/MetadataResource.java
@@ -20,12 +20,15 @@
package org.apache.druid.server.http;
import com.google.common.base.Function;
+import com.google.common.base.Throwables;
import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.inject.Inject;
import com.sun.jersey.spi.container.ResourceFilters;
import org.apache.druid.client.DataSourcesSnapshot;
import org.apache.druid.client.ImmutableDruidDataSource;
+import org.apache.druid.error.DruidException;
import org.apache.druid.error.InvalidInput;
import org.apache.druid.indexing.overlord.IndexerMetadataStorageCoordinator;
import org.apache.druid.indexing.overlord.Segments;
@@ -340,6 +343,7 @@ public class MetadataResource
@GET
@Path("/datasources/{dataSourceName}/unusedSegments")
@Produces(MediaType.APPLICATION_JSON)
+ @ResourceFilters(DatasourceResourceFilter.class)
public Response getUnusedSegmentsInDataSource(
@Context final HttpServletRequest req,
@PathParam("dataSourceName") final String dataSource,
@@ -349,37 +353,43 @@ public class MetadataResource
@QueryParam("sortOrder") @Nullable final String sortOrder
)
{
- if (dataSource == null || dataSource.isEmpty()) {
- throw InvalidInput.exception("dataSourceName must be non-empty");
- }
- if (limit != null && limit < 0) {
- throw InvalidInput.exception("Invalid limit[%s] specified. Limit must be
> 0", limit);
- }
-
- if (lastSegmentId != null && SegmentId.tryParse(dataSource, lastSegmentId)
== null) {
- throw InvalidInput.exception("Invalid lastSegmentId[%s] specified.",
lastSegmentId);
- }
+ try {
+ if (dataSource == null || dataSource.isEmpty()) {
+ throw InvalidInput.exception("dataSourceName must be non-empty.");
+ }
- SortOrder theSortOrder = sortOrder == null ? null :
SortOrder.fromValue(sortOrder);
+ if (limit != null && limit < 0) {
+ throw InvalidInput.exception("Invalid limit[%s] specified. Limit must
be > 0.", limit);
+ }
- final Interval theInterval = interval != null ?
Intervals.of(interval.replace('_', '/')) : null;
- Iterable<DataSegmentPlus> unusedSegments =
segmentsMetadataManager.iterateAllUnusedSegmentsForDatasource(
- dataSource,
- theInterval,
- limit,
- lastSegmentId,
- theSortOrder
- );
+ if (lastSegmentId != null && SegmentId.tryParse(dataSource,
lastSegmentId) == null) {
+ throw InvalidInput.exception("Invalid lastSegmentId[%s] specified.",
lastSegmentId);
+ }
- final Function<DataSegmentPlus, Iterable<ResourceAction>> raGenerator =
segment -> Collections.singletonList(
-
AuthorizationUtils.DATASOURCE_READ_RA_GENERATOR.apply(segment.getDataSegment().getDataSource()));
+ final SortOrder theSortOrder = sortOrder == null ? null :
SortOrder.fromValue(sortOrder);
- final Iterable<DataSegmentPlus> authorizedSegments =
- AuthorizationUtils.filterAuthorizedResources(req, unusedSegments,
raGenerator, authorizerMapper);
+ final Interval theInterval = interval != null ?
Intervals.of(interval.replace('_', '/')) : null;
+ final Iterable<DataSegmentPlus> unusedSegments =
segmentsMetadataManager.iterateAllUnusedSegmentsForDatasource(
+ dataSource,
+ theInterval,
+ limit,
+ lastSegmentId,
+ theSortOrder
+ );
- final List<DataSegmentPlus> retVal = new ArrayList<>();
- authorizedSegments.iterator().forEachRemaining(retVal::add);
- return Response.status(Response.Status.OK).entity(retVal).build();
+ final List<DataSegmentPlus> retVal = new ArrayList<>();
+ unusedSegments.iterator().forEachRemaining(retVal::add);
+ return Response.status(Response.Status.OK).entity(retVal).build();
+ }
+ catch (DruidException e) {
+ return ServletResourceUtils.buildErrorResponseFrom(e);
+ }
+ catch (Exception e) {
+ return Response
+ .serverError()
+ .entity(ImmutableMap.of("error", "Exception occurred.", "message",
Throwables.getRootCause(e).toString()))
+ .build();
+ }
}
@GET
diff --git
a/server/src/main/java/org/apache/druid/server/http/security/DatasourceResourceFilter.java
b/server/src/main/java/org/apache/druid/server/http/security/DatasourceResourceFilter.java
index e32446cfe5c..2e84e5bd1f3 100644
---
a/server/src/main/java/org/apache/druid/server/http/security/DatasourceResourceFilter.java
+++
b/server/src/main/java/org/apache/druid/server/http/security/DatasourceResourceFilter.java
@@ -20,7 +20,6 @@
package org.apache.druid.server.http.security;
import com.google.common.base.Preconditions;
-import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import com.google.inject.Inject;
import com.sun.jersey.spi.container.ContainerRequest;
@@ -33,16 +32,15 @@ import org.apache.druid.server.security.ResourceAction;
import org.apache.druid.server.security.ResourceType;
import javax.ws.rs.core.PathSegment;
+import java.util.List;
/**
- * Use this ResourceFilter when the datasource information is present after
"datasources" segment in the request Path
- * Here are some example paths where this filter is used -
- * - druid/coordinator/v1/datasources/{dataSourceName}/...
- * - druid/coordinator/v1/metadata/datasources/{dataSourceName}/...
- * - druid/v2/datasources/{dataSourceName}/...
+ * Use this resource filter for API endpoints that contain {@link
#DATASOURCES_PATH_SEGMENT} in their request path.
*/
public class DatasourceResourceFilter extends AbstractResourceFilter
{
+ private static final String DATASOURCES_PATH_SEGMENT = "datasources";
+
@Inject
public DatasourceResourceFilter(
AuthorizerMapper authorizerMapper
@@ -74,20 +72,11 @@ public class DatasourceResourceFilter extends
AbstractResourceFilter
private String getRequestDatasourceName(ContainerRequest request)
{
- final String dataSourceName = request.getPathSegments()
- .get(
- Iterables.indexOf(
- request.getPathSegments(),
- new Predicate<PathSegment>()
- {
- @Override
- public boolean
apply(PathSegment input)
- {
- return
"datasources".equals(input.getPath());
- }
- }
- ) + 1
- ).getPath();
+ final List<PathSegment> pathSegments = request.getPathSegments();
+ final String dataSourceName = pathSegments.get(
+ Iterables.indexOf(pathSegments, input ->
DATASOURCES_PATH_SEGMENT.equals(input.getPath())) + 1
+ ).getPath();
+
Preconditions.checkNotNull(dataSourceName);
return dataSourceName;
}
diff --git
a/server/src/test/java/org/apache/druid/server/http/MetadataResourceTest.java
b/server/src/test/java/org/apache/druid/server/http/MetadataResourceTest.java
index c76da048295..cc1e4b7bc58 100644
---
a/server/src/test/java/org/apache/druid/server/http/MetadataResourceTest.java
+++
b/server/src/test/java/org/apache/druid/server/http/MetadataResourceTest.java
@@ -25,10 +25,10 @@ import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import org.apache.druid.client.DataSourcesSnapshot;
import org.apache.druid.client.ImmutableDruidDataSource;
-import org.apache.druid.error.DruidExceptionMatcher;
+import org.apache.druid.error.DruidException;
+import org.apache.druid.error.ErrorResponse;
import org.apache.druid.indexing.overlord.IndexerMetadataStorageCoordinator;
import org.apache.druid.java.util.common.DateTimes;
-import org.apache.druid.java.util.common.StringUtils;
import org.apache.druid.java.util.common.granularity.Granularities;
import org.apache.druid.java.util.common.guava.Comparators;
import org.apache.druid.metadata.SegmentsMetadataManager;
@@ -129,6 +129,15 @@ public class MetadataResourceTest
.when(storageCoordinator)
.retrieveSegmentForId(segments[5].getId().toString(), true);
+ Mockito.doAnswer(mockIterateAllUnusedSegmentsForDatasource())
+ .when(segmentsMetadataManager)
+ .iterateAllUnusedSegmentsForDatasource(
+ ArgumentMatchers.any(),
+ ArgumentMatchers.any(),
+ ArgumentMatchers.any(),
+ ArgumentMatchers.any(),
+ ArgumentMatchers.any());
+
metadataResource = new MetadataResource(
segmentsMetadataManager,
storageCoordinator,
@@ -251,101 +260,124 @@ public class MetadataResourceTest
Assert.assertEquals(new SegmentStatusInCluster(realTimeSegments[1], false,
null, 40L, true), resultList.get(5));
}
+
@Test
public void testGetUnusedSegmentsInDataSource()
{
- Mockito.doAnswer(mockIterateAllUnusedSegmentsForDatasource())
- .when(segmentsMetadataManager)
- .iterateAllUnusedSegmentsForDatasource(
- ArgumentMatchers.any(),
- ArgumentMatchers.any(),
- ArgumentMatchers.any(),
- ArgumentMatchers.any(),
- ArgumentMatchers.any());
-
- // test with null datasource name - fails with expected bad datasource
name error
- DruidExceptionMatcher.invalidInput().expectMessageIs(
- "dataSourceName must be non-empty"
- ).assertThrowsAndMatches(
- () -> metadataResource.getUnusedSegmentsInDataSource(request, null,
null, null, null, null)
- );
-
- // test with empty datasource name - fails with expected bad datasource
name error
- DruidExceptionMatcher.invalidInput().expectMessageIs(
- "dataSourceName must be non-empty"
- ).assertThrowsAndMatches(
- () -> metadataResource.getUnusedSegmentsInDataSource(request, "",
null, null, null, null)
- );
+ final Response response = metadataResource
+ .getUnusedSegmentsInDataSource(request, DATASOURCE1, null, null, null,
null);
+ final List<DataSegmentPlus> observedSegments =
extractResponseList(response);
+ Assert.assertEquals(segmentsPlus, observedSegments);
+ }
- // test invalid datasource - returns empty segments
- Response response = metadataResource.getUnusedSegmentsInDataSource(
- request,
- "invalid_datasource",
- null,
- null,
- null,
- null
- );
- List<DataSegmentPlus> resultList = extractResponseList(response);
- Assert.assertTrue(resultList.isEmpty());
-
- // test valid datasource with bad limit - fails with expected invalid
limit message
- DruidExceptionMatcher.invalidInput().expectMessageIs(
- StringUtils.format("Invalid limit[%s] specified. Limit must be > 0",
-1)
- ).assertThrowsAndMatches(
- () -> metadataResource.getUnusedSegmentsInDataSource(request,
DATASOURCE1, null, -1, null, null)
- );
+ @Test
+ public void testGetUnusedSegmentsInDataSourceWithIntervalFilter()
+ {
+ final String interval = SEGMENT_START_INTERVAL + "_P2D";
+ final int expectedLimit = NUM_PARTITIONS * 2;
+
+ final Response response = metadataResource
+ .getUnusedSegmentsInDataSource(request, DATASOURCE1, interval, null,
null, null);
+ final List<DataSegmentPlus> observedSegments =
extractResponseList(response);
+ Assert.assertEquals(expectedLimit, observedSegments.size());
+
Assert.assertEquals(segmentsPlus.stream().limit(expectedLimit).collect(Collectors.toList()),
observedSegments);
+ }
- // test valid datasource with invalid lastSegmentId - fails with expected
invalid lastSegmentId message
- DruidExceptionMatcher.invalidInput().expectMessageIs(
- StringUtils.format("Invalid lastSegmentId[%s] specified.", "invalid")
- ).assertThrowsAndMatches(
- () -> metadataResource.getUnusedSegmentsInDataSource(request,
DATASOURCE1, null, null, "invalid", null)
- );
+ @Test
+ public void testGetUnusedSegmentsInDataSourceWithLimitFilter()
+ {
+ final int limit = 3;
+ final String interval = SEGMENT_START_INTERVAL + "_P2D";
+
+ final Response response = metadataResource
+ .getUnusedSegmentsInDataSource(request, DATASOURCE1, interval, limit,
null, null);
+ final List<DataSegmentPlus> observedSegments =
extractResponseList(response);
+ Assert.assertEquals(limit, observedSegments.size());
+
Assert.assertEquals(segmentsPlus.stream().limit(limit).collect(Collectors.toList()),
observedSegments);
+ }
- // test valid datasource - returns all unused segments for that datasource
- response = metadataResource.getUnusedSegmentsInDataSource(request,
DATASOURCE1, null, null, null, null);
+ @Test
+ public void
testGetUnusedSegmentsInDataSourceWithLimitAndLastSegmentIdFilter()
+ {
+ final int limit = 3;
+ final String interval = SEGMENT_START_INTERVAL + "_P2D";
+ final String lastSegmentId = segments[limit - 1].getId().toString();
+ final Response response = metadataResource
+ .getUnusedSegmentsInDataSource(request, DATASOURCE1, interval, limit,
lastSegmentId, null);
+ final List<DataSegmentPlus> observedSegments =
extractResponseList(response);
+ Assert.assertEquals(Collections.singletonList(segmentsPlus.get(limit)),
observedSegments);
+ }
- resultList = extractResponseList(response);
- Assert.assertEquals(segmentsPlus, resultList);
+ @Test
+ public void testGetUnusedSegmentsInDataSourceWithNonExistentDataSource()
+ {
+ final Response response = metadataResource
+ .getUnusedSegmentsInDataSource(request, "non-existent", null, null,
null, null);
+ final List<DataSegmentPlus> observedSegments =
extractResponseList(response);
+ Assert.assertTrue(observedSegments.isEmpty());
+ }
- // test valid datasource with interval filter - returns all unused
segments for that datasource within interval
- int numDays = 2;
- String interval = SEGMENT_START_INTERVAL + "_P" + numDays + "D";
- response = metadataResource.getUnusedSegmentsInDataSource(request,
DATASOURCE1, interval, null, null, null);
+ @Test
+ public void testGetUnusedSegmentsWithNoDataSource()
+ {
+ final Response response = metadataResource
+ .getUnusedSegmentsInDataSource(request, null, null, null, null, null);
+ Assert.assertEquals(400, response.getStatus());
+ Assert.assertEquals("dataSourceName must be non-empty.",
getExceptionMessage(response));
+ }
- resultList = extractResponseList(response);
- Assert.assertEquals(NUM_PARTITIONS * numDays, resultList.size());
- Assert.assertEquals(
- Arrays.asList(segmentsPlus.get(0), segmentsPlus.get(1),
segmentsPlus.get(2), segmentsPlus.get(3)),
- resultList
- );
+ @Test
+ public void testGetUnusedSegmentsWithEmptyDatasourceName()
+ {
+ final Response response = metadataResource
+ .getUnusedSegmentsInDataSource(request, "", null, null, null, null);
+ Assert.assertEquals(400, response.getStatus());
+ Assert.assertEquals("dataSourceName must be non-empty.",
getExceptionMessage(response));
+ }
- // test valid datasource with interval filter limit and last segment id -
returns unused segments for that
- // datasource within interval upto limit starting at last segment id
- int limit = 3;
- response = metadataResource.getUnusedSegmentsInDataSource(request,
DATASOURCE1, interval, limit, null, null);
+ @Test
+ public void testGetUnusedSegmentsWithInvalidLimit()
+ {
+ final Response response = metadataResource
+ .getUnusedSegmentsInDataSource(request, DATASOURCE1, null, -1, null,
null);
+ Assert.assertEquals(400, response.getStatus());
+ Assert.assertEquals("Invalid limit[-1] specified. Limit must be > 0.",
getExceptionMessage(response));
+ }
- resultList = extractResponseList(response);
- Assert.assertEquals(limit, resultList.size());
- Assert.assertEquals(Arrays.asList(segmentsPlus.get(0),
segmentsPlus.get(1), segmentsPlus.get(2)), resultList);
+ @Test
+ public void testGetUnusedSegmentsWithInvalidLastSegmentId()
+ {
+ final Response response = metadataResource
+ .getUnusedSegmentsInDataSource(request, DATASOURCE1, null, null,
"invalid", null);
+ Assert.assertEquals(400, response.getStatus());
+ Assert.assertEquals("Invalid lastSegmentId[invalid] specified.",
getExceptionMessage(response));
+ }
- // test valid datasource with interval filter limit and offset - returns
unused segments for that datasource within
- // interval upto limit starting at offset
- response = metadataResource.getUnusedSegmentsInDataSource(
- request,
- DATASOURCE1,
- interval,
- limit,
- segments[2].getId().toString(),
- null
+ @Test
+ public void testGetUnusedSegmentsWithInvalidInterval()
+ {
+ final Response response = metadataResource
+ .getUnusedSegmentsInDataSource(request, DATASOURCE1, "2015/2014",
null, null, null);
+ Assert.assertEquals(400, response.getStatus());
+ Assert.assertEquals(
+ "Invalid interval[2015/2014]: [The end instant must be greater than
the start instant]",
+ getExceptionMessage(response)
);
+ }
- resultList = extractResponseList(response);
- Assert.assertEquals(Collections.singletonList(segmentsPlus.get(3)),
resultList);
+ @Test
+ public void testGetUnusedSegmentsWithInvalidSortOrder()
+ {
+ final Response response = metadataResource
+ .getUnusedSegmentsInDataSource(request, DATASOURCE1, null, null, null,
"Ascd");
+ Assert.assertEquals(400, response.getStatus());
+ Assert.assertEquals(
+ "Unexpected value[Ascd] for SortOrder. Possible values are: [ASC,
DESC]",
+ getExceptionMessage(response)
+ );
}
- Answer<Iterable<DataSegmentPlus>> mockIterateAllUnusedSegmentsForDatasource()
+ private Answer<Iterable<DataSegmentPlus>>
mockIterateAllUnusedSegmentsForDatasource()
{
return invocationOnMock -> {
String dataSourceName = invocationOnMock.getArgument(0);
@@ -375,6 +407,15 @@ public class MetadataResourceTest
};
}
+ private static String getExceptionMessage(final Response response)
+ {
+ if (response.getEntity() instanceof DruidException) {
+ return ((DruidException) response.getEntity()).getMessage();
+ } else {
+ return ((ErrorResponse)
response.getEntity()).getUnderlyingException().getMessage();
+ }
+ }
+
@Test
public void testGetDataSourceInformation()
{
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]