This is an automated email from the ASF dual-hosted git repository.
kgyrtkirk 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 c1e387e69f8 fix: release the Coordinator segment response in
sys.segments (#20523)
c1e387e69f8 is described below
commit c1e387e69f8414ab0a3b576c7aa0504d9398f5ad
Author: Zoltan Haindrich <[email protected]>
AuthorDate: Thu Oct 15 08:23:12 2026 +0200
fix: release the Coordinator segment response in sys.segments (#20523)
fetchAllUsedSegmentsWithOvershadowedStatus hands out a ResourceHolder that
closes the streamed response; MetadataSegmentView and SystemSchema close it,
also on early exit, and QueryHandler now closes the bound Interpreter so scans
under Filter/Project nodes are released too. No segment left behind.
---
.../calcite/schema/SysSegmentsTableBenchmark.java | 9 +-
.../embedded/server/CoordinatorClientTest.java | 9 +-
.../client/coordinator/CoordinatorClient.java | 7 +-
.../client/coordinator/CoordinatorClientImpl.java | 22 +++-
.../client/coordinator/NoopCoordinatorClient.java | 5 +-
.../coordinator/CoordinatorClientImplTest.java | 44 +++++--
.../druid/sql/calcite/planner/QueryHandler.java | 90 +++++++-------
.../sql/calcite/schema/MetadataSegmentView.java | 62 ++++++----
.../druid/sql/calcite/schema/SystemSchema.java | 10 +-
.../sql/calcite/planner/QueryHandlerTest.java | 57 +++++++++
.../calcite/schema/MetadataSegmentViewTest.java | 135 ++++++++++++++++++++-
.../druid/sql/calcite/schema/SystemSchemaTest.java | 46 ++++++-
12 files changed, 397 insertions(+), 99 deletions(-)
diff --git
a/benchmarks/src/test/java/org/apache/druid/sql/calcite/schema/SysSegmentsTableBenchmark.java
b/benchmarks/src/test/java/org/apache/druid/sql/calcite/schema/SysSegmentsTableBenchmark.java
index d75f46c89ee..3b85e24f9d1 100644
---
a/benchmarks/src/test/java/org/apache/druid/sql/calcite/schema/SysSegmentsTableBenchmark.java
+++
b/benchmarks/src/test/java/org/apache/druid/sql/calcite/schema/SysSegmentsTableBenchmark.java
@@ -35,12 +35,12 @@ import org.apache.druid.client.BrokerSegmentWatcherConfig;
import org.apache.druid.client.InternalQueryConfig;
import org.apache.druid.client.TimelineServerView;
import org.apache.druid.client.coordinator.NoopCoordinatorClient;
+import org.apache.druid.collections.ResourceHolder;
+import org.apache.druid.collections.StupidResourceHolder;
import org.apache.druid.error.NotYetImplemented;
import org.apache.druid.jackson.DefaultObjectMapper;
-import org.apache.druid.java.util.common.CloseableIterators;
import org.apache.druid.java.util.common.Intervals;
import org.apache.druid.java.util.common.StringUtils;
-import org.apache.druid.java.util.common.parsers.CloseableIterator;
import org.apache.druid.segment.join.JoinableFactory;
import org.apache.druid.segment.metadata.CentralizedDatasourceSchemaConfig;
import org.apache.druid.server.QueryLifecycleFactory;
@@ -73,6 +73,7 @@ import org.openjdk.jmh.infra.Blackhole;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.EnumMap;
+import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -180,12 +181,12 @@ public class SysSegmentsTableBenchmark
final NoopCoordinatorClient coordinatorClient = new NoopCoordinatorClient()
{
@Override
- public ListenableFuture<CloseableIterator<SegmentStatusInCluster>>
fetchAllUsedSegmentsWithOvershadowedStatus(
+ public
ListenableFuture<ResourceHolder<Iterator<SegmentStatusInCluster>>>
fetchAllUsedSegmentsWithOvershadowedStatus(
Set<String> watchedDataSources,
boolean includeRealtimeSegments
)
{
- return
Futures.immediateFuture(CloseableIterators.withEmptyBaggage(publishedSegments.iterator()));
+ return
Futures.immediateFuture(StupidResourceHolder.create(publishedSegments.iterator()));
}
};
diff --git
a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/server/CoordinatorClientTest.java
b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/server/CoordinatorClientTest.java
index 065963f25a3..980b2b256ab 100644
---
a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/server/CoordinatorClientTest.java
+++
b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/server/CoordinatorClientTest.java
@@ -21,11 +21,11 @@ package org.apache.druid.testing.embedded.server;
import org.apache.druid.client.ImmutableSegmentLoadInfo;
import org.apache.druid.client.coordinator.CoordinatorClient;
+import org.apache.druid.collections.ResourceHolder;
import org.apache.druid.common.utils.IdUtils;
import org.apache.druid.indexing.common.task.Task;
import org.apache.druid.indexing.common.task.TaskBuilder;
import org.apache.druid.java.util.common.Intervals;
-import org.apache.druid.java.util.common.parsers.CloseableIterator;
import org.apache.druid.query.SegmentDescriptor;
import
org.apache.druid.server.coordinator.rules.ForeverBroadcastDistributionRule;
import org.apache.druid.server.coordinator.rules.Rule;
@@ -44,9 +44,9 @@ import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
-import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
+import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -154,13 +154,14 @@ public class CoordinatorClientTest extends
EmbeddedClusterTestBase
@Test
@Timeout(20)
- public void test_fetchAllUsedSegmentsWithOvershadowedStatus() throws
IOException
+ public void test_fetchAllUsedSegmentsWithOvershadowedStatus()
{
runIndexTask();
- try (CloseableIterator<SegmentStatusInCluster> iterator =
cluster.callApi().onLeaderCoordinator(
+ try (ResourceHolder<Iterator<SegmentStatusInCluster>> segments =
cluster.callApi().onLeaderCoordinator(
c -> c.fetchAllUsedSegmentsWithOvershadowedStatus(Set.of(dataSource),
true))
) {
+ final Iterator<SegmentStatusInCluster> iterator = segments.get();
Assertions.assertTrue(iterator.hasNext());
SegmentStatusInCluster segmentStatus = iterator.next();
Assertions.assertEquals(dataSource,
segmentStatus.getDataSegment().getDataSource());
diff --git
a/server/src/main/java/org/apache/druid/client/coordinator/CoordinatorClient.java
b/server/src/main/java/org/apache/druid/client/coordinator/CoordinatorClient.java
index fb23fadf489..bc2e0a71511 100644
---
a/server/src/main/java/org/apache/druid/client/coordinator/CoordinatorClient.java
+++
b/server/src/main/java/org/apache/druid/client/coordinator/CoordinatorClient.java
@@ -22,7 +22,7 @@ package org.apache.druid.client.coordinator;
import com.google.common.util.concurrent.ListenableFuture;
import org.apache.druid.client.BootstrapSegmentsResponse;
import org.apache.druid.client.ImmutableSegmentLoadInfo;
-import org.apache.druid.java.util.common.parsers.CloseableIterator;
+import org.apache.druid.collections.ResourceHolder;
import org.apache.druid.query.SegmentDescriptor;
import org.apache.druid.query.lookup.LookupExtractorFactoryContainer;
import org.apache.druid.rpc.ServiceRetryPolicy;
@@ -37,6 +37,7 @@ import org.joda.time.Interval;
import javax.annotation.Nullable;
import java.net.URI;
+import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -137,12 +138,14 @@ public interface CoordinatorClient
/**
* Returns an iterator over the metadata segments of multiple datasources in
the cluster, fetching them in one go.
* <p>
+ * The caller is responsible for closing the holder.
+ * <p>
* API: {@code GET
/druid/coordinator/v1/metadata/segments?includeOvershadowedStatus}
*
* @param watchedDataSources Optional datasources to filter the segments by.
If null or empty, all segments are returned.
* @param includeRealtimeSegments If true, includes realtime segments in the
result.
*/
- ListenableFuture<CloseableIterator<SegmentStatusInCluster>>
fetchAllUsedSegmentsWithOvershadowedStatus(
+ ListenableFuture<ResourceHolder<Iterator<SegmentStatusInCluster>>>
fetchAllUsedSegmentsWithOvershadowedStatus(
@Nullable Set<String> watchedDataSources,
boolean includeRealtimeSegments
);
diff --git
a/server/src/main/java/org/apache/druid/client/coordinator/CoordinatorClientImpl.java
b/server/src/main/java/org/apache/druid/client/coordinator/CoordinatorClientImpl.java
index 1fed06decc9..9dfa0bab5b7 100644
---
a/server/src/main/java/org/apache/druid/client/coordinator/CoordinatorClientImpl.java
+++
b/server/src/main/java/org/apache/druid/client/coordinator/CoordinatorClientImpl.java
@@ -28,10 +28,10 @@ import io.netty.handler.codec.http.HttpMethod;
import org.apache.druid.client.BootstrapSegmentsResponse;
import org.apache.druid.client.ImmutableSegmentLoadInfo;
import org.apache.druid.client.JsonParserIterator;
+import org.apache.druid.collections.ResourceHolder;
import org.apache.druid.common.guava.FutureUtils;
import org.apache.druid.java.util.common.StringUtils;
import org.apache.druid.java.util.common.jackson.JacksonUtils;
-import org.apache.druid.java.util.common.parsers.CloseableIterator;
import
org.apache.druid.java.util.http.client.response.BytesFullResponseHandler;
import org.apache.druid.java.util.http.client.response.BytesFullResponseHolder;
import
org.apache.druid.java.util.http.client.response.InputStreamResponseHandler;
@@ -52,6 +52,7 @@ import
org.apache.druid.server.coordinator.CoordinatorDynamicConfig;
import org.apache.druid.server.coordinator.rules.Rule;
import org.apache.druid.timeline.DataSegment;
import org.apache.druid.timeline.SegmentStatusInCluster;
+import org.apache.druid.utils.CloseableUtils;
import org.joda.time.Interval;
import javax.annotation.Nullable;
@@ -59,6 +60,7 @@ import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
+import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -321,7 +323,7 @@ public class CoordinatorClientImpl implements
CoordinatorClient
}
@Override
- public ListenableFuture<CloseableIterator<SegmentStatusInCluster>>
fetchAllUsedSegmentsWithOvershadowedStatus(
+ public ListenableFuture<ResourceHolder<Iterator<SegmentStatusInCluster>>>
fetchAllUsedSegmentsWithOvershadowedStatus(
@Nullable Set<String> watchedDataSources,
boolean includeRealtimeSegments
)
@@ -346,11 +348,25 @@ public class CoordinatorClientImpl implements
CoordinatorClient
new InputStreamResponseHandler()
),
inputStream -> {
- return new JsonParserIterator<>(
+ final JsonParserIterator<SegmentStatusInCluster> segments = new
JsonParserIterator<>(
jsonMapper.getTypeFactory().constructType(SegmentStatusInCluster.class),
Futures.immediateFuture(inputStream),
jsonMapper
);
+ return new ResourceHolder<Iterator<SegmentStatusInCluster>>()
+ {
+ @Override
+ public Iterator<SegmentStatusInCluster> get()
+ {
+ return segments;
+ }
+
+ @Override
+ public void close()
+ {
+ CloseableUtils.closeAndWrapExceptions(() ->
CloseableUtils.closeAll(segments, inputStream));
+ }
+ };
}
);
}
diff --git
a/server/src/main/java/org/apache/druid/client/coordinator/NoopCoordinatorClient.java
b/server/src/main/java/org/apache/druid/client/coordinator/NoopCoordinatorClient.java
index b8aa55ec1e4..24881e6994b 100644
---
a/server/src/main/java/org/apache/druid/client/coordinator/NoopCoordinatorClient.java
+++
b/server/src/main/java/org/apache/druid/client/coordinator/NoopCoordinatorClient.java
@@ -22,7 +22,7 @@ package org.apache.druid.client.coordinator;
import com.google.common.util.concurrent.ListenableFuture;
import org.apache.druid.client.BootstrapSegmentsResponse;
import org.apache.druid.client.ImmutableSegmentLoadInfo;
-import org.apache.druid.java.util.common.parsers.CloseableIterator;
+import org.apache.druid.collections.ResourceHolder;
import org.apache.druid.query.SegmentDescriptor;
import org.apache.druid.query.lookup.LookupExtractorFactoryContainer;
import org.apache.druid.rpc.ServiceRetryPolicy;
@@ -37,6 +37,7 @@ import org.joda.time.Interval;
import javax.annotation.Nullable;
import java.net.URI;
+import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -131,7 +132,7 @@ public class NoopCoordinatorClient implements
CoordinatorClient
}
@Override
- public ListenableFuture<CloseableIterator<SegmentStatusInCluster>>
fetchAllUsedSegmentsWithOvershadowedStatus(
+ public ListenableFuture<ResourceHolder<Iterator<SegmentStatusInCluster>>>
fetchAllUsedSegmentsWithOvershadowedStatus(
@Nullable Set<String> watchedDataSources,
boolean includeOvershadowed
)
diff --git
a/server/src/test/java/org/apache/druid/client/coordinator/CoordinatorClientImplTest.java
b/server/src/test/java/org/apache/druid/client/coordinator/CoordinatorClientImplTest.java
index 206e5df8446..d7c3c5a8e0b 100644
---
a/server/src/test/java/org/apache/druid/client/coordinator/CoordinatorClientImplTest.java
+++
b/server/src/test/java/org/apache/druid/client/coordinator/CoordinatorClientImplTest.java
@@ -35,14 +35,15 @@ import io.netty.handler.codec.http.HttpVersion;
import org.apache.druid.client.BootstrapSegmentsResponse;
import org.apache.druid.client.DruidServer;
import org.apache.druid.client.ImmutableSegmentLoadInfo;
+import org.apache.druid.collections.ResourceHolder;
import org.apache.druid.common.guava.FutureUtils;
import org.apache.druid.guice.StartupInjectorBuilder;
import org.apache.druid.initialization.CoreInjectorBuilder;
import org.apache.druid.jackson.DefaultObjectMapper;
import org.apache.druid.java.util.common.Intervals;
import org.apache.druid.java.util.common.StringUtils;
-import org.apache.druid.java.util.common.parsers.CloseableIterator;
import
org.apache.druid.java.util.http.client.response.StringFullResponseHolder;
+import org.apache.druid.query.QueryInterruptedException;
import org.apache.druid.query.SegmentDescriptor;
import org.apache.druid.query.lookup.LookupExtractorFactory;
import org.apache.druid.query.lookup.LookupExtractorFactoryContainer;
@@ -78,6 +79,7 @@ import javax.ws.rs.core.MediaType;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
@@ -575,10 +577,10 @@ public class CoordinatorClientImplTest
jsonMapper.writeValueAsBytes(segments)
);
- CloseableIterator<SegmentStatusInCluster> iterator =
FutureUtils.getUnchecked(
+ Iterator<SegmentStatusInCluster> iterator = FutureUtils.getUnchecked(
coordinatorClient.fetchAllUsedSegmentsWithOvershadowedStatus(null,
true),
true
- );
+ ).get();
List<SegmentStatusInCluster> actualSegments = new ArrayList<>();
while (iterator.hasNext()) {
actualSegments.add(iterator.next());
@@ -606,10 +608,10 @@ public class CoordinatorClientImplTest
jsonMapper.writeValueAsBytes(segments)
);
- CloseableIterator<SegmentStatusInCluster> iterator =
FutureUtils.getUnchecked(
+ Iterator<SegmentStatusInCluster> iterator = FutureUtils.getUnchecked(
coordinatorClient.fetchAllUsedSegmentsWithOvershadowedStatus(null,
false),
true
- );
+ ).get();
List<SegmentStatusInCluster> actualSegments = new ArrayList<>();
while (iterator.hasNext()) {
actualSegments.add(iterator.next());
@@ -622,6 +624,26 @@ public class CoordinatorClientImplTest
);
}
+ @Test
+ public void
test_fetchAllUsedSegmentsWithOvershadowedStatus_closeBeforeIteratingReleasesTheResponse()
+ throws JsonProcessingException
+ {
+ serviceClient.expectAndRespond(
+ new RequestBuilder(HttpMethod.GET,
"/druid/coordinator/v1/metadata/segments?includeOvershadowedStatus"),
+ HttpResponseStatus.OK,
+ ImmutableMap.of(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON),
+ jsonMapper.writeValueAsBytes(ImmutableList.of(SEGMENT1))
+ );
+
+ final ResourceHolder<Iterator<SegmentStatusInCluster>> segments =
FutureUtils.getUnchecked(
+ coordinatorClient.fetchAllUsedSegmentsWithOvershadowedStatus(null,
false),
+ true
+ );
+ segments.close();
+
+ Assertions.assertThrows(QueryInterruptedException.class, () ->
segments.get().hasNext());
+ }
+
@Test
public void
test_fetchAllUsedSegmentsWithOvershadowedStatus_filterByDataSource() throws
Exception
{
@@ -635,10 +657,10 @@ public class CoordinatorClientImplTest
jsonMapper.writeValueAsBytes(ImmutableList.of(SEGMENT3))
);
- CloseableIterator<SegmentStatusInCluster> iterator =
FutureUtils.getUnchecked(
+ Iterator<SegmentStatusInCluster> iterator = FutureUtils.getUnchecked(
coordinatorClient.fetchAllUsedSegmentsWithOvershadowedStatus(Set.of("abc"),
true),
true
- );
+ ).get();
List<SegmentStatusInCluster> actualSegments = new ArrayList<>();
while (iterator.hasNext()) {
@@ -666,10 +688,10 @@ public class CoordinatorClientImplTest
);
Set<String> dataSources = new LinkedHashSet<>(List.of("xyz", "abc"));
- CloseableIterator<SegmentStatusInCluster> iterator =
FutureUtils.getUnchecked(
+ Iterator<SegmentStatusInCluster> iterator = FutureUtils.getUnchecked(
coordinatorClient.fetchAllUsedSegmentsWithOvershadowedStatus(dataSources, true),
true
- );
+ ).get();
List<SegmentStatusInCluster> actualSegments = new ArrayList<>();
while (iterator.hasNext()) {
@@ -696,10 +718,10 @@ public class CoordinatorClientImplTest
jsonMapper.writeValueAsBytes(List.of(SEGMENT3))
);
- CloseableIterator<SegmentStatusInCluster> iterator =
FutureUtils.getUnchecked(
+ Iterator<SegmentStatusInCluster> iterator = FutureUtils.getUnchecked(
coordinatorClient.fetchAllUsedSegmentsWithOvershadowedStatus(Set.of("abc"),
false),
true
- );
+ ).get();
List<SegmentStatusInCluster> actualSegments = new ArrayList<>();
while (iterator.hasNext()) {
diff --git
a/sql/src/main/java/org/apache/druid/sql/calcite/planner/QueryHandler.java
b/sql/src/main/java/org/apache/druid/sql/calcite/planner/QueryHandler.java
index 1c8dc24f6f9..55c1354e47c 100644
--- a/sql/src/main/java/org/apache/druid/sql/calcite/planner/QueryHandler.java
+++ b/sql/src/main/java/org/apache/druid/sql/calcite/planner/QueryHandler.java
@@ -23,6 +23,7 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.base.Supplier;
@@ -59,6 +60,8 @@ import org.apache.druid.error.DruidException;
import org.apache.druid.error.InvalidSqlInput;
import org.apache.druid.jackson.DefaultObjectMapper;
import org.apache.druid.java.util.common.guava.BaseSequence;
+import org.apache.druid.java.util.common.guava.Sequence;
+import org.apache.druid.java.util.common.guava.SequenceWrapper;
import org.apache.druid.java.util.common.guava.Sequences;
import org.apache.druid.java.util.emitter.EmittingLogger;
import org.apache.druid.query.Query;
@@ -336,45 +339,48 @@ public abstract class QueryHandler extends
SqlStatementHandler.BaseStatementHand
planner.getTypeFactory(),
plannerContext.getParameters()
);
- final Supplier<QueryResponse<Object[]>> resultsSupplier = () -> {
- final Enumerable<?> enumerable = theRel.bind(dataContext);
- final Enumerator<?> enumerator = enumerable.enumerator();
- return QueryResponse.withEmptyContext(
- Sequences.withBaggage(new BaseSequence<>(
- new BaseSequence.IteratorMaker<Object[],
QueryHandler.EnumeratorIterator<Object[]>>()
- {
- @Override
- public QueryHandler.EnumeratorIterator<Object[]> make()
- {
- return new QueryHandler.EnumeratorIterator<>(new
Iterator<>()
- {
- @Override
- public boolean hasNext()
- {
- return enumerator.moveNext();
- }
-
- @Override
- public Object[] next()
- {
- return (Object[]) enumerator.current();
- }
- });
- }
-
- @Override
- public void
cleanup(QueryHandler.EnumeratorIterator<Object[]> iterFromMake)
- {
-
- }
- }
- ), enumerator::close)
- );
- };
+ final Supplier<QueryResponse<Object[]>> resultsSupplier =
+ () ->
QueryResponse.withEmptyContext(enumerate(theRel.bind(dataContext)));
return new PlannerResult(resultsSupplier, rootQueryRel.validatedRowType);
}
}
+ /**
+ * Rows of a bound {@link BindableRel}; closing the sequence releases
everything the binding holds.
+ */
+ @VisibleForTesting
+ static Sequence<Object[]> enumerate(final Enumerable<?> enumerable)
+ {
+ final Sequence<Object[]> rows = new BaseSequence<>(
+ new BaseSequence.IteratorMaker<Object[], EnumeratorIterator>()
+ {
+ @Override
+ public EnumeratorIterator make()
+ {
+ return new EnumeratorIterator(enumerable.enumerator());
+ }
+
+ @Override
+ public void cleanup(EnumeratorIterator iterFromMake)
+ {
+ iterFromMake.enumerator.close();
+ }
+ }
+ );
+
+ if (!(enumerable instanceof AutoCloseable closeable)) {
+ return rows;
+ }
+ return Sequences.wrap(rows, new SequenceWrapper()
+ {
+ @Override
+ public void after(boolean isDone, Throwable thrown) throws Exception
+ {
+ closeable.close();
+ }
+ });
+ }
+
/**
* Construct a {@link PlannerResult} for an 'explain' query from a {@link
RelNode} and root {@link RelRoot}
*/
@@ -778,25 +784,25 @@ public abstract class QueryHandler extends
SqlStatementHandler.BaseStatementHand
}
}
- private static class EnumeratorIterator<T> implements Iterator<T>
+ private static class EnumeratorIterator implements Iterator<Object[]>
{
- private final Iterator<T> it;
+ private final Enumerator<?> enumerator;
- EnumeratorIterator(Iterator<T> it)
+ EnumeratorIterator(Enumerator<?> enumerator)
{
- this.it = it;
+ this.enumerator = enumerator;
}
@Override
public boolean hasNext()
{
- return it.hasNext();
+ return enumerator.moveNext();
}
@Override
- public T next()
+ public Object[] next()
{
- return it.next();
+ return (Object[]) enumerator.current();
}
}
}
diff --git
a/sql/src/main/java/org/apache/druid/sql/calcite/schema/MetadataSegmentView.java
b/sql/src/main/java/org/apache/druid/sql/calcite/schema/MetadataSegmentView.java
index b292e5e8e04..e623efedbef 100644
---
a/sql/src/main/java/org/apache/druid/sql/calcite/schema/MetadataSegmentView.java
+++
b/sql/src/main/java/org/apache/druid/sql/calcite/schema/MetadataSegmentView.java
@@ -29,9 +29,11 @@ import com.google.inject.Inject;
import org.apache.druid.client.BrokerSegmentWatcherConfig;
import org.apache.druid.client.DataSegmentInterner;
import org.apache.druid.client.coordinator.CoordinatorClient;
+import org.apache.druid.collections.ResourceHolder;
import org.apache.druid.common.guava.FutureUtils;
import org.apache.druid.concurrent.LifecycleLock;
import org.apache.druid.guice.ManageLifecycle;
+import org.apache.druid.java.util.common.CloseableIterators;
import org.apache.druid.java.util.common.ISE;
import org.apache.druid.java.util.common.Stopwatch;
import org.apache.druid.java.util.common.concurrent.Execs;
@@ -47,6 +49,7 @@ import org.apache.druid.metadata.segment.cache.Metric;
import org.apache.druid.timeline.DataSegment;
import org.apache.druid.timeline.SegmentId;
import org.apache.druid.timeline.SegmentStatusInCluster;
+import org.apache.druid.utils.CloseableUtils;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.joda.time.Duration;
@@ -154,26 +157,35 @@ public class MetadataSegmentView
{
log.info("Polling segments from coordinator");
final Stopwatch syncTime = Stopwatch.createStarted();
- final CloseableIterator<SegmentStatusInCluster> metadataSegments =
fetchSegmentMetadataFromCoordinator();
final ImmutableSortedSet.Builder<SegmentStatusInCluster> builder =
ImmutableSortedSet.naturalOrder();
- while (metadataSegments.hasNext()) {
- final SegmentStatusInCluster segment = metadataSegments.next();
- final DataSegment interned =
DataSegmentInterner.intern(segment.getDataSegment());
- Integer replicationFactor = segment.getReplicationFactor();
- if (replicationFactor == null) {
- replicationFactor =
segmentIdToReplicationFactor.getIfPresent(segment.getDataSegment().getId());
- } else {
- segmentIdToReplicationFactor.put(segment.getDataSegment().getId(),
segment.getReplicationFactor());
+ final ResourceHolder<Iterator<SegmentStatusInCluster>> metadataSegments =
fetchSegmentMetadataFromCoordinator();
+ try {
+ final Iterator<SegmentStatusInCluster> segments = metadataSegments.get();
+ while (segments.hasNext()) {
+ final SegmentStatusInCluster segment = segments.next();
+ final DataSegment interned =
DataSegmentInterner.intern(segment.getDataSegment());
+ Integer replicationFactor = segment.getReplicationFactor();
+ if (replicationFactor == null) {
+ replicationFactor =
segmentIdToReplicationFactor.getIfPresent(segment.getDataSegment().getId());
+ } else {
+ segmentIdToReplicationFactor.put(segment.getDataSegment().getId(),
segment.getReplicationFactor());
+ }
+ final SegmentStatusInCluster segmentStatusInCluster = new
SegmentStatusInCluster(
+ interned,
+ segment.isOvershadowed(),
+ replicationFactor,
+ segment.getNumRows(),
+ segment.isRealtime()
+ );
+ builder.add(segmentStatusInCluster);
}
- final SegmentStatusInCluster segmentStatusInCluster = new
SegmentStatusInCluster(
- interned,
- segment.isOvershadowed(),
- replicationFactor,
- segment.getNumRows(),
- segment.isRealtime()
+ }
+ finally {
+ CloseableUtils.closeAndSuppressExceptions(
+ metadataSegments,
+ e -> log.warn(e, "Failed to close the segment metadata response from
the Coordinator")
);
- builder.add(segmentStatusInCluster);
}
publishedSegments = builder.build();
cachePopulated.countDown();
@@ -187,7 +199,7 @@ public class MetadataSegmentView
* {@link BrokerSegmentMetadataCacheConfig#isMetadataSegmentCacheEnable()})
* OR by querying the Coordinator on the fly.
*/
- Iterator<SegmentStatusInCluster> getSegments()
+ CloseableIterator<SegmentStatusInCluster> getSegments()
{
return getSegments(null);
}
@@ -196,23 +208,27 @@ public class MetadataSegmentView
* Returns published (and, with centralized schema, realtime) segment
metadata, optionally
* restricted to {@code dataSources}.
*/
- Iterator<SegmentStatusInCluster> getSegments(@Nullable Set<String>
dataSources)
+ CloseableIterator<SegmentStatusInCluster> getSegments(@Nullable Set<String>
dataSources)
{
- final Iterator<SegmentStatusInCluster> base;
+ final CloseableIterator<SegmentStatusInCluster> base;
if (isCacheEnabled) {
Uninterruptibles.awaitUninterruptibly(cachePopulated);
- base = publishedSegments.iterator();
+ base = CloseableIterators.withEmptyBaggage(publishedSegments.iterator());
} else {
// Cache disabled: the Coordinator returns all used segments; filter
client-side to preserve semantics.
- base = fetchSegmentMetadataFromCoordinator();
+ final ResourceHolder<Iterator<SegmentStatusInCluster>> fetched =
fetchSegmentMetadataFromCoordinator();
+ base = CloseableIterators.wrap(fetched.get(), fetched);
}
return dataSources == null
? base
- : Iterators.filter(base, s ->
dataSources.contains(s.getDataSegment().getDataSource()));
+ : CloseableIterators.wrap(
+ Iterators.filter(base, s ->
dataSources.contains(s.getDataSegment().getDataSource())),
+ base
+ );
}
// Note that coordinator must be up to get segments
- private CloseableIterator<SegmentStatusInCluster>
fetchSegmentMetadataFromCoordinator()
+ private ResourceHolder<Iterator<SegmentStatusInCluster>>
fetchSegmentMetadataFromCoordinator()
{
// includeRealtimeSegments flag would additionally request realtime
segments
// note that realtime segments are returned only when
druid.centralizedDatasourceSchema.enabled is set on the Coordinator
diff --git
a/sql/src/main/java/org/apache/druid/sql/calcite/schema/SystemSchema.java
b/sql/src/main/java/org/apache/druid/sql/calcite/schema/SystemSchema.java
index 885fc1ca352..09e175d2415 100644
--- a/sql/src/main/java/org/apache/druid/sql/calcite/schema/SystemSchema.java
+++ b/sql/src/main/java/org/apache/druid/sql/calcite/schema/SystemSchema.java
@@ -55,6 +55,7 @@ import org.apache.druid.indexer.TaskStatusPlus;
import org.apache.druid.indexing.overlord.supervisor.SupervisorStatus;
import org.apache.druid.java.util.common.ISE;
import org.apache.druid.java.util.common.StringUtils;
+import org.apache.druid.java.util.common.io.Closer;
import org.apache.druid.java.util.common.jackson.JacksonUtils;
import org.apache.druid.java.util.common.parsers.CloseableIterator;
import org.apache.druid.java.util.http.client.HttpClient;
@@ -441,10 +442,11 @@ public class SystemSchema extends AbstractTableSchema
: new HashSet<>();
// Get segments from metadata segment cache (if enabled in SQL planner
config), else directly from
- // Coordinator. This may include both published and realtime segments.
- final Iterator<SegmentStatusInCluster> metadataStoreSegments =
metadataView.getSegments(dataSourceFilter);
+ // Coordinator. This may include both published and realtime segments.
Fetched on first read, so a scan that is
+ // never read holds no Coordinator response.
+ final Closer closer = Closer.create();
final FluentIterable<Object[]> publishedSegments = FluentIterable
- .from(() -> getAuthorizedPublishedSegments(metadataStoreSegments))
+ .from(() ->
getAuthorizedPublishedSegments(closer.register(metadataView.getSegments(dataSourceFilter))))
.transform(val -> {
final DataSegment segment = val.getDataSegment();
final AvailableSegmentMetadata availableSegmentMetadata =
@@ -549,7 +551,7 @@ public class SystemSchema extends AbstractTableSchema
Iterables.concat(publishedSegments, availableSegments)
);
- return Linq4j.asEnumerable(allSegments)
+ return Linq4j.asEnumerable(() -> wrap(allSegments.iterator(), closer))
.where(Objects::nonNull)
.select(row -> projectSegmentsRow(row, projects,
jsonMapper));
}
diff --git
a/sql/src/test/java/org/apache/druid/sql/calcite/planner/QueryHandlerTest.java
b/sql/src/test/java/org/apache/druid/sql/calcite/planner/QueryHandlerTest.java
new file mode 100644
index 00000000000..93d5f35c68b
--- /dev/null
+++
b/sql/src/test/java/org/apache/druid/sql/calcite/planner/QueryHandlerTest.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.druid.sql.calcite.planner;
+
+import org.apache.calcite.interpreter.Interpreter;
+import org.apache.calcite.linq4j.Enumerator;
+import org.apache.calcite.linq4j.Linq4j;
+import org.apache.druid.java.util.common.ISE;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import java.util.List;
+
+public class QueryHandlerTest
+{
+ private final Interpreter interpreter = Mockito.mock(Interpreter.class);
+
+ @Test
+ public void
testEnumerateClosesTheEnumeratorAndTheInterpreterOnceTheResultsAreConsumed()
+ {
+ final Enumerator<Object[]> enumerator =
Mockito.spy(Linq4j.enumerator(List.<Object[]>of(new Object[]{1L})));
+ Mockito.when(interpreter.enumerator()).thenReturn(enumerator);
+
+ Assertions.assertEquals(1,
QueryHandler.enumerate(interpreter).toList().size());
+
+ Mockito.verify(enumerator).close();
+ Mockito.verify(interpreter).close();
+ }
+
+ @Test
+ public void testEnumerateClosesTheInterpreterIfEnumerationFails()
+ {
+ Mockito.when(interpreter.enumerator()).thenThrow(new ISE("Interpreter node
failed"));
+
+ Assertions.assertThrows(ISE.class, () ->
QueryHandler.enumerate(interpreter).toList());
+
+ Mockito.verify(interpreter).close();
+ }
+}
diff --git
a/sql/src/test/java/org/apache/druid/sql/calcite/schema/MetadataSegmentViewTest.java
b/sql/src/test/java/org/apache/druid/sql/calcite/schema/MetadataSegmentViewTest.java
index 08b8d111b96..b84ef6818fe 100644
---
a/sql/src/test/java/org/apache/druid/sql/calcite/schema/MetadataSegmentViewTest.java
+++
b/sql/src/test/java/org/apache/druid/sql/calcite/schema/MetadataSegmentViewTest.java
@@ -23,7 +23,9 @@ import com.google.common.collect.Iterators;
import com.google.common.util.concurrent.Futures;
import org.apache.druid.client.BrokerSegmentWatcherConfig;
import org.apache.druid.client.coordinator.CoordinatorClient;
-import org.apache.druid.java.util.common.CloseableIterators;
+import org.apache.druid.collections.ResourceHolder;
+import org.apache.druid.collections.StupidResourceHolder;
+import org.apache.druid.java.util.common.ISE;
import org.apache.druid.java.util.metrics.NoopTaskHolder;
import org.apache.druid.metadata.segment.cache.Metric;
import org.apache.druid.segment.TestDataSource;
@@ -31,14 +33,23 @@ import
org.apache.druid.server.coordinator.CreateDataSegments;
import org.apache.druid.server.metrics.LatchableEmitter;
import org.apache.druid.server.metrics.LatchableEmitterConfig;
import org.apache.druid.timeline.SegmentStatusInCluster;
+import org.apache.druid.utils.CloseableUtils;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentMatchers;
import org.mockito.Mockito;
+import java.io.Closeable;
+import java.io.IOException;
import java.util.ArrayList;
+import java.util.Iterator;
import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
public class MetadataSegmentViewTest
{
@@ -84,7 +95,7 @@ public class MetadataSegmentViewTest
ArgumentMatchers.eq(true)
)
).thenReturn(
-
Futures.immediateFuture(CloseableIterators.withEmptyBaggage(expectedSegments.iterator()))
+
Futures.immediateFuture(StupidResourceHolder.create(expectedSegments.iterator()))
);
// Start the test target and wait for it to sync with the Coordinator
@@ -96,4 +107,124 @@ public class MetadataSegmentViewTest
Assertions.assertEquals(10, observedSegments.size());
Assertions.assertEquals(expectedSegments, observedSegments);
}
+
+ @Test
+ public void test_poll_releasesTheCoordinatorResponse()
+ {
+ final AtomicBoolean closed = new AtomicBoolean(false);
+
+ Mockito.when(
+ coordinatorClient.fetchAllUsedSegmentsWithOvershadowedStatus(
+ ArgumentMatchers.eq(null),
+ ArgumentMatchers.eq(true)
+ )
+ ).thenAnswer(
+ invocation -> Futures.immediateFuture(
+ holder(List.<SegmentStatusInCluster>of().iterator(), () ->
closed.set(true))
+ )
+ );
+
+ segmentView.start();
+ emitter.waitForEvent(event ->
event.hasMetricName(Metric.SYNC_DURATION_MILLIS));
+
+ Assertions.assertTrue(closed.get(), "poll must close the streamed
response");
+ }
+
+ @Test
+ public void test_poll_releasesTheCoordinatorResponse_whenIterationFails()
throws Exception
+ {
+ final CountDownLatch closed = new CountDownLatch(1);
+
+ final Iterator<SegmentStatusInCluster> failsMidStream = new Iterator<>()
+ {
+ @Override
+ public boolean hasNext()
+ {
+ throw new ISE("Coordinator response ended mid-stream");
+ }
+
+ @Override
+ public SegmentStatusInCluster next()
+ {
+ throw new NoSuchElementException();
+ }
+ };
+
+ Mockito.when(
+ coordinatorClient.fetchAllUsedSegmentsWithOvershadowedStatus(
+ ArgumentMatchers.eq(null),
+ ArgumentMatchers.eq(true)
+ )
+ ).thenAnswer(
+ invocation -> Futures.immediateFuture(holder(failsMidStream,
closed::countDown))
+ );
+
+ segmentView.start();
+
+ try {
+ Assertions.assertTrue(
+ closed.await(30, TimeUnit.SECONDS),
+ "a failed poll must still release the streamed response"
+ );
+ }
+ finally {
+ segmentView.stop();
+ }
+ }
+
+ @Test
+ public void
test_getSegments_releasesTheCoordinatorResponse_ifCacheIsDisabledAndFiltered()
throws IOException
+ {
+ final AtomicBoolean closed = new AtomicBoolean(false);
+
+ Mockito.when(
+ coordinatorClient.fetchAllUsedSegmentsWithOvershadowedStatus(
+ ArgumentMatchers.eq(null),
+ ArgumentMatchers.eq(true)
+ )
+ ).thenReturn(
+ Futures.immediateFuture(
+ holder(List.<SegmentStatusInCluster>of().iterator(), () ->
closed.set(true))
+ )
+ );
+
+ final MetadataSegmentView uncachedView = new MetadataSegmentView(
+ coordinatorClient,
+ new BrokerSegmentWatcherConfig(),
+ new BrokerSegmentMetadataCacheConfig()
+ {
+ @Override
+ public boolean isMetadataSegmentCacheEnable()
+ {
+ return false;
+ }
+ },
+ emitter
+ );
+
+ uncachedView.getSegments(Set.of(TestDataSource.WIKI)).close();
+
+ Assertions.assertTrue(closed.get());
+ }
+
+ private static ResourceHolder<Iterator<SegmentStatusInCluster>> holder(
+ final Iterator<SegmentStatusInCluster> segments,
+ final Closeable response
+ )
+ {
+ return new ResourceHolder<>()
+ {
+ @Override
+ public Iterator<SegmentStatusInCluster> get()
+ {
+ return segments;
+ }
+
+ @Override
+ public void close()
+ {
+ CloseableUtils.closeAndWrapExceptions(response);
+ }
+ };
+ }
}
diff --git
a/sql/src/test/java/org/apache/druid/sql/calcite/schema/SystemSchemaTest.java
b/sql/src/test/java/org/apache/druid/sql/calcite/schema/SystemSchemaTest.java
index ea3e981a8d9..28945c6c066 100644
---
a/sql/src/test/java/org/apache/druid/sql/calcite/schema/SystemSchemaTest.java
+++
b/sql/src/test/java/org/apache/druid/sql/calcite/schema/SystemSchemaTest.java
@@ -35,6 +35,7 @@ import io.netty.handler.codec.http.HttpVersion;
import org.apache.calcite.DataContext;
import org.apache.calcite.adapter.java.JavaTypeFactory;
import org.apache.calcite.jdbc.JavaTypeFactoryImpl;
+import org.apache.calcite.linq4j.Enumerator;
import org.apache.calcite.linq4j.QueryProvider;
import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rel.type.RelDataTypeField;
@@ -153,6 +154,7 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
public class SystemSchemaTest extends CalciteTestBase
{
@@ -707,7 +709,9 @@ public class SystemSchemaTest extends CalciteTestBase
new SegmentStatusInCluster(segment2, false, 0, null, false)
));
-
EasyMock.expect(metadataView.getSegments(EasyMock.anyObject())).andReturn(publishedSegments.iterator()).once();
+ EasyMock.expect(metadataView.getSegments(EasyMock.anyObject()))
+
.andReturn(CloseableIterators.withEmptyBaggage(publishedSegments.iterator()))
+ .once();
EasyMock.replay(request, responseHolder, responseHandler, metadataView);
DataContext dataContext = createDataContext();
@@ -828,7 +832,9 @@ public class SystemSchemaTest extends CalciteTestBase
new SegmentStatusInCluster(segment2, false, 0, null, false)
));
-
EasyMock.expect(metadataView.getSegments(EasyMock.anyObject())).andReturn(publishedSegments.iterator()).once();
+ EasyMock.expect(metadataView.getSegments(EasyMock.anyObject()))
+
.andReturn(CloseableIterators.withEmptyBaggage(publishedSegments.iterator()))
+ .once();
EasyMock.replay(request, responseHolder, responseHandler, metadataView);
DataContext dataContext = createDataContext();
@@ -884,6 +890,42 @@ public class SystemSchemaTest extends CalciteTestBase
);
}
+ @Test
+ public void testSegmentsTableReleasesTheCoordinatorResponseWhenClosedEarly()
+ {
+ final SegmentsTable segmentsTable =
+ new SegmentsTable(segmentMetadataCache, metadataView, MAPPER,
authMapper, createAuthResult(Users.SUPER));
+ final List<SegmentStatusInCluster> publishedSegments = List.of(
+ new SegmentStatusInCluster(segment1, true, 2, null, false),
+ new SegmentStatusInCluster(segment2, false, 0, null, false)
+ );
+ final AtomicBoolean closed = new AtomicBoolean(false);
+
+ EasyMock.expect(metadataView.getSegments(EasyMock.anyObject()))
+ .andReturn(CloseableIterators.wrap(publishedSegments.iterator(),
() -> closed.set(true)))
+ .once();
+
+ EasyMock.replay(request, responseHolder, responseHandler, metadataView);
+ final Enumerator<Object[]> rows =
+ segmentsTable.scan(createDataContext(), Collections.emptyList(),
null).enumerator();
+ Assertions.assertTrue(rows.moveNext());
+ rows.close();
+
+ Assertions.assertTrue(closed.get());
+ }
+
+ @Test
+ public void testSegmentsTableDefersTheCoordinatorFetchUntilRowsAreRead()
+ {
+ final SegmentsTable segmentsTable =
+ new SegmentsTable(segmentMetadataCache, metadataView, MAPPER,
authMapper, createAuthResult(Users.SUPER));
+
+ EasyMock.replay(request, responseHolder, responseHandler, metadataView);
+ segmentsTable.scan(createDataContext(), Collections.emptyList(),
null).enumerator().close();
+
+ EasyMock.verify(metadataView);
+ }
+
@Test
public void testServersTable() throws URISyntaxException
{
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]