This is an automated email from the ASF dual-hosted git repository.
FrankChen021 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 0387ffa9028 fix: stabilize task queue and ingestion CI tests (#20291)
0387ffa9028 is described below
commit 0387ffa90287033948275b9d3a727b0c27258cc9
Author: Frank Chen <[email protected]>
AuthorDate: Sat Sep 26 12:00:39 2026 +0800
fix: stabilize task queue and ingestion CI tests (#20291)
* fix: stabilize task queue and ingestion CI tests
* test: drain ingestion smoke test tasks during cleanup
* fix: compile supervisor autoscaling tests
* fix: clear stale datasource rebuild state on removal
* fix: satisfy guarded datasource rebuild cleanup
* fix: emit dataSource/removed metric at most once per table removal
The last-segment callback and an in-flight refresh can both observe the
same datasource removal. Gate the metric on the tables.remove() result in
both paths so exactly one segment/schemaCache/dataSource/removed event is
emitted, regardless of which path removes the table first.
* fix: guard the empty-signature removal metric as well
buildDataSourceRowSignature returns an empty (non-null) signature when a
datasource only has unrefreshed segments. That branch of refresh() still
emitted dataSource/removed unconditionally, so a last-segment callback
racing an in-flight refresh could report the removal twice. Apply the same
tables.remove() guard, and add a latch-controlled test for the interleaving
plus one for a never-initialised datasource.
---
.../embedded/indexing/IngestionSmokeTest.java | 54 ++++++-
.../indexing/KafkaBoundedSupervisorTest.java | 25 +++-
.../indexing/overlord/TaskQueueScaleTest.java | 20 +--
.../metadata/AbstractSegmentMetadataCache.java | 21 ++-
.../calcite/schema/BrokerSegmentMetadataCache.java | 37 +++--
.../schema/BrokerSegmentMetadataCacheTest.java | 165 +++++++++++++++++++++
6 files changed, 296 insertions(+), 26 deletions(-)
diff --git
a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/indexing/IngestionSmokeTest.java
b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/indexing/IngestionSmokeTest.java
index 2d22e80960b..b05c4731fb6 100644
---
a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/indexing/IngestionSmokeTest.java
+++
b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/indexing/IngestionSmokeTest.java
@@ -24,6 +24,7 @@ import org.apache.commons.io.IOUtils;
import org.apache.druid.common.utils.IdUtils;
import org.apache.druid.data.input.impl.CsvInputFormat;
import org.apache.druid.data.input.impl.TimestampSpec;
+import org.apache.druid.indexer.TaskStatusPlus;
import org.apache.druid.indexing.common.task.CompactionTask;
import org.apache.druid.indexing.common.task.IndexTask;
import org.apache.druid.indexing.common.task.NoopTask;
@@ -33,6 +34,9 @@ import org.apache.druid.indexing.kafka.KafkaIndexTaskModule;
import org.apache.druid.indexing.kafka.simulate.KafkaResource;
import org.apache.druid.indexing.kafka.supervisor.KafkaSupervisorSpec;
import org.apache.druid.indexing.overlord.Segments;
+import org.apache.druid.indexing.overlord.TaskMaster;
+import org.apache.druid.indexing.overlord.TaskRunner;
+import org.apache.druid.indexing.overlord.TaskRunnerWorkItem;
import org.apache.druid.indexing.overlord.supervisor.SupervisorStatus;
import org.apache.druid.java.util.common.DateTimes;
import org.apache.druid.java.util.common.Intervals;
@@ -40,6 +44,7 @@ import org.apache.druid.java.util.common.StringUtils;
import
org.apache.druid.metadata.storage.postgresql.PostgreSQLMetadataStorageModule;
import org.apache.druid.query.DruidMetrics;
import org.apache.druid.query.http.SqlTaskStatus;
+import org.apache.druid.rpc.indexing.OverlordClient;
import org.apache.druid.segment.metadata.Metric;
import org.apache.druid.tasklogs.TaskLogStreamer;
import org.apache.druid.testing.embedded.EmbeddedBroker;
@@ -67,6 +72,7 @@ import org.junit.jupiter.api.Test;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -145,7 +151,51 @@ public class IngestionSmokeTest extends
EmbeddedClusterTestBase
@AfterEach
public void cleanUp()
{
- markSegmentsAsUnused(dataSource);
+ try {
+ final List<SupervisorStatus> supervisors = new ArrayList<>();
+
cluster.callApi().onLeaderOverlord(OverlordClient::supervisorStatuses).forEachRemaining(supervisors::add);
+ for (final SupervisorStatus supervisor : supervisors) {
+ if (dataSource.equals(supervisor.getId())) {
+ cluster.callApi().onLeaderOverlord(o ->
o.terminateSupervisor(supervisor.getId()));
+ }
+ }
+
+ cluster.callApi()
+ .waitForResult(this::cancelTasksForCurrentTest, Set::isEmpty)
+ .withTimeoutMillis(60_000)
+ .go();
+ }
+ finally {
+ markSegmentsAsUnused(dataSource);
+ }
+ }
+
+ private Set<String> cancelTasksForCurrentTest()
+ {
+ final Set<String> taskIds = new HashSet<>();
+ for (final String state : List.of("running", "pending", "waiting")) {
+ for (final TaskStatusPlus task : cluster.callApi().getTasks(dataSource,
state)) {
+ taskIds.add(task.getId());
+ }
+ }
+
+ // A task can be complete in storage while still occupying a worker slot.
+ // Docker subclasses may use an external leader, so its runner is not
available here.
+ final Optional<TaskRunner> taskRunner =
overlord.bindings().getInstance(TaskMaster.class).getTaskRunner();
+ if (taskRunner.isPresent()) {
+ final List<TaskRunnerWorkItem> runnerTasks = new
ArrayList<>(taskRunner.get().getPendingTasks());
+ runnerTasks.addAll(taskRunner.get().getRunningTasks());
+ for (final TaskRunnerWorkItem task : runnerTasks) {
+ if (dataSource.equals(task.getDataSource())) {
+ taskIds.add(task.getTaskId());
+ }
+ }
+ }
+
+ for (final String taskId : taskIds) {
+ cluster.callApi().onLeaderOverlord(o -> o.cancelTask(taskId));
+ }
+ return taskIds;
}
protected int markSegmentsAsUnused(String dataSource)
@@ -363,7 +413,7 @@ public class IngestionSmokeTest extends
EmbeddedClusterTestBase
final String taskId = IdUtils.getRandomId();
final long runDurationMillis = 100_000L;
cluster.callApi().onLeaderOverlord(
- o -> o.runTask(taskId, new NoopTask(taskId, null, null,
runDurationMillis, 0L, null))
+ o -> o.runTask(taskId, new NoopTask(taskId, null, dataSource,
runDurationMillis, 0L, null))
);
eventCollector.latchableEmitter().waitForEvent(
diff --git
a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/indexing/KafkaBoundedSupervisorTest.java
b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/indexing/KafkaBoundedSupervisorTest.java
index 79214ab88c1..7f83375bc0e 100644
---
a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/indexing/KafkaBoundedSupervisorTest.java
+++
b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/indexing/KafkaBoundedSupervisorTest.java
@@ -29,11 +29,18 @@ import
org.apache.druid.indexing.seekablestream.supervisor.BoundedStreamConfig;
import org.apache.druid.query.DruidMetrics;
import org.apache.druid.testing.embedded.EmbeddedDruidCluster;
import org.apache.druid.testing.embedded.StreamIngestResource;
+import org.apache.druid.testing.embedded.tools.EventSerializer;
+import org.apache.druid.testing.embedded.tools.JsonEventSerializer;
+import org.apache.druid.testing.embedded.tools.StreamGenerator;
+import
org.apache.druid.testing.embedded.tools.WikipediaStreamEventStreamGenerator;
+import org.apache.kafka.clients.producer.ProducerRecord;
import org.joda.time.Period;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
+import java.util.ArrayList;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
/**
@@ -203,12 +210,26 @@ public class KafkaBoundedSupervisorTest extends
StreamIndexTestBase
.build(dataSource, topic);
}
+ private void publishRecordsToBothPartitions(String topic)
+ {
+ final EventSerializer serializer = new
JsonEventSerializer(overlord.bindings().jsonMapper());
+ final StreamGenerator generator = new
WikipediaStreamEventStreamGenerator(serializer, 100, 100);
+ final List<byte[]> records = generator.generateEvents(10);
+ final List<ProducerRecord<byte[], byte[]>> producerRecords = new
ArrayList<>();
+ // Fixed per-partition end offsets require data in both partitions;
Kafka's default partitioner need not balance it.
+ for (int i = 0; i < records.size(); i++) {
+ producerRecords.add(new ProducerRecord<>(topic, i % 2, null,
records.get(i)));
+ }
+ kafkaServer.produceRecordsWithoutTransaction(producerRecords);
+ Assertions.assertEquals(Map.of("0", 500L, "1", 500L),
kafkaServer.getPartitionOffsets(topic));
+ }
+
@Test
public void test_boundedSupervisor_withMismatchedMetadata_is_unhealthy()
{
final String topic = IdUtils.getRandomId();
kafkaServer.createTopicWithPartitions(topic, 2);
- publish1kRecords(topic, false);
+ publishRecordsToBothPartitions(topic);
// Get the current end offsets for all partitions
Map<String, Long> currentOffsets = kafkaServer.getPartitionOffsets(topic);
@@ -282,7 +303,7 @@ public class KafkaBoundedSupervisorTest extends
StreamIndexTestBase
{
final String topic = IdUtils.getRandomId();
kafkaServer.createTopicWithPartitions(topic, 2);
- publish1kRecords(topic, false);
+ publishRecordsToBothPartitions(topic);
// Run 1: ingest up to offset 100 on each partition and complete.
Map<String, Long> startOffsets1 = new HashMap<>();
diff --git
a/indexing-service/src/test/java/org/apache/druid/indexing/overlord/TaskQueueScaleTest.java
b/indexing-service/src/test/java/org/apache/druid/indexing/overlord/TaskQueueScaleTest.java
index 04670ba28a3..e0c21df78b3 100644
---
a/indexing-service/src/test/java/org/apache/druid/indexing/overlord/TaskQueueScaleTest.java
+++
b/indexing-service/src/test/java/org/apache/druid/indexing/overlord/TaskQueueScaleTest.java
@@ -179,12 +179,8 @@ public class TaskQueueScaleTest
taskQueue.add(testTask);
}
- // in theory we can get a race here, since we fetch the counts at separate
times
+ // The running, pending, and waiting counters are separate snapshots and
cannot be summed while tasks transition.
Assertions.assertEquals(numTasks, taskQueue.getTasks().size(), "all tasks
should be known");
- long runningTasks =
taskQueue.getRunningTaskCount().values().stream().mapToLong(Long::longValue).sum();
- long pendingTasks =
taskQueue.getPendingTaskCount().values().stream().mapToLong(Long::longValue).sum();
- long waitingTasks =
taskQueue.getWaitingTaskCount().values().stream().mapToLong(Long::longValue).sum();
- Assertions.assertEquals(numTasks, (runningTasks + pendingTasks +
waitingTasks), "all tasks should be known");
// Wait for all tasks to finish.
final TaskLookup.CompleteTaskLookup completeTaskLookup =
@@ -194,12 +190,18 @@ public class TaskQueueScaleTest
Thread.sleep(100);
}
- Thread.sleep(100);
+ // Completion is persisted before cleanup finishes. The test timeout
bounds this wait.
+ while (!taskStorage.getActiveTasks().isEmpty()
+ || taskQueue.getRunningTaskCount().values().stream().anyMatch(count
-> count != 0)
+ || taskQueue.getPendingTaskCount().values().stream().anyMatch(count
-> count != 0)
+ || taskQueue.getWaitingTaskCount().values().stream().anyMatch(count
-> count != 0)) {
+ Thread.sleep(100);
+ }
Assertions.assertEquals(0, taskStorage.getActiveTasks().size(), "no tasks
should be active");
- runningTasks =
taskQueue.getRunningTaskCount().values().stream().mapToLong(Long::longValue).sum();
- pendingTasks =
taskQueue.getPendingTaskCount().values().stream().mapToLong(Long::longValue).sum();
- waitingTasks =
taskQueue.getWaitingTaskCount().values().stream().mapToLong(Long::longValue).sum();
+ final long runningTasks =
taskQueue.getRunningTaskCount().values().stream().mapToLong(Long::longValue).sum();
+ final long pendingTasks =
taskQueue.getPendingTaskCount().values().stream().mapToLong(Long::longValue).sum();
+ final long waitingTasks =
taskQueue.getWaitingTaskCount().values().stream().mapToLong(Long::longValue).sum();
Assertions.assertEquals(0, runningTasks, "no tasks should be running");
Assertions.assertEquals(0, pendingTasks, "no tasks should be pending");
Assertions.assertEquals(0, waitingTasks, "no tasks should be waiting");
diff --git
a/server/src/main/java/org/apache/druid/segment/metadata/AbstractSegmentMetadataCache.java
b/server/src/main/java/org/apache/druid/segment/metadata/AbstractSegmentMetadataCache.java
index d8677fba952..34d1e755e2f 100644
---
a/server/src/main/java/org/apache/druid/segment/metadata/AbstractSegmentMetadataCache.java
+++
b/server/src/main/java/org/apache/druid/segment/metadata/AbstractSegmentMetadataCache.java
@@ -591,7 +591,7 @@ public abstract class AbstractSegmentMetadataCache<T
extends DataSourceInformati
segmentsNeedingRefresh.remove(segment.getId());
unmarkSegmentAsMutable(segment.getId());
- segmentMetadataInfo.compute(
+ final ConcurrentSkipListMap<SegmentId, AvailableSegmentMetadata>
remainingSegments = segmentMetadataInfo.compute(
segment.getDataSource(),
(dataSource, segmentsMap) -> {
if (segmentsMap == null) {
@@ -605,7 +605,11 @@ public abstract class AbstractSegmentMetadataCache<T
extends DataSourceInformati
}
removeSegmentAction(segment.getId());
if (segmentsMap.isEmpty()) {
- tables.remove(segment.getDataSource());
+ // Emit the removal action only if this call actually removed
the table, so that a concurrent
+ // refresh which also finds the datasource gone cannot report
the same removal twice.
+ if (tables.remove(segment.getDataSource()) != null) {
+ removeDataSourceAction(segment.getDataSource());
+ }
log.info("dataSource [%s] no longer exists, all metadata
removed.", segment.getDataSource());
return null;
} else {
@@ -615,6 +619,9 @@ public abstract class AbstractSegmentMetadataCache<T
extends DataSourceInformati
}
}
);
+ if (remainingSegments == null) {
+ dataSourcesNeedingRebuild.remove(segment.getDataSource());
+ }
lock.notifyAll();
}
@@ -625,6 +632,16 @@ public abstract class AbstractSegmentMetadataCache<T
extends DataSourceInformati
*/
protected abstract void removeSegmentAction(SegmentId segmentId);
+ /**
+ * Called under the cache lock after the last segment of a datasource has
been removed and its table was actually
+ * removed from {@link #tables} by that removal. It is not called when no
table existed for the datasource, so a
+ * single datasource removal triggers this action at most once even if a
refresh observes the removal concurrently.
+ */
+ protected void removeDataSourceAction(String dataSource)
+ {
+ // No additional action by default.
+ }
+
@VisibleForTesting
public void removeServerSegment(final DruidServerMetadata server, final
DataSegment segment)
{
diff --git
a/sql/src/main/java/org/apache/druid/sql/calcite/schema/BrokerSegmentMetadataCache.java
b/sql/src/main/java/org/apache/druid/sql/calcite/schema/BrokerSegmentMetadataCache.java
index 26a6c63660d..0baa5294f56 100644
---
a/sql/src/main/java/org/apache/druid/sql/calcite/schema/BrokerSegmentMetadataCache.java
+++
b/sql/src/main/java/org/apache/druid/sql/calcite/schema/BrokerSegmentMetadataCache.java
@@ -251,11 +251,11 @@ public class BrokerSegmentMetadataCache extends
AbstractSegmentMetadataCache<Phy
final RowSignature rowSignature =
buildDataSourceRowSignature(dataSource);
if (rowSignature == null) {
log.info("datasource [%s] no longer exists, all metadata removed.",
dataSource);
- tables.remove(dataSource);
- emitMetric(
- Metric.DATASOURCE_REMOVED,
- 1,
- ServiceMetricEvent.builder().setDimension(DruidMetrics.DATASOURCE,
dataSource));
+ // The last-segment callback may already have removed the table and
emitted the metric while this refresh
+ // was in flight. Only emit if this refresh is the one that actually
removed the table.
+ if (tables.remove(dataSource) != null) {
+ emitDataSourceRemoved(dataSource);
+ }
continue;
}
@@ -264,12 +264,11 @@ public class BrokerSegmentMetadataCache extends
AbstractSegmentMetadataCache<Phy
// and a new datasource is added
log.info("datasource [%s] schema has not been initialized yet, "
+ "check coordinator logs if this message is persistent.",
dataSource);
- // this is a harmless call
- tables.remove(dataSource);
- emitMetric(
- Metric.DATASOURCE_REMOVED,
- 1,
- ServiceMetricEvent.builder().setDimension(DruidMetrics.DATASOURCE,
dataSource));
+ // Usually there is no table to remove here. If there was one, a
concurrent last-segment callback may have
+ // removed it and emitted the metric already, so only emit if this
refresh actually removed the table.
+ if (tables.remove(dataSource) != null) {
+ emitDataSourceRemoved(dataSource);
+ }
continue;
}
@@ -307,6 +306,22 @@ public class BrokerSegmentMetadataCache extends
AbstractSegmentMetadataCache<Phy
// noop, no additional action needed when segment is removed.
}
+ @Override
+ protected void removeDataSourceAction(String dataSource)
+ {
+ // The last-segment callback can remove the table without another schema
refresh.
+ emitDataSourceRemoved(dataSource);
+ }
+
+ private void emitDataSourceRemoved(String dataSource)
+ {
+ emitMetric(
+ Metric.DATASOURCE_REMOVED,
+ 1,
+ ServiceMetricEvent.builder().setDimension(DruidMetrics.DATASOURCE,
dataSource)
+ );
+ }
+
private Set<String> queryDataSources()
{
Set<String> dataSources = new HashSet<>();
diff --git
a/sql/src/test/java/org/apache/druid/sql/calcite/schema/BrokerSegmentMetadataCacheTest.java
b/sql/src/test/java/org/apache/druid/sql/calcite/schema/BrokerSegmentMetadataCacheTest.java
index 55abaf7b4a3..57e10f07a5b 100644
---
a/sql/src/test/java/org/apache/druid/sql/calcite/schema/BrokerSegmentMetadataCacheTest.java
+++
b/sql/src/test/java/org/apache/druid/sql/calcite/schema/BrokerSegmentMetadataCacheTest.java
@@ -34,6 +34,7 @@ import org.apache.calcite.rel.type.RelDataTypeField;
import org.apache.calcite.sql.type.SqlTypeName;
import org.apache.druid.client.ImmutableDruidServer;
import org.apache.druid.client.InternalQueryConfig;
+import org.apache.druid.client.TimelineServerView;
import org.apache.druid.client.coordinator.CoordinatorClient;
import org.apache.druid.client.coordinator.NoopCoordinatorClient;
import org.apache.druid.data.input.InputRow;
@@ -105,6 +106,8 @@ import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
public class BrokerSegmentMetadataCacheTest extends
BrokerSegmentMetadataCacheTestBase
@@ -720,6 +723,168 @@ public class BrokerSegmentMetadataCacheTest extends
BrokerSegmentMetadataCacheTe
Assertions.assertEquals(5, schema.getSegmentMetadataSnapshot().size());
}
+ @Test
+ public void testLastSegmentRemovalClearsRebuildState() throws IOException
+ {
+ final BrokerSegmentMetadataCache schema = new BrokerSegmentMetadataCache(
+ CalciteTests.createMockQueryLifecycleFactory(walker, conglomerate),
+ Mockito.mock(TimelineServerView.class),
+ SEGMENT_CACHE_CONFIG_DEFAULT,
+ new NoopEscalator(),
+ new InternalQueryConfig(),
+ emitter,
+ new PhysicalDatasourceMetadataFactory(globalTableJoinable,
segmentManager),
+ new NoopCoordinatorClient(),
+ CentralizedDatasourceSchemaConfig.create()
+ );
+ runningSchema = schema;
+ final List<DataSegment> segments = ImmutableList.of(segment1, segment2);
+ segments.forEach(segment ->
schema.addSegment(druidServers.get(0).getMetadata(), segment));
+ schema.refresh(
+ segments.stream().map(DataSegment::getId).collect(Collectors.toSet()),
+ new HashSet<>(Set.of("foo"))
+ );
+ Assertions.assertNotNull(schema.getDatasource("foo"));
+ emitter.flush();
+
+ // Removing a non-last segment marks the datasource for rebuild before the
last segment is removed.
+ schema.removeSegment(segments.get(0));
+ schema.removeSegment(segments.get(1));
+ Assertions.assertNull(schema.getDatasource("foo"));
+ emitter.verifyEmitted(Metric.DATASOURCE_REMOVED,
Map.of(DruidMetrics.DATASOURCE, "foo"), 1);
+
+ // A later refresh must not process stale rebuild state and emit the
removal metric again.
+ schema.refresh(new HashSet<>(), new HashSet<>());
+ emitter.verifyEmitted(Metric.DATASOURCE_REMOVED,
Map.of(DruidMetrics.DATASOURCE, "foo"), 1);
+
+ // A refresh that had already captured the datasource for rebuild before
the last segment was removed
+ // (or that is handed the datasource explicitly) finds no table left to
remove and must not emit again.
+ schema.refresh(new HashSet<>(), new HashSet<>(Set.of("foo")));
+ Assertions.assertNull(schema.getDatasource("foo"));
+ emitter.verifyEmitted(Metric.DATASOURCE_REMOVED,
Map.of(DruidMetrics.DATASOURCE, "foo"), 1);
+ }
+
+ @Test
+ public void testRefreshOfUnknownDatasourceDoesNotEmitRemovalMetric() throws
IOException
+ {
+ final BrokerSegmentMetadataCache schema = new BrokerSegmentMetadataCache(
+ CalciteTests.createMockQueryLifecycleFactory(walker, conglomerate),
+ Mockito.mock(TimelineServerView.class),
+ SEGMENT_CACHE_CONFIG_DEFAULT,
+ new NoopEscalator(),
+ new InternalQueryConfig(),
+ emitter,
+ new PhysicalDatasourceMetadataFactory(globalTableJoinable,
segmentManager),
+ new NoopCoordinatorClient(),
+ CentralizedDatasourceSchemaConfig.create()
+ );
+ runningSchema = schema;
+
+ // No table was ever built for this datasource, so there is nothing to
report as removed.
+ schema.refresh(new HashSet<>(), new HashSet<>(Set.of("never-existed")));
+ Assertions.assertNull(schema.getDatasource("never-existed"));
+ emitter.verifyNotEmitted(Metric.DATASOURCE_REMOVED);
+ }
+
+ @Test
+ public void testRefreshOfUninitializedDatasourceDoesNotEmitRemovalMetric()
throws IOException
+ {
+ final BrokerSegmentMetadataCache schema = new BrokerSegmentMetadataCache(
+ CalciteTests.createMockQueryLifecycleFactory(walker, conglomerate),
+ Mockito.mock(TimelineServerView.class),
+ SEGMENT_CACHE_CONFIG_DEFAULT,
+ new NoopEscalator(),
+ new InternalQueryConfig(),
+ emitter,
+ new PhysicalDatasourceMetadataFactory(globalTableJoinable,
segmentManager),
+ new NoopCoordinatorClient(),
+ CentralizedDatasourceSchemaConfig.create()
+ );
+ runningSchema = schema;
+
+ // The segment is known but has not been refreshed, so the datasource row
signature is empty and no table has
+ // ever been built. Rebuilding it must not report a removal.
+ schema.addSegment(druidServers.get(0).getMetadata(), segment1);
+ schema.refresh(new HashSet<>(), new HashSet<>(Set.of("foo")));
+ Assertions.assertNull(schema.getDatasource("foo"));
+ emitter.verifyNotEmitted(Metric.DATASOURCE_REMOVED);
+ }
+
+ @Test
+ public void
testLastSegmentRemovedDuringRefreshWithUninitializedSegmentEmitsRemovalOnce()
throws Exception
+ {
+ final AtomicBoolean blockSignatureBuild = new AtomicBoolean(false);
+ final CountDownLatch signatureBuilt = new CountDownLatch(1);
+ final CountDownLatch resumeRefresh = new CountDownLatch(1);
+ final BrokerSegmentMetadataCache schema = new BrokerSegmentMetadataCache(
+ CalciteTests.createMockQueryLifecycleFactory(walker, conglomerate),
+ Mockito.mock(TimelineServerView.class),
+ SEGMENT_CACHE_CONFIG_DEFAULT,
+ new NoopEscalator(),
+ new InternalQueryConfig(),
+ emitter,
+ new PhysicalDatasourceMetadataFactory(globalTableJoinable,
segmentManager),
+ new NoopCoordinatorClient(),
+ CentralizedDatasourceSchemaConfig.create()
+ )
+ {
+ @Override
+ public RowSignature buildDataSourceRowSignature(String dataSource)
+ {
+ final RowSignature rowSignature =
super.buildDataSourceRowSignature(dataSource);
+ if (blockSignatureBuild.get()) {
+ // Hold the refresh between computing the (empty) signature and
acting on it.
+ signatureBuilt.countDown();
+ try {
+ Assertions.assertTrue(resumeRefresh.await(WAIT_TIMEOUT_SECS,
TimeUnit.SECONDS));
+ }
+ catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new RuntimeException(e);
+ }
+ }
+ return rowSignature;
+ }
+ };
+ runningSchema = schema;
+
+ // Build the table from a refreshed segment.
+ schema.addSegment(druidServers.get(0).getMetadata(), segment1);
+ schema.refresh(new HashSet<>(Set.of(segment1.getId())), new
HashSet<>(Set.of("foo")));
+ Assertions.assertNotNull(schema.getDatasource("foo"));
+ emitter.flush();
+
+ // Leave only an unrefreshed segment behind, so the next rebuild sees an
empty (non-null) row signature.
+ schema.addSegment(druidServers.get(0).getMetadata(), segment2);
+ schema.removeSegment(segment1);
+
+ blockSignatureBuild.set(true);
+ final AtomicReference<Throwable> refreshError = new AtomicReference<>();
+ final Thread refreshThread = new Thread(() -> {
+ try {
+ schema.refresh(new HashSet<>(), new HashSet<>(Set.of("foo")));
+ }
+ catch (Throwable t) {
+ refreshError.set(t);
+ }
+ });
+ refreshThread.start();
+ Assertions.assertTrue(signatureBuilt.await(WAIT_TIMEOUT_SECS,
TimeUnit.SECONDS));
+
+ // The last segment disappears while the refresh is in flight: the
callback removes the table and emits once.
+ schema.removeSegment(segment2);
+ Assertions.assertNull(schema.getDatasource("foo"));
+ emitter.verifyEmitted(Metric.DATASOURCE_REMOVED,
Map.of(DruidMetrics.DATASOURCE, "foo"), 1);
+
+ // The refresh resumes, finds the table already gone and must not emit a
second removal.
+ resumeRefresh.countDown();
+ refreshThread.join(TimeUnit.SECONDS.toMillis(WAIT_TIMEOUT_SECS));
+ Assertions.assertFalse(refreshThread.isAlive(), "refresh did not finish");
+ Assertions.assertNull(refreshError.get());
+ Assertions.assertNull(schema.getDatasource("foo"));
+ emitter.verifyEmitted(Metric.DATASOURCE_REMOVED,
Map.of(DruidMetrics.DATASOURCE, "foo"), 1);
+ }
+
@Test
public void testAllDatasourcesRebuiltOnDatasourceRemoval() throws
IOException, InterruptedException
{
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]