This is an automated email from the ASF dual-hosted git repository.
abhishekrb 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 96be82a3e67 Clean up duty for non-overlapping eternity tombstones
(#15281)
96be82a3e67 is described below
commit 96be82a3e67bb3fdb62d0640c0c8885f7f5813b0
Author: Abhishek Radhakrishnan <[email protected]>
AuthorDate: Mon Dec 11 08:57:15 2023 -0800
Clean up duty for non-overlapping eternity tombstones (#15281)
* Add initial draft of MarkDanglingTombstonesAsUnused duty.
* Use overshadowed segments instead of all used segments.
* Add unit test for MarkDanglingSegmentsAsUnused duty.
* Add mock call
* Simplify code.
* Docs
* shorter lines formatting
* metric doc
* More tests, refactor and fix up some logic.
* update javadocs; other review comments.
* Make numCorePartitions as 0 in the TombstoneShardSpec.
* fix up test
* Add tombstone core partition tests
* Update docs/design/coordinator.md
Co-authored-by: 317brian <[email protected]>
* review comment
* Minor cleanup
* Only consider tombstones with 0 core partitions
* Need to register the test shard type to make jackson happy
* test comments
* checkstyle
* fixup misc typos in comments
* Update logic to use overshadowed segments
* minor cleanup
* Rename duty to eternity tombstone instead of dangling. Add test for full
eternity tombstone.
* Address review feedback.
---------
Co-authored-by: 317brian <[email protected]>
---
docs/design/coordinator.md | 9 +-
docs/operations/metrics.md | 1 +
.../druid/testing/utils/MsqTestQueryHelper.java | 8 +-
.../timeline/partition/OverwriteShardSpec.java | 2 +-
.../org/apache/druid/discovery/BrokerClient.java | 2 +-
.../druid/server/coordinator/DruidCoordinator.java | 2 +
.../server/coordinator/duty/CompactSegments.java | 4 +-
.../duty/MarkEternityTombstonesAsUnused.java | 178 ++++++++
.../druid/server/coordinator/stats/Stats.java | 2 +
.../duty/MarkEternityTombstonesAsUnusedTest.java | 501 +++++++++++++++++++++
10 files changed, 701 insertions(+), 8 deletions(-)
diff --git a/docs/design/coordinator.md b/docs/design/coordinator.md
index e3652d2c344..9cb2279d497 100644
--- a/docs/design/coordinator.md
+++ b/docs/design/coordinator.md
@@ -61,7 +61,7 @@ org.apache.druid.cli.Main server coordinator
Segments can be automatically loaded and dropped from the cluster based on a
set of rules. For more information on rules, see [Rule
Configuration](../operations/rule-configuration.md).
-## Cleaning up segments
+### Clean up overshadowed segments
On each run, the Coordinator compares the set of used segments in the database
with the segments served by some
Historical nodes in the cluster. The Coordinator sends requests to Historical
nodes to unload unused segments or segments
@@ -70,6 +70,13 @@ that are removed from the database.
Segments that are overshadowed (their versions are too old and their data has
been replaced by newer segments) are
marked as unused. During the next Coordinator's run, they will be unloaded
from Historical nodes in the cluster.
+### Clean up non-overshadowed eternity tombstone segments
+
+On each run, the Coordinator determines and cleans up unneeded eternity
tombstone segments for each datasource. These segments must fit all the
following criteria:
+- It is a tombstone segment that starts at -INF or ends at INF (for example, a
tombstone with an interval of `-146136543-09-08T08:23:32.096Z/2000-01-01` or
`2020-01-01/146140482-04-24T15:36:27.903Z` or
`-146136543-09-08T08:23:32.096Z/146140482-04-24T15:36:27.903Z`)
+- It does not overlap with any overshadowed segment
+- It has 0 core partitions
+
## Segment availability
If a Historical service restarts or becomes unavailable for any reason, the
Coordinator will notice a service has gone missing and treat all segments
served by that service as being dropped. Given a sufficient period of time, the
segments may be reassigned to other Historical services in the cluster.
However, each segment that is dropped is not immediately forgotten. Instead,
there is a transitional data structure that stores all dropped segments with an
associated lifetime. The lifetime [...]
diff --git a/docs/operations/metrics.md b/docs/operations/metrics.md
index df8ce218bde..8fefc8b6e13 100644
--- a/docs/operations/metrics.md
+++ b/docs/operations/metrics.md
@@ -329,6 +329,7 @@ These metrics are for the Druid Coordinator and are reset
each time the Coordina
|`segment/size`|Total size of used segments in a data source. Emitted only for
data sources to which at least one used segment belongs.|`dataSource`|Varies|
|`segment/count`|Number of used segments belonging to a data source. Emitted
only for data sources to which at least one used segment
belongs.|`dataSource`|< max|
|`segment/overShadowed/count`|Number of segments marked as unused due to being
overshadowed.| |Varies|
+|`segment/unneededEternityTombstone/count`|Number of non-overshadowed eternity
tombstones marked as unused.| |Varies|
|`segment/unavailable/count`|Number of unique segments left to load until all
used segments are available for queries.|`dataSource`|0|
|`segment/underReplicated/count`|Number of segments, including replicas, left
to load until all used segments are available for queries.|`tier`,
`dataSource`|0|
|`tier/historical/count`|Number of available historical nodes in each
tier.|`tier`|Varies|
diff --git
a/integration-tests/src/main/java/org/apache/druid/testing/utils/MsqTestQueryHelper.java
b/integration-tests/src/main/java/org/apache/druid/testing/utils/MsqTestQueryHelper.java
index 7525cb9d874..a7510db860f 100644
---
a/integration-tests/src/main/java/org/apache/druid/testing/utils/MsqTestQueryHelper.java
+++
b/integration-tests/src/main/java/org/apache/druid/testing/utils/MsqTestQueryHelper.java
@@ -130,9 +130,11 @@ public class MsqTestQueryHelper extends
AbstractTestQueryHelper<MsqQueryWithResu
HttpResponseStatus httpResponseStatus = statusResponseHolder.getStatus();
if (!httpResponseStatus.equals(HttpResponseStatus.ACCEPTED)) {
throw new ISE(
- "Unable to submit the task successfully. Received response status
code [%d], and response content:\n[%s]",
- httpResponseStatus,
- statusResponseHolder.getContent()
+ StringUtils.format(
+ "Unable to submit the task successfully. Received response
status code [%d], and response content:\n[%s]",
+ httpResponseStatus.getCode(),
+ statusResponseHolder.getContent()
+ )
);
}
String content = statusResponseHolder.getContent();
diff --git
a/processing/src/main/java/org/apache/druid/timeline/partition/OverwriteShardSpec.java
b/processing/src/main/java/org/apache/druid/timeline/partition/OverwriteShardSpec.java
index f51e688c24c..ec784c44ffb 100644
---
a/processing/src/main/java/org/apache/druid/timeline/partition/OverwriteShardSpec.java
+++
b/processing/src/main/java/org/apache/druid/timeline/partition/OverwriteShardSpec.java
@@ -30,7 +30,7 @@ public interface OverwriteShardSpec extends ShardSpec
{
/**
* The core partition concept is not used with segment locking. Instead, the
{@link AtomicUpdateGroup} is used
- * to atomically overshadow segments. Here, we always returns 0 so that the
{@link PartitionHolder} skips checking
+ * to atomically overshadow segments. Here, we always return 0 so that the
{@link PartitionHolder} skips checking
* the completeness of the core partitions.
*/
@Override
diff --git a/server/src/main/java/org/apache/druid/discovery/BrokerClient.java
b/server/src/main/java/org/apache/druid/discovery/BrokerClient.java
index bc97c2490ef..a64c8d670e4 100644
--- a/server/src/main/java/org/apache/druid/discovery/BrokerClient.java
+++ b/server/src/main/java/org/apache/druid/discovery/BrokerClient.java
@@ -116,7 +116,7 @@ public class BrokerClient
catch (MalformedURLException e) {
// Not an IOException; this is our own fault.
throw DruidException.defensive(
- "Failed to build url with path[%] and query string [%s].",
+ "Failed to build url with path[%s] and query string [%s].",
oldRequest.getUrl().getPath(),
oldRequest.getUrl().getQuery()
);
diff --git
a/server/src/main/java/org/apache/druid/server/coordinator/DruidCoordinator.java
b/server/src/main/java/org/apache/druid/server/coordinator/DruidCoordinator.java
index 2415fcaaa24..5558d204e81 100644
---
a/server/src/main/java/org/apache/druid/server/coordinator/DruidCoordinator.java
+++
b/server/src/main/java/org/apache/druid/server/coordinator/DruidCoordinator.java
@@ -67,6 +67,7 @@ import org.apache.druid.server.coordinator.duty.KillRules;
import org.apache.druid.server.coordinator.duty.KillStalePendingSegments;
import org.apache.druid.server.coordinator.duty.KillSupervisors;
import org.apache.druid.server.coordinator.duty.KillUnusedSegments;
+import org.apache.druid.server.coordinator.duty.MarkEternityTombstonesAsUnused;
import
org.apache.druid.server.coordinator.duty.MarkOvershadowedSegmentsAsUnused;
import org.apache.druid.server.coordinator.duty.PrepareBalancerAndLoadQueues;
import org.apache.druid.server.coordinator.duty.RunRules;
@@ -515,6 +516,7 @@ public class DruidCoordinator
new UpdateReplicationStatus(),
new UnloadUnusedSegments(loadQueueManager),
new MarkOvershadowedSegmentsAsUnused(segments ->
metadataManager.segments().markSegmentsAsUnused(segments)),
+ new MarkEternityTombstonesAsUnused(segments ->
metadataManager.segments().markSegmentsAsUnused(segments)),
new BalanceSegments(config.getCoordinatorPeriod()),
new CollectSegmentAndServerStats(taskMaster)
);
diff --git
a/server/src/main/java/org/apache/druid/server/coordinator/duty/CompactSegments.java
b/server/src/main/java/org/apache/druid/server/coordinator/duty/CompactSegments.java
index ab92f180149..bb88b86dbf8 100644
---
a/server/src/main/java/org/apache/druid/server/coordinator/duty/CompactSegments.java
+++
b/server/src/main/java/org/apache/druid/server/coordinator/duty/CompactSegments.java
@@ -373,7 +373,7 @@ public class CompactSegments implements
CoordinatorCustomDuty
final String dataSourceName = segmentsToCompact.get(0).getDataSource();
- // As these segments will be compacted, we will aggregates the statistic
to the Compacted statistics
+ // As these segments will be compacted, we will aggregate the statistic
to the Compacted statistics
AutoCompactionSnapshot.Builder snapshotBuilder =
currentRunAutoCompactionSnapshotBuilders.computeIfAbsent(
dataSourceName,
k -> new AutoCompactionSnapshot.Builder(k,
AutoCompactionSnapshot.AutoCompactionScheduleStatus.RUNNING)
@@ -395,7 +395,7 @@ public class CompactSegments implements
CoordinatorCustomDuty
Granularity segmentGranularityToUse = null;
if (config.getGranularitySpec() == null ||
config.getGranularitySpec().getSegmentGranularity() == null) {
// Determines segmentGranularity from the segmentsToCompact
- // Each batch of segmentToCompact from CompactionSegmentIterator will
contains the same interval as
+ // Each batch of segmentToCompact from CompactionSegmentIterator will
contain the same interval as
// segmentGranularity is not set in the compaction config
Interval interval = segmentsToCompact.get(0).getInterval();
if (segmentsToCompact.stream().allMatch(segment ->
interval.overlaps(segment.getInterval()))) {
diff --git
a/server/src/main/java/org/apache/druid/server/coordinator/duty/MarkEternityTombstonesAsUnused.java
b/server/src/main/java/org/apache/druid/server/coordinator/duty/MarkEternityTombstonesAsUnused.java
new file mode 100644
index 00000000000..8a2e6c9dfd9
--- /dev/null
+++
b/server/src/main/java/org/apache/druid/server/coordinator/duty/MarkEternityTombstonesAsUnused.java
@@ -0,0 +1,178 @@
+/*
+ * 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.server.coordinator.duty;
+
+import com.google.common.base.Optional;
+import org.apache.druid.client.DataSourcesSnapshot;
+import org.apache.druid.java.util.common.DateTimes;
+import org.apache.druid.java.util.common.Intervals;
+import org.apache.druid.java.util.common.granularity.Granularities;
+import org.apache.druid.java.util.common.logger.Logger;
+import org.apache.druid.server.coordinator.DruidCoordinatorRuntimeParams;
+import org.apache.druid.server.coordinator.stats.CoordinatorRunStats;
+import org.apache.druid.server.coordinator.stats.Dimension;
+import org.apache.druid.server.coordinator.stats.RowKey;
+import org.apache.druid.server.coordinator.stats.Stats;
+import org.apache.druid.timeline.DataSegment;
+import org.apache.druid.timeline.Partitions;
+import org.apache.druid.timeline.SegmentId;
+import org.apache.druid.timeline.SegmentTimeline;
+import org.apache.druid.timeline.partition.TombstoneShardSpec;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Mark eternity tombstones not overshadowed by currently served segments as
unused. A candidate segment must fit all
+ * the criteria:
+ * <li> It is a tombstone that starts at {@link DateTimes#MIN} or ends at
{@link DateTimes#MAX} </li>
+ * <li> It does not overlap with any overshadowed segment in the datasource
</li>
+ * <li> It has has 0 core partitions i.e., {@link
TombstoneShardSpec#getNumCorePartitions()} == 0</li>
+ *
+ * <p>
+ * Only infinite-interval tombstones are considered as candidate segments in
this duty because they
+ * don't honor the preferred segment granularity specified at ingest time to
cover an underlying segment with
+ * {@link Granularities#ALL} as it can generate too many segments per time
chunk and cause an OOM. The infinite-interval
+ * tombstones make it hard to append data on the end of a data set that
started out with an {@link Granularities#ALL} eternity
+ * and then moved to actual time grains, so the compromise is that the
coordinator will remove these segments as long as it
+ * doesn't overlap any other segment.
+ * </p>
+ * <p>
+ * The overlapping condition is necessary as a candidate segment can overlap
with an overshadowed segment, and the latter
+ * needs to be marked as unused first by {@link
MarkOvershadowedSegmentsAsUnused} duty before the tombstone candidate
+ * can be marked as unused by {@link MarkEternityTombstonesAsUnused} duty.
+ *</p>
+ * <p>
+ * Only tombstones with 0 core partitions is considered as candidate segments.
Earlier generation tombstones with 1 core
+ * partition (i.e., {@link TombstoneShardSpec#getNumCorePartitions()} == 1)
are ignored by this duty because it can potentially
+ * cause data loss in a concurrent append and replace scenario and needs to be
manually cleaned up. See this
+ * <a href="https://github.com/apache/druid/pull/15379">for details</a>.
+ * </p>
+ */
+public class MarkEternityTombstonesAsUnused implements CoordinatorDuty
+{
+ private static final Logger log = new
Logger(MarkEternityTombstonesAsUnused.class);
+
+ private final SegmentDeleteHandler deleteHandler;
+
+ public MarkEternityTombstonesAsUnused(final SegmentDeleteHandler
deleteHandler)
+ {
+ this.deleteHandler = deleteHandler;
+ }
+
+ @Override
+ public DruidCoordinatorRuntimeParams run(final DruidCoordinatorRuntimeParams
params)
+ {
+ DataSourcesSnapshot dataSourcesSnapshot = params.getDataSourcesSnapshot();
+
+ final Map<String, Set<SegmentId>>
datasourceToNonOvershadowedEternityTombstones =
+ determineNonOvershadowedEternityTombstones(
+ dataSourcesSnapshot
+ );
+
+ if (datasourceToNonOvershadowedEternityTombstones.size() == 0) {
+ log.debug("No non-overshadowed eternity tombstones found.");
+ return params;
+ }
+
+ log.debug("Found [%d] datasource containing non-overshadowed eternity
tombstones[%s]",
+ datasourceToNonOvershadowedEternityTombstones.size(),
datasourceToNonOvershadowedEternityTombstones
+ );
+
+ final CoordinatorRunStats stats = params.getCoordinatorStats();
+ datasourceToNonOvershadowedEternityTombstones.forEach((datasource,
nonOvershadowedEternityTombstones) -> {
+ final RowKey datasourceKey = RowKey.of(Dimension.DATASOURCE, datasource);
+ stats.add(Stats.Segments.UNNEEDED_ETERNITY_TOMBSTONE, datasourceKey,
nonOvershadowedEternityTombstones.size());
+ final int unusedCount =
deleteHandler.markSegmentsAsUnused(nonOvershadowedEternityTombstones);
+ log.info(
+ "Successfully marked [%d] non-overshadowed eternity tombstones[%s]
of datasource[%s] as unused.",
+ unusedCount,
+ nonOvershadowedEternityTombstones,
+ datasource
+ );
+ });
+
+ return params;
+ }
+
+ /**
+ * Computes the set of unneeded eternity tombstones per datasource using the
datasources snapshot. The computation is
+ * as follows:
+ *
+ * <li> Determine the set of used and non-overshadowed segments from the
used segments' timeline. </li>
+ * <li> For each such candidate segment that is a tombstone with an infinite
start or end, look at the set of overshadowed
+ * segments to see if any of the intervals overlaps with the candidate
segment.
+ * <li> If there is no overlap, add the candidate segment to the eternity
segments result set. </li>
+ * There can at most be two such candidate tombstones per datasource -- one
that starts at {@link DateTimes#MIN}
+ * and another that ends at {@link DateTimes#MAX}. </li>
+ * </p>
+ *
+ * @param dataSourcesSnapshot the datasources snapshot for segments timeline
+ * @return the set of non-overshadowed eternity tombstones grouped by
datasource
+ */
+ private Map<String, Set<SegmentId>>
determineNonOvershadowedEternityTombstones(final DataSourcesSnapshot
dataSourcesSnapshot)
+ {
+ final Map<String, Set<SegmentId>>
datasourceToNonOvershadowedEternityTombstones = new HashMap<>();
+
+ dataSourcesSnapshot.getDataSourcesMap().keySet().forEach((datasource) -> {
+ final SegmentTimeline usedSegmentsTimeline
+ =
dataSourcesSnapshot.getUsedSegmentsTimelinesPerDataSource().get(datasource);
+
+ final Optional<Set<DataSegment>> usedNonOvershadowedSegments =
+ Optional.fromNullable(usedSegmentsTimeline)
+ .transform(timeline ->
timeline.findNonOvershadowedObjectsInInterval(
+ Intervals.ETERNITY,
+ Partitions.ONLY_COMPLETE
+ ));
+
+ if (usedNonOvershadowedSegments.isPresent()) {
+ usedNonOvershadowedSegments.get().forEach(candidateSegment -> {
+ if (isNewGenerationEternityTombstone(candidateSegment)) {
+ boolean overlaps =
dataSourcesSnapshot.getOvershadowedSegments().stream()
+ .filter(overshadowedSegment
->
+
candidateSegment.getDataSource()
+
.equals(overshadowedSegment.getDataSource()))
+ .anyMatch(
+ overshadowedSegment ->
+
candidateSegment.getInterval()
+
.overlaps(overshadowedSegment.getInterval())
+ );
+ if (!overlaps) {
+ datasourceToNonOvershadowedEternityTombstones
+ .computeIfAbsent(datasource, ds -> new HashSet<>())
+ .add(candidateSegment.getId());
+ }
+ }
+ });
+ }
+ });
+
+ return datasourceToNonOvershadowedEternityTombstones;
+ }
+
+ private boolean isNewGenerationEternityTombstone(final DataSegment segment)
+ {
+ return segment.isTombstone() &&
segment.getShardSpec().getNumCorePartitions() == 0 && (
+ DateTimes.MIN.equals(segment.getInterval().getStart()) ||
+ DateTimes.MAX.equals(segment.getInterval().getEnd()));
+ }
+}
diff --git
a/server/src/main/java/org/apache/druid/server/coordinator/stats/Stats.java
b/server/src/main/java/org/apache/druid/server/coordinator/stats/Stats.java
index 539d58d5594..a964a28aab7 100644
--- a/server/src/main/java/org/apache/druid/server/coordinator/stats/Stats.java
+++ b/server/src/main/java/org/apache/druid/server/coordinator/stats/Stats.java
@@ -57,6 +57,8 @@ public class Stats
= CoordinatorStat.toDebugAndEmit("unneeded", "segment/unneeded/count");
public static final CoordinatorStat OVERSHADOWED
= CoordinatorStat.toDebugAndEmit("overshadowed",
"segment/overshadowed/count");
+ public static final CoordinatorStat UNNEEDED_ETERNITY_TOMBSTONE
+ = CoordinatorStat.toDebugAndEmit("unneededEternityTombstone",
"segment/unneededEternityTombstone/count");
}
public static class SegmentQueue
diff --git
a/server/src/test/java/org/apache/druid/server/coordinator/duty/MarkEternityTombstonesAsUnusedTest.java
b/server/src/test/java/org/apache/druid/server/coordinator/duty/MarkEternityTombstonesAsUnusedTest.java
new file mode 100644
index 00000000000..d6dd4f98206
--- /dev/null
+++
b/server/src/test/java/org/apache/druid/server/coordinator/duty/MarkEternityTombstonesAsUnusedTest.java
@@ -0,0 +1,501 @@
+/*
+ * 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.server.coordinator.duty;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Sets;
+import org.apache.druid.client.DruidServer;
+import org.apache.druid.java.util.common.DateTimes;
+import org.apache.druid.java.util.common.Intervals;
+import org.apache.druid.server.coordination.ServerType;
+import org.apache.druid.server.coordinator.CoordinatorDynamicConfig;
+import org.apache.druid.server.coordinator.DruidCluster;
+import org.apache.druid.server.coordinator.DruidCoordinatorRuntimeParams;
+import org.apache.druid.server.coordinator.ServerHolder;
+import org.apache.druid.server.coordinator.balancer.RandomBalancerStrategy;
+import org.apache.druid.server.coordinator.loading.SegmentLoadQueueManager;
+import org.apache.druid.server.coordinator.loading.TestLoadQueuePeon;
+import
org.apache.druid.server.coordinator.simulate.TestSegmentsMetadataManager;
+import org.apache.druid.server.coordinator.stats.CoordinatorRunStats;
+import org.apache.druid.server.coordinator.stats.Dimension;
+import org.apache.druid.server.coordinator.stats.RowKey;
+import org.apache.druid.server.coordinator.stats.Stats;
+import org.apache.druid.timeline.DataSegment;
+import org.apache.druid.timeline.SegmentTimeline;
+import org.apache.druid.timeline.partition.TombstoneShardSpec;
+import org.joda.time.Interval;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Set;
+
+public class MarkEternityTombstonesAsUnusedTest
+{
+ private final String ds1 = "foo";
+ private final String ds2 = "bar";
+
+ // The verbose variable names follow the convention for readability in the
tests:
+ // datasource name - shard spec type - interval - version
+ private final DataSegment ds1NumberedSegmentMinToMaxV0 =
DataSegment.builder().dataSource(ds1)
+
.interval(Intervals.ETERNITY)
+
.version("0")
+ .size(0)
+ .build();
+
+ private final DataSegment ds1TombstoneSegmentMinToMaxV1 =
DataSegment.builder().dataSource(ds1)
+
.shardSpec(new TombstoneShardSpec())
+
.interval(Intervals.ETERNITY)
+
.version("1")
+ .size(0)
+
.build();
+
+ private final DataSegment ds1TombstoneSegmentMinTo2000V1 =
DataSegment.builder().dataSource(ds1)
+
.shardSpec(new TombstoneShardSpec())
+
.interval(new Interval(DateTimes.MIN, DateTimes.of("2000")))
+
.version("1")
+
.size(0)
+
.build();
+
+ private final DataSegment ds1NumberedSegment2000To2001V1 =
DataSegment.builder().dataSource(ds1)
+
.interval(Intervals.of("2000/2001"))
+
.version("1")
+
.size(0)
+
.build();
+
+ private final DataSegment ds1TombstoneSegment2001ToMaxV1 =
DataSegment.builder().dataSource(ds1)
+
.shardSpec(new TombstoneShardSpec())
+
.interval(new Interval(DateTimes.of("2001"), DateTimes.MAX))
+
.version("1")
+
.size(0)
+
.build();
+
+ final DataSegment ds1TombstoneSegmentMinTo2000V2 =
ds1TombstoneSegmentMinTo2000V1.withVersion("2");
+ final DataSegment ds1TombstoneSegment2001ToMaxV2 =
ds1TombstoneSegment2001ToMaxV1.withVersion("2");
+
+ private final DataSegment ds2TombstoneSegment1995To2005V0 =
DataSegment.builder().dataSource(ds2)
+
.shardSpec(new TombstoneShardSpec())
+
.interval(Intervals.of("1995/2005"))
+
.version("0")
+
.size(0)
+
.build();
+
+ private final DataSegment ds2TombstoneSegmentMinTo2000V1 =
DataSegment.builder().dataSource(ds2)
+
.shardSpec(new TombstoneShardSpec())
+
.interval(new Interval(DateTimes.MIN, DateTimes.of("2000")))
+
.version("1")
+
.size(0)
+
.build();
+
+ private final DataSegment ds2NumberedSegment3000To4000V1 =
DataSegment.builder().dataSource(ds2)
+
.interval(Intervals.of("3000/4000"))
+
.version("1")
+
.size(0)
+
.build();
+
+ private final DataSegment ds2TombstoneSegment4000ToMaxV1 =
DataSegment.builder().dataSource(ds2)
+
.shardSpec(new TombstoneShardSpec())
+
.interval(new Interval(DateTimes.of("4000"), DateTimes.MAX))
+
.version("1")
+
.size(0)
+
.build();
+
+ private final DataSegment ds2TombstoneSegment4000To4001V1 =
DataSegment.builder().dataSource(ds2)
+
.shardSpec(new TombstoneShardSpec())
+
.interval(new Interval(DateTimes.of("4000"), DateTimes.of("4001")))
+
.version("1")
+
.size(0)
+
.build();
+
+ private final DataSegment ds2NumberedSegment1999To2500V2 =
DataSegment.builder().dataSource(ds2)
+
.interval(Intervals.of("1999/2500"))
+
.version("2")
+
.size(0)
+
.build();
+
+
+ /**
+ * An old generation tombstone with 1 core partition instead of the default
0.
+ */
+ private final DataSegment ds2TombstoneSegment4000ToMaxV1With1CorePartition =
ds2TombstoneSegment4000ToMaxV1.withShardSpec(
+ new TombstoneShardSpec() {
+ @Override
+ @JsonProperty("partitions")
+ public int getNumCorePartitions()
+ {
+ return 1;
+ }
+ });
+
+
+ private TestSegmentsMetadataManager segmentsMetadataManager;
+
+ @Before
+ public void setup()
+ {
+ segmentsMetadataManager = new TestSegmentsMetadataManager();
+ }
+
+ /**
+ * Half-inifinity tombstones overlapping with overshadowed segments
shouldn't be marked as unused.
+ */
+ @Test
+ public void testCandidateTombstonesWithUsedOvershadowedSegments()
+ {
+ final ImmutableList<DataSegment> allUsedSegments = ImmutableList.of(
+ ds1NumberedSegmentMinToMaxV0,
+ ds1TombstoneSegmentMinTo2000V1,
+ ds1NumberedSegment2000To2001V1,
+ ds1TombstoneSegment2001ToMaxV1
+ );
+ final ImmutableList<DataSegment> expectedUsedSegments =
ImmutableList.copyOf(allUsedSegments);
+ final DruidCoordinatorRuntimeParams params =
initializeServerAndGetParams(allUsedSegments);
+
+ final SegmentTimeline timeline =
segmentsMetadataManager.getSnapshotOfDataSourcesWithAllUsedSegments()
+
.getUsedSegmentsTimelinesPerDataSource()
+ .get(ds1);
+
+ // Verify that the half-infinity tombstone is overshadowed and everything
else is not
+ Assert.assertTrue(timeline.isOvershadowed(ds1NumberedSegmentMinToMaxV0));
+
Assert.assertFalse(timeline.isOvershadowed(ds1TombstoneSegmentMinTo2000V1));
+
Assert.assertFalse(timeline.isOvershadowed(ds1NumberedSegment2000To2001V1));
+
Assert.assertFalse(timeline.isOvershadowed(ds1TombstoneSegment2001ToMaxV1));
+
+ runEternityTombstonesDutyAndVerify(params, allUsedSegments,
expectedUsedSegments);
+ }
+
+ /**
+ * Inifinity tombstones overlapping with overshadowed segments shouldn't be
marked as unused.
+ */
+ @Test
+ public void testCandidateTombstonesWithUsedOvershadowedSegments2()
+ {
+ final ImmutableList<DataSegment> allUsedSegments = ImmutableList.of(
+ ds1NumberedSegmentMinToMaxV0,
+ ds1TombstoneSegmentMinToMaxV1
+ );
+ final ImmutableList<DataSegment> expectedUsedSegments =
ImmutableList.copyOf(allUsedSegments);
+ final DruidCoordinatorRuntimeParams params =
initializeServerAndGetParams(allUsedSegments);
+
+ final SegmentTimeline ds1Timeline =
segmentsMetadataManager.getSnapshotOfDataSourcesWithAllUsedSegments()
+
.getUsedSegmentsTimelinesPerDataSource()
+ .get(ds1);
+
+
Assert.assertTrue(ds1Timeline.isOvershadowed(ds1NumberedSegmentMinToMaxV0));
+
Assert.assertFalse(ds1Timeline.isOvershadowed(ds1TombstoneSegmentMinToMaxV1));
+
+ runEternityTombstonesDutyAndVerify(params, allUsedSegments,
expectedUsedSegments);
+ }
+
+ /**
+ * Half-inifinity tombstones that don't overlap with an overshadowed segment
should be marked as unused.
+ */
+ @Test
+ public void testCandidateTombstonesWithNoUsedOvershadowedSegments()
+ {
+ final ImmutableList<DataSegment> allUsedSegments = ImmutableList.of(
+ ds1TombstoneSegmentMinTo2000V1,
+ ds1NumberedSegment2000To2001V1,
+ ds1TombstoneSegment2001ToMaxV1
+ );
+ final ImmutableList<DataSegment> expectedUsedSegments = ImmutableList.of(
+ ds1NumberedSegment2000To2001V1
+ );
+ final DruidCoordinatorRuntimeParams params =
initializeServerAndGetParams(allUsedSegments);
+
+ SegmentTimeline timeline =
segmentsMetadataManager.getSnapshotOfDataSourcesWithAllUsedSegments()
+
.getUsedSegmentsTimelinesPerDataSource()
+ .get(ds1);
+
+
Assert.assertFalse(timeline.isOvershadowed(ds1TombstoneSegmentMinTo2000V1));
+
Assert.assertFalse(timeline.isOvershadowed(ds1NumberedSegment2000To2001V1));
+
Assert.assertFalse(timeline.isOvershadowed(ds1TombstoneSegmentMinTo2000V2));
+
+ runEternityTombstonesDutyAndVerify(params, allUsedSegments,
expectedUsedSegments);
+ }
+
+ /**
+ * Full-infinity tombstones that has no overlap should be marked as unused.
+ */
+ @Test
+ public void testCandidateTombstonesWithNoUsedOvershadowedSegments2()
+ {
+ final ImmutableList<DataSegment> allUsedSegments = ImmutableList.of(
+ ds1TombstoneSegmentMinToMaxV1
+ );
+ final ImmutableList<DataSegment> expectedUsedSegments = ImmutableList.of();
+ final DruidCoordinatorRuntimeParams params =
initializeServerAndGetParams(allUsedSegments);
+
+ final SegmentTimeline ds1Timeline =
segmentsMetadataManager.getSnapshotOfDataSourcesWithAllUsedSegments()
+
.getUsedSegmentsTimelinesPerDataSource()
+ .get(ds1);
+
+
Assert.assertFalse(ds1Timeline.isOvershadowed(ds1TombstoneSegmentMinToMaxV1));
+
+ runEternityTombstonesDutyAndVerify(params, allUsedSegments,
expectedUsedSegments);
+ }
+
+ /**
+ * Half-inifinity tombstones that overlap with overshadowed used segments
shouldn't be marked as unused.
+ */
+ @Test
+ public void testCandiateTombstonesWithManyOvershadowedSegments()
+ {
+ final ImmutableList<DataSegment> allUsedSegments = ImmutableList.of(
+ ds1TombstoneSegmentMinTo2000V1,
+ ds1NumberedSegment2000To2001V1,
+ ds1TombstoneSegment2001ToMaxV1,
+ ds1TombstoneSegmentMinTo2000V2,
+ ds1TombstoneSegment2001ToMaxV2
+ );
+ final ImmutableList<DataSegment> expectedUsedSegments =
ImmutableList.copyOf(allUsedSegments);
+
+ final DruidCoordinatorRuntimeParams params =
initializeServerAndGetParams(allUsedSegments);
+
+ SegmentTimeline timeline =
segmentsMetadataManager.getSnapshotOfDataSourcesWithAllUsedSegments()
+
.getUsedSegmentsTimelinesPerDataSource()
+ .get(ds1);
+
+ Assert.assertTrue(timeline.isOvershadowed(ds1TombstoneSegmentMinTo2000V1));
+ Assert.assertTrue(timeline.isOvershadowed(ds1TombstoneSegment2001ToMaxV1));
+
Assert.assertFalse(timeline.isOvershadowed(ds1NumberedSegment2000To2001V1));
+
Assert.assertFalse(timeline.isOvershadowed(ds1TombstoneSegmentMinTo2000V2));
+
Assert.assertFalse(timeline.isOvershadowed(ds1TombstoneSegment2001ToMaxV2));
+
+ runEternityTombstonesDutyAndVerify(params, allUsedSegments,
expectedUsedSegments);
+ }
+
+ /**
+ * <p>
+ * Datasource 1 has the following half-infinity tombstones:
+ * <li> {@link #ds1TombstoneSegmentMinTo2000V1} is overshadowed by {@link
#ds1TombstoneSegmentMinTo2000V2}, so cannot be marked as unused. </li>
+ * <li> {@link #ds1TombstoneSegmentMinTo2000V2} overlaps with {@link
#ds1TombstoneSegmentMinTo2000V1}, so cannot be marked as unused. </li>
+ * <li> {@link #ds1TombstoneSegment2001ToMaxV1} doesn't overlap with any
other segment and can be marked as unused. </li>
+ *
+ * Note that {@link #ds1TombstoneSegmentMinTo2000V1} will be marked as
unused by {@link MarkOvershadowedSegmentsAsUnused} duty
+ * and then subsequently {@link #ds1TombstoneSegmentMinTo2000V2} will be
marked as unused by the {@link MarkEternityTombstonesAsUnused}
+ * duty eventually.
+ * </p>
+ *
+ * <p>
+ * Datasource 2 has half eternity tombstones that don't overlap with any
other segment, so both can be removed.
+ * </p>
+ */
+ @Test
+ public void testCandidateTombstonesInMultipleDatasources()
+ {
+ final ImmutableList<DataSegment> allUsedSegments = ImmutableList.of(
+ ds1TombstoneSegmentMinTo2000V1,
+ ds1NumberedSegment2000To2001V1,
+ ds1TombstoneSegment2001ToMaxV1,
+ ds1TombstoneSegmentMinTo2000V2,
+ ds2TombstoneSegmentMinTo2000V1,
+ ds2NumberedSegment3000To4000V1,
+ ds2TombstoneSegment4000ToMaxV1
+ );
+
+ final ImmutableList<DataSegment> expectedUsedSegments = ImmutableList.of(
+ ds1TombstoneSegmentMinTo2000V1,
+ ds1NumberedSegment2000To2001V1,
+ ds1TombstoneSegmentMinTo2000V2,
+ ds2NumberedSegment3000To4000V1
+ );
+ final DruidCoordinatorRuntimeParams params =
initializeServerAndGetParams(allUsedSegments);
+
+ SegmentTimeline ds1Timeline =
segmentsMetadataManager.getSnapshotOfDataSourcesWithAllUsedSegments()
+
.getUsedSegmentsTimelinesPerDataSource()
+ .get(ds1);
+
Assert.assertTrue(ds1Timeline.isOvershadowed(ds1TombstoneSegmentMinTo2000V1));
+
Assert.assertFalse(ds1Timeline.isOvershadowed(ds1NumberedSegment2000To2001V1));
+
Assert.assertFalse(ds1Timeline.isOvershadowed(ds1TombstoneSegment2001ToMaxV1));
+
Assert.assertFalse(ds1Timeline.isOvershadowed(ds1TombstoneSegmentMinTo2000V2));
+
+ SegmentTimeline ds2Timeline =
segmentsMetadataManager.getSnapshotOfDataSourcesWithAllUsedSegments()
+
.getUsedSegmentsTimelinesPerDataSource()
+ .get(ds2);
+
Assert.assertFalse(ds2Timeline.isOvershadowed(ds2TombstoneSegmentMinTo2000V1));
+
Assert.assertFalse(ds2Timeline.isOvershadowed(ds2NumberedSegment3000To4000V1));
+
Assert.assertFalse(ds2Timeline.isOvershadowed(ds2TombstoneSegment4000ToMaxV1));
+
+ runEternityTombstonesDutyAndVerify(params, allUsedSegments,
expectedUsedSegments);
+ }
+
+ /**
+ * Half-inifinity tombstones that partially overlaps with other segments
(not overshadowed) can still
+ * be marked as used.
+ */
+ @Test
+ public void testCandidateTombstonesWithPartiallyOverlappingSegment()
+ {
+ final ImmutableList<DataSegment> allUsedSegments = ImmutableList.of(
+ ds2TombstoneSegment1995To2005V0,
+ ds2TombstoneSegmentMinTo2000V1,
+ ds2NumberedSegment3000To4000V1,
+ ds2TombstoneSegment4000ToMaxV1
+ );
+
+ final ImmutableList<DataSegment> expectedUsedSegments = ImmutableList.of(
+ ds2TombstoneSegment1995To2005V0,
+ ds2NumberedSegment3000To4000V1
+ );
+ final DruidCoordinatorRuntimeParams params =
initializeServerAndGetParams(allUsedSegments);
+
+ final SegmentTimeline ds2Timeline =
segmentsMetadataManager.getSnapshotOfDataSourcesWithAllUsedSegments()
+
.getUsedSegmentsTimelinesPerDataSource()
+ .get(ds2);
+
+
Assert.assertFalse(ds2Timeline.isOvershadowed(ds2TombstoneSegment1995To2005V0));
+
Assert.assertFalse(ds2Timeline.isOvershadowed(ds2TombstoneSegmentMinTo2000V1));
+
Assert.assertFalse(ds2Timeline.isOvershadowed(ds2NumberedSegment3000To4000V1));
+
Assert.assertFalse(ds2Timeline.isOvershadowed(ds2TombstoneSegment4000ToMaxV1));
+
+ runEternityTombstonesDutyAndVerify(params, allUsedSegments,
expectedUsedSegments);
+ }
+
+ /**
+ * Half-inifinity tombstones that partially overlaps with other used
segments would still be considered as used and
+ * non-overshadowed and can be marked as unused.
+ */
+ @Test
+ public void
testCandidateTombstonesWithPartiallyOverlappingHigherVersionUsedSegment()
+ {
+ final ImmutableList<DataSegment> allUsedSegments = ImmutableList.of(
+ ds2TombstoneSegmentMinTo2000V1,
+ ds2NumberedSegment3000To4000V1,
+ ds2TombstoneSegment4000ToMaxV1,
+ ds2NumberedSegment1999To2500V2
+ );
+
+ final ImmutableList<DataSegment> expectedUsedSegments = ImmutableList.of(
+ ds2NumberedSegment3000To4000V1,
+ ds2NumberedSegment1999To2500V2
+ );
+
+ final DruidCoordinatorRuntimeParams params =
initializeServerAndGetParams(allUsedSegments);
+
+ final SegmentTimeline ds2Timeline =
segmentsMetadataManager.getSnapshotOfDataSourcesWithAllUsedSegments()
+
.getUsedSegmentsTimelinesPerDataSource()
+ .get(ds2);
+
Assert.assertFalse(ds2Timeline.isOvershadowed(ds2TombstoneSegmentMinTo2000V1));
+
Assert.assertFalse(ds2Timeline.isOvershadowed(ds2NumberedSegment3000To4000V1));
+
Assert.assertFalse(ds2Timeline.isOvershadowed(ds2TombstoneSegment4000ToMaxV1));
+
Assert.assertFalse(ds2Timeline.isOvershadowed(ds2NumberedSegment1999To2500V2));
+
+ runEternityTombstonesDutyAndVerify(params, allUsedSegments,
expectedUsedSegments);
+ }
+
+ /**
+ * Finite-interval tombstones shouldn't be marked as unused.
+ */
+ @Test
+ public void testFiniteIntervalTombstone()
+ {
+ final ImmutableList<DataSegment> allUsedSegments = ImmutableList.of(
+ ds2TombstoneSegment4000To4001V1
+ );
+ final ImmutableList<DataSegment> expectedUsedSegments = ImmutableList.of(
+ ds2TombstoneSegment4000To4001V1
+ );
+ final DruidCoordinatorRuntimeParams params =
initializeServerAndGetParams(allUsedSegments);
+
+ final SegmentTimeline ds2Timeline =
segmentsMetadataManager.getSnapshotOfDataSourcesWithAllUsedSegments()
+
.getUsedSegmentsTimelinesPerDataSource()
+ .get(ds2);
+
+
Assert.assertFalse(ds2Timeline.isOvershadowed(ds2TombstoneSegment4000To4001V1));
+
+ runEternityTombstonesDutyAndVerify(params, allUsedSegments,
expectedUsedSegments);
+ }
+
+ /**
+ * Tombstones with 1 core partition i.e., {@link
TombstoneShardSpec#getNumCorePartitions()} == 1 shouldn't be
+ * marked as unused.
+ */
+ @Test
+ public void testTombstoneWith1CorePartition()
+ {
+ final ImmutableList<DataSegment> allUsedSegments = ImmutableList.of(
+ ds2TombstoneSegment4000ToMaxV1With1CorePartition
+ );
+ final ImmutableList<DataSegment> expectedUsedSegments = ImmutableList.of(
+ ds2TombstoneSegment4000ToMaxV1With1CorePartition
+ );
+ final DruidCoordinatorRuntimeParams params =
initializeServerAndGetParams(allUsedSegments);
+
+ final SegmentTimeline ds2Timeline =
segmentsMetadataManager.getSnapshotOfDataSourcesWithAllUsedSegments()
+
.getUsedSegmentsTimelinesPerDataSource()
+ .get(ds2);
+
+
Assert.assertFalse(ds2Timeline.isOvershadowed(ds2TombstoneSegment4000ToMaxV1With1CorePartition));
+
+ runEternityTombstonesDutyAndVerify(params, allUsedSegments,
expectedUsedSegments);
+ }
+
+ private DruidCoordinatorRuntimeParams initializeServerAndGetParams(final
ImmutableList<DataSegment> segments)
+ {
+ final DruidServer druidServer = new DruidServer("", "", "", 0L,
ServerType.fromString("broker"), "", 0);
+ for (final DataSegment segment : segments) {
+ segmentsMetadataManager.addSegment(segment);
+ druidServer.addDataSegment(segment);
+ }
+
+ final DruidCluster druidCluster = DruidCluster
+ .builder()
+ .add(new ServerHolder(druidServer.toImmutableDruidServer(), new
TestLoadQueuePeon()))
+ .build();
+
+ final DruidCoordinatorRuntimeParams params = DruidCoordinatorRuntimeParams
+ .newBuilder(DateTimes.nowUtc())
+ .withDataSourcesSnapshot(
+
segmentsMetadataManager.getSnapshotOfDataSourcesWithAllUsedSegments()
+ )
+ .withDruidCluster(druidCluster)
+ .withDynamicConfigs(
+
CoordinatorDynamicConfig.builder().withMarkSegmentAsUnusedDelayMillis(0).build()
+ )
+ .withBalancerStrategy(new RandomBalancerStrategy())
+ .withSegmentAssignerUsing(new SegmentLoadQueueManager(null, null))
+ .build();
+
+ return params;
+ }
+
+ private void runEternityTombstonesDutyAndVerify(
+ DruidCoordinatorRuntimeParams params,
+ final ImmutableList<DataSegment> allUsedSegments,
+ final ImmutableList<DataSegment> expectedUsedSegments
+ )
+ {
+ params = new
MarkEternityTombstonesAsUnused(segmentsMetadataManager::markSegmentsAsUnused).run(params);
+
+ final Set<DataSegment> actualUsedSegments =
Sets.newHashSet(segmentsMetadataManager.iterateAllUsedSegments());
+
+ Assert.assertEquals(expectedUsedSegments.size(),
actualUsedSegments.size());
+ Assert.assertTrue(actualUsedSegments.containsAll(expectedUsedSegments));
+
+ final CoordinatorRunStats runStats = params.getCoordinatorStats();
+ Assert.assertEquals(
+ allUsedSegments.size() - expectedUsedSegments.size(),
+ runStats.get(Stats.Segments.UNNEEDED_ETERNITY_TOMBSTONE,
RowKey.of(Dimension.DATASOURCE, ds1)) +
+ runStats.get(Stats.Segments.UNNEEDED_ETERNITY_TOMBSTONE,
RowKey.of(Dimension.DATASOURCE, ds2))
+ );
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]